@opengsd/gsd-path 1.5.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 +138 -0
- package/DOCS.md +4 -3
- package/FULL.md +64 -4
- package/HOOKS.md +63 -2
- package/MIGRATE.md +12 -0
- package/QUICK.md +4 -2
- package/README.md +67 -15
- package/RUNTIME.md +23 -5
- package/UPDATE.md +35 -0
- package/WORKFLOW.md +58 -3
- package/package.json +1 -1
- package/scripts/_common.py +107 -1
- package/scripts/build_recovery.py +62 -30
- package/scripts/build_state.py +195 -5
- package/scripts/check_handoffs.py +92 -30
- package/scripts/check_task_briefs.py +19 -14
- package/scripts/detect_project.py +17 -2
- package/scripts/discussion_records.py +9 -1
- package/scripts/discussion_validate.py +7 -6
- package/scripts/dispatch_driver.py +157 -31
- package/scripts/git_guard.py +24 -5
- package/scripts/guard_hook.py +1216 -47
- package/scripts/install.mjs +76 -3
- package/scripts/install.py +182 -37
- package/scripts/integration.py +98 -64
- package/scripts/isolation.py +148 -11
- package/scripts/members.py +68 -25
- package/scripts/migrate_core.py +2 -0
- package/scripts/model_policy.py +5 -1
- package/scripts/pipeline_diagnose.py +17 -2
- package/scripts/pipeline_git.py +68 -30
- package/scripts/pipeline_state.py +28 -6
- package/scripts/pipeline_undo.py +17 -8
- 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 +1 -0
- package/scripts/state_checkpoint.py +30 -9
- package/scripts/state_promote.py +15 -4
- package/scripts/task_context.py +2 -0
- package/scripts/workflow_run.py +9 -2
- package/skills/gsd-path/DEFINE.md +6 -2
- package/skills/gsd-path/PLAN.md +3 -0
- package/skills/gsd-path/ROADMAP.md +16 -6
- package/skills/gsd-path/SHIP.md +26 -4
- package/skills/gsd-path/SKILL.md +52 -24
- package/skills/gsd-path/references/build-native.md +96 -9
- package/skills/gsd-path/references/build-recovery.md +13 -0
- package/skills/gsd-path/references/coder.md +7 -3
- package/skills/gsd-path/references/operating-rules.md +11 -3
- package/skills/gsd-path/references/planner.md +13 -1
- package/skills/gsd-path/references/reviewer-final.md +5 -5
- package/skills/gsd-path/references/reviewer-wave.md +46 -13
- package/skills/gsd-path/references/runtime-selection.md +9 -0
- package/skills/gsd-path/scripts/_common.py +107 -1
- package/skills/gsd-path/scripts/build_recovery.py +62 -30
- package/skills/gsd-path/scripts/build_state.py +195 -5
- package/skills/gsd-path/scripts/check_handoffs.py +92 -30
- package/skills/gsd-path/scripts/check_task_briefs.py +19 -14
- package/skills/gsd-path/scripts/detect_project.py +17 -2
- package/skills/gsd-path/scripts/discussion_records.py +9 -1
- package/skills/gsd-path/scripts/discussion_validate.py +7 -6
- package/skills/gsd-path/scripts/dispatch_driver.py +157 -31
- package/skills/gsd-path/scripts/git_guard.py +24 -5
- package/skills/gsd-path/scripts/integration.py +98 -64
- package/skills/gsd-path/scripts/isolation.py +148 -11
- package/skills/gsd-path/scripts/members.py +68 -25
- package/skills/gsd-path/scripts/model_policy.py +5 -1
- package/skills/gsd-path/scripts/pipeline_git.py +68 -30
- package/skills/gsd-path/scripts/pipeline_state.py +28 -6
- 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 +30 -9
- package/skills/gsd-path/scripts/state_promote.py +15 -4
- package/skills/gsd-path/scripts/task_context.py +2 -0
- package/skills/gsd-path/scripts/workflow_run.py +9 -2
- package/skills/gsd-path/templates/final-review.md +5 -5
- package/skills/gsd-path/templates/state.md +1 -0
- package/skills/gsd-path/templates/task.md +1 -0
- package/skills/gsd-path/templates/wave-review.md +18 -9
- package/skills/gsd-path-build/references/build-native.md +96 -9
- 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/operating-rules.md +11 -3
- package/skills/gsd-path-build/references/reviewer-final.md +5 -5
- package/skills/gsd-path-build/references/reviewer-wave.md +46 -13
- package/skills/gsd-path-build/references/runtime-selection.md +9 -0
- package/skills/gsd-path-build/scripts/_common.py +107 -1
- package/skills/gsd-path-build/scripts/build_recovery.py +62 -30
- package/skills/gsd-path-build/scripts/build_state.py +195 -5
- package/skills/gsd-path-build/scripts/check_handoffs.py +92 -30
- package/skills/gsd-path-build/scripts/check_task_briefs.py +19 -14
- package/skills/gsd-path-build/scripts/detect_project.py +17 -2
- package/skills/gsd-path-build/scripts/discussion_records.py +9 -1
- package/skills/gsd-path-build/scripts/discussion_validate.py +7 -6
- package/skills/gsd-path-build/scripts/dispatch_driver.py +157 -31
- package/skills/gsd-path-build/scripts/integration.py +98 -64
- package/skills/gsd-path-build/scripts/isolation.py +148 -11
- package/skills/gsd-path-build/scripts/members.py +68 -25
- package/skills/gsd-path-build/scripts/model_policy.py +5 -1
- package/skills/gsd-path-build/scripts/pipeline_git.py +68 -30
- package/skills/gsd-path-build/scripts/pipeline_state.py +28 -6
- 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 +30 -9
- package/skills/gsd-path-build/scripts/state_promote.py +15 -4
- package/skills/gsd-path-build/scripts/task_context.py +2 -0
- package/skills/gsd-path-build/scripts/workflow_run.py +9 -2
- package/skills/gsd-path-build/templates/task.md +1 -0
- package/skills/gsd-path-build/templates/wave-review.md +18 -9
- 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 +107 -1
- package/skills/gsd-path-decide/scripts/build_recovery.py +62 -30
- package/skills/gsd-path-decide/scripts/check_handoffs.py +92 -30
- package/skills/gsd-path-decide/scripts/check_task_briefs.py +19 -14
- package/skills/gsd-path-decide/scripts/detect_project.py +17 -2
- package/skills/gsd-path-decide/scripts/discussion_records.py +9 -1
- package/skills/gsd-path-decide/scripts/discussion_validate.py +7 -6
- package/skills/gsd-path-decide/scripts/integration.py +98 -64
- package/skills/gsd-path-decide/scripts/isolation.py +148 -11
- package/skills/gsd-path-decide/scripts/members.py +68 -25
- package/skills/gsd-path-decide/scripts/model_policy.py +5 -1
- package/skills/gsd-path-decide/scripts/pipeline_git.py +68 -30
- package/skills/gsd-path-decide/scripts/pipeline_state.py +28 -6
- 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 +30 -9
- package/skills/gsd-path-decide/scripts/state_promote.py +15 -4
- package/skills/gsd-path-define/SKILL.md +6 -2
- 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/scripts/_common.py +107 -1
- package/skills/gsd-path-define/scripts/build_recovery.py +62 -30
- package/skills/gsd-path-define/scripts/check_handoffs.py +92 -30
- package/skills/gsd-path-define/scripts/check_task_briefs.py +19 -14
- package/skills/gsd-path-define/scripts/detect_project.py +17 -2
- package/skills/gsd-path-define/scripts/discussion_records.py +9 -1
- package/skills/gsd-path-define/scripts/discussion_validate.py +7 -6
- package/skills/gsd-path-define/scripts/integration.py +98 -64
- package/skills/gsd-path-define/scripts/isolation.py +148 -11
- package/skills/gsd-path-define/scripts/members.py +68 -25
- package/skills/gsd-path-define/scripts/model_policy.py +5 -1
- package/skills/gsd-path-define/scripts/pipeline_git.py +68 -30
- package/skills/gsd-path-define/scripts/pipeline_state.py +28 -6
- 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 +30 -9
- package/skills/gsd-path-define/scripts/state_promote.py +15 -4
- package/skills/gsd-path-define/templates/state.md +1 -0
- 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 +107 -1
- package/skills/gsd-path-discuss/scripts/discussion_records.py +9 -1
- package/skills/gsd-path-discuss/scripts/model_policy.py +5 -1
- 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 +107 -1
- package/skills/gsd-path-docs-audit/scripts/build_recovery.py +62 -30
- package/skills/gsd-path-docs-audit/scripts/check_handoffs.py +92 -30
- package/skills/gsd-path-docs-audit/scripts/check_task_briefs.py +19 -14
- package/skills/gsd-path-docs-audit/scripts/detect_project.py +17 -2
- package/skills/gsd-path-docs-audit/scripts/discussion_records.py +9 -1
- package/skills/gsd-path-docs-audit/scripts/discussion_validate.py +7 -6
- package/skills/gsd-path-docs-audit/scripts/integration.py +98 -64
- package/skills/gsd-path-docs-audit/scripts/isolation.py +148 -11
- package/skills/gsd-path-docs-audit/scripts/members.py +68 -25
- package/skills/gsd-path-docs-audit/scripts/model_policy.py +5 -1
- package/skills/gsd-path-docs-audit/scripts/pipeline_git.py +68 -30
- package/skills/gsd-path-docs-audit/scripts/pipeline_state.py +28 -6
- 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 +30 -9
- package/skills/gsd-path-docs-audit/scripts/state_promote.py +15 -4
- 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 +107 -1
- package/skills/gsd-path-forensics/scripts/discussion_records.py +9 -1
- package/skills/gsd-path-forensics/scripts/model_policy.py +5 -1
- package/skills/gsd-path-forensics/scripts/pipeline_diagnose.py +17 -2
- package/skills/gsd-path-forensics/scripts/pipeline_undo.py +17 -8
- 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 +107 -1
- package/skills/gsd-path-inspect/scripts/build_recovery.py +62 -30
- package/skills/gsd-path-inspect/scripts/check_handoffs.py +92 -30
- package/skills/gsd-path-inspect/scripts/check_task_briefs.py +19 -14
- package/skills/gsd-path-inspect/scripts/detect_project.py +17 -2
- package/skills/gsd-path-inspect/scripts/discussion_records.py +9 -1
- package/skills/gsd-path-inspect/scripts/discussion_validate.py +7 -6
- package/skills/gsd-path-inspect/scripts/integration.py +98 -64
- package/skills/gsd-path-inspect/scripts/isolation.py +148 -11
- package/skills/gsd-path-inspect/scripts/members.py +68 -25
- package/skills/gsd-path-inspect/scripts/model_policy.py +5 -1
- package/skills/gsd-path-inspect/scripts/pipeline_git.py +68 -30
- package/skills/gsd-path-inspect/scripts/pipeline_state.py +28 -6
- 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 +30 -9
- package/skills/gsd-path-inspect/scripts/state_promote.py +15 -4
- package/skills/gsd-path-inspect/scripts/workflow_run.py +9 -2
- package/skills/gsd-path-inspect/templates/state.md +1 -0
- 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 +107 -1
- package/skills/gsd-path-loop/scripts/discussion_records.py +9 -1
- package/skills/gsd-path-loop/scripts/model_policy.py +5 -1
- 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 +107 -1
- package/skills/gsd-path-migrate/scripts/discussion_records.py +9 -1
- 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-plan/SKILL.md +3 -0
- package/skills/gsd-path-plan/references/build-recovery.md +13 -0
- package/skills/gsd-path-plan/references/operating-rules.md +11 -3
- package/skills/gsd-path-plan/references/planner.md +13 -1
- package/skills/gsd-path-plan/references/runtime-selection.md +9 -0
- package/skills/gsd-path-plan/scripts/_common.py +107 -1
- package/skills/gsd-path-plan/scripts/build_recovery.py +62 -30
- package/skills/gsd-path-plan/scripts/check_handoffs.py +92 -30
- package/skills/gsd-path-plan/scripts/check_task_briefs.py +19 -14
- package/skills/gsd-path-plan/scripts/detect_project.py +17 -2
- package/skills/gsd-path-plan/scripts/discussion_records.py +9 -1
- package/skills/gsd-path-plan/scripts/discussion_validate.py +7 -6
- package/skills/gsd-path-plan/scripts/integration.py +98 -64
- package/skills/gsd-path-plan/scripts/isolation.py +148 -11
- package/skills/gsd-path-plan/scripts/members.py +68 -25
- package/skills/gsd-path-plan/scripts/model_policy.py +5 -1
- package/skills/gsd-path-plan/scripts/pipeline_git.py +68 -30
- package/skills/gsd-path-plan/scripts/pipeline_state.py +28 -6
- 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 +30 -9
- package/skills/gsd-path-plan/scripts/state_promote.py +15 -4
- package/skills/gsd-path-plan/scripts/workflow_run.py +9 -2
- package/skills/gsd-path-plan/templates/task.md +1 -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 +107 -1
- package/skills/gsd-path-research/scripts/build_recovery.py +62 -30
- package/skills/gsd-path-research/scripts/check_handoffs.py +92 -30
- package/skills/gsd-path-research/scripts/check_task_briefs.py +19 -14
- package/skills/gsd-path-research/scripts/detect_project.py +17 -2
- package/skills/gsd-path-research/scripts/discussion_records.py +9 -1
- package/skills/gsd-path-research/scripts/discussion_validate.py +7 -6
- package/skills/gsd-path-research/scripts/integration.py +98 -64
- package/skills/gsd-path-research/scripts/isolation.py +148 -11
- package/skills/gsd-path-research/scripts/members.py +68 -25
- package/skills/gsd-path-research/scripts/model_policy.py +5 -1
- package/skills/gsd-path-research/scripts/pipeline_git.py +68 -30
- package/skills/gsd-path-research/scripts/pipeline_state.py +28 -6
- 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 +30 -9
- package/skills/gsd-path-research/scripts/state_promote.py +15 -4
- package/skills/gsd-path-roadmap/SKILL.md +16 -6
- 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 +107 -1
- package/skills/gsd-path-roadmap/scripts/build_recovery.py +62 -30
- package/skills/gsd-path-roadmap/scripts/check_handoffs.py +92 -30
- package/skills/gsd-path-roadmap/scripts/check_task_briefs.py +19 -14
- package/skills/gsd-path-roadmap/scripts/detect_project.py +17 -2
- package/skills/gsd-path-roadmap/scripts/discussion_records.py +9 -1
- package/skills/gsd-path-roadmap/scripts/discussion_validate.py +7 -6
- package/skills/gsd-path-roadmap/scripts/integration.py +98 -64
- package/skills/gsd-path-roadmap/scripts/isolation.py +148 -11
- package/skills/gsd-path-roadmap/scripts/members.py +68 -25
- package/skills/gsd-path-roadmap/scripts/model_policy.py +5 -1
- package/skills/gsd-path-roadmap/scripts/pipeline_git.py +68 -30
- package/skills/gsd-path-roadmap/scripts/pipeline_state.py +28 -6
- 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 +30 -9
- package/skills/gsd-path-roadmap/scripts/state_promote.py +15 -4
- package/skills/gsd-path-ship/SKILL.md +26 -4
- package/skills/gsd-path-ship/references/operating-rules.md +11 -3
- package/skills/gsd-path-ship/references/reviewer-final.md +5 -5
- package/skills/gsd-path-ship/references/reviewer-wave.md +46 -13
- package/skills/gsd-path-ship/references/runtime-selection.md +9 -0
- package/skills/gsd-path-ship/scripts/_common.py +107 -1
- package/skills/gsd-path-ship/scripts/build_recovery.py +62 -30
- package/skills/gsd-path-ship/scripts/build_state.py +195 -5
- package/skills/gsd-path-ship/scripts/check_handoffs.py +92 -30
- package/skills/gsd-path-ship/scripts/check_task_briefs.py +19 -14
- package/skills/gsd-path-ship/scripts/detect_project.py +17 -2
- package/skills/gsd-path-ship/scripts/discussion_records.py +9 -1
- package/skills/gsd-path-ship/scripts/discussion_validate.py +7 -6
- package/skills/gsd-path-ship/scripts/integration.py +98 -64
- package/skills/gsd-path-ship/scripts/isolation.py +148 -11
- package/skills/gsd-path-ship/scripts/members.py +68 -25
- package/skills/gsd-path-ship/scripts/model_policy.py +5 -1
- package/skills/gsd-path-ship/scripts/pipeline_git.py +68 -30
- package/skills/gsd-path-ship/scripts/pipeline_state.py +28 -6
- 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 +30 -9
- package/skills/gsd-path-ship/scripts/state_promote.py +15 -4
- package/skills/gsd-path-ship/scripts/workflow_run.py +9 -2
- package/skills/gsd-path-ship/templates/final-review.md +5 -5
- package/skills/gsd-path-ship/templates/wave-review.md +18 -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 +107 -1
- package/skills/gsd-path-undo/scripts/discussion_records.py +9 -1
- package/skills/gsd-path-undo/scripts/model_policy.py +5 -1
- package/skills/gsd-path-undo/scripts/pipeline_undo.py +17 -8
- package/skills/path/DEFINE.md +6 -2
- package/skills/path/PLAN.md +3 -0
- package/skills/path/ROADMAP.md +16 -6
- package/skills/path/SHIP.md +26 -4
- package/skills/path/SKILL.md +52 -24
- package/skills/path/references/build-native.md +96 -9
- package/skills/path/references/build-recovery.md +13 -0
- package/skills/path/references/coder.md +7 -3
- package/skills/path/references/operating-rules.md +11 -3
- package/skills/path/references/planner.md +13 -1
- package/skills/path/references/reviewer-final.md +5 -5
- package/skills/path/references/reviewer-wave.md +46 -13
- package/skills/path/references/runtime-selection.md +9 -0
- package/skills/path/scripts/_common.py +107 -1
- package/skills/path/scripts/build_recovery.py +62 -30
- package/skills/path/scripts/build_state.py +195 -5
- package/skills/path/scripts/check_handoffs.py +92 -30
- package/skills/path/scripts/check_task_briefs.py +19 -14
- package/skills/path/scripts/detect_project.py +17 -2
- package/skills/path/scripts/discussion_records.py +9 -1
- package/skills/path/scripts/discussion_validate.py +7 -6
- package/skills/path/scripts/dispatch_driver.py +157 -31
- package/skills/path/scripts/git_guard.py +24 -5
- package/skills/path/scripts/integration.py +98 -64
- package/skills/path/scripts/isolation.py +148 -11
- package/skills/path/scripts/members.py +68 -25
- package/skills/path/scripts/model_policy.py +5 -1
- package/skills/path/scripts/pipeline_git.py +68 -30
- package/skills/path/scripts/pipeline_state.py +28 -6
- 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 +30 -9
- package/skills/path/scripts/state_promote.py +15 -4
- package/skills/path/scripts/task_context.py +2 -0
- package/skills/path/scripts/workflow_run.py +9 -2
- package/skills/path/templates/final-review.md +5 -5
- package/skills/path/templates/state.md +1 -0
- package/skills/path/templates/task.md +1 -0
- package/skills/path/templates/wave-review.md +18 -9
|
@@ -25,7 +25,8 @@ import signal
|
|
|
25
25
|
import subprocess
|
|
26
26
|
import time
|
|
27
27
|
from pathlib import Path
|
|
28
|
-
from typing import BinaryIO, Iterator, List, Optional, Sequence
|
|
28
|
+
from typing import BinaryIO, Iterable, Iterator, List, Optional, Sequence, Tuple
|
|
29
|
+
|
|
29
30
|
|
|
30
31
|
# Chosen by what imports, not os.name, so tests can emulate Windows on POSIX.
|
|
31
32
|
try:
|
|
@@ -161,6 +162,33 @@ def project_ignore_error(repo: Path) -> Optional[str]:
|
|
|
161
162
|
+ ". Anchor the product rule (for example `/build/`) in a task that owns it.")
|
|
162
163
|
|
|
163
164
|
|
|
165
|
+
_DEFAULT_BRANCH_RE = re.compile(
|
|
166
|
+
r"[^\s#\"'\\~^:?*\[\x00-\x1f\x7f-][^\s#\"'\\~^:?*\[\x00-\x1f\x7f]*"
|
|
167
|
+
)
|
|
168
|
+
_RESERVED_BRANCH_PREFIXES = ("gsd-path/", "gsd-path-integrate/", "gsd-path-task/")
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def valid_default_branch(name: str) -> bool:
|
|
172
|
+
"""A git branch name that STATE frontmatter can hold and Path does not reserve."""
|
|
173
|
+
return (
|
|
174
|
+
bool(_DEFAULT_BRANCH_RE.fullmatch(name))
|
|
175
|
+
and name != "null"
|
|
176
|
+
and not name.startswith(_RESERVED_BRANCH_PREFIXES)
|
|
177
|
+
and not name.endswith(("/", ".", ".lock"))
|
|
178
|
+
and not any(part in name for part in ("..", "//", "/.", "@{"))
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def origin_default_branch(repo: Path) -> str:
|
|
183
|
+
"""The origin default branch that initialization records; an unset origin/HEAD means main."""
|
|
184
|
+
result = run_git(repo, "symbolic-ref", "--quiet", "refs/remotes/origin/HEAD")
|
|
185
|
+
prefix = "refs/remotes/origin/"
|
|
186
|
+
remote_ref = result.stdout.strip()
|
|
187
|
+
if result.returncode != 0 or not remote_ref.startswith(prefix):
|
|
188
|
+
return "main"
|
|
189
|
+
return remote_ref.removeprefix(prefix)
|
|
190
|
+
|
|
191
|
+
|
|
164
192
|
def section_body(text: str, heading: str) -> Optional[str]:
|
|
165
193
|
"""The text under `## <heading>` up to the next `## `, or None when absent."""
|
|
166
194
|
match = re.search(
|
|
@@ -169,6 +197,79 @@ def section_body(text: str, heading: str) -> Optional[str]:
|
|
|
169
197
|
return match.group("body") if match else None
|
|
170
198
|
|
|
171
199
|
|
|
200
|
+
def overlapping_paths(left: Sequence[str], right: Sequence[str]) -> List[str]:
|
|
201
|
+
"""Return the sorted paths that two task `files` lists both cover.
|
|
202
|
+
|
|
203
|
+
An entry covers itself and the paths below it. For a declared directory
|
|
204
|
+
and a path below it, the result names the lower path. The callers have no
|
|
205
|
+
task base, so this does not ask if an entry is a file: the task-brief
|
|
206
|
+
lint rejects a path below a file at the base.
|
|
207
|
+
"""
|
|
208
|
+
return sorted(
|
|
209
|
+
{
|
|
210
|
+
max(one, other, key=len)
|
|
211
|
+
for one in left
|
|
212
|
+
for other in right
|
|
213
|
+
if one == other or one.startswith(other + "/") or other.startswith(one + "/")
|
|
214
|
+
}
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
# The `- ` of an indented sub-bullet line. A line that would then read as a
|
|
219
|
+
# numbered item (`- 2. text`) keeps its marker, so both readings of a source
|
|
220
|
+
# have the same criteria.
|
|
221
|
+
SUB_BULLET_MARKER = re.compile(r"(?m)^([ \t]+)-[ \t]+(?![ \t]*\d+\.\s)")
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def without_sub_bullet_markers(source: str) -> str:
|
|
225
|
+
"""`source` with the `- ` marker of each indented sub-bullet line removed."""
|
|
226
|
+
return SUB_BULLET_MARKER.sub(r"\1", source)
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
class Criterion(str):
|
|
230
|
+
"""A criterion as its parser joins it, with the `- ` markers of its sub-bullet lines.
|
|
231
|
+
|
|
232
|
+
`bare` is the same criterion parsed from `without_sub_bullet_markers(source)`.
|
|
233
|
+
The joined text cannot tell a marker from a minus sign; the source lines can.
|
|
234
|
+
"""
|
|
235
|
+
|
|
236
|
+
bare: str
|
|
237
|
+
|
|
238
|
+
def __new__(cls, text: str, bare: str) -> "Criterion":
|
|
239
|
+
criterion = super().__new__(cls, text)
|
|
240
|
+
criterion.bare = bare
|
|
241
|
+
return criterion
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def quotes_criterion(quote: str, criterion: str) -> bool:
|
|
245
|
+
"""Whether a review quote is `criterion`, for every reader.
|
|
246
|
+
|
|
247
|
+
Only whitespace is collapsed. The quote must equal the criterion with all
|
|
248
|
+
the markers of its sub-bullet lines or with none of them; every other
|
|
249
|
+
character, a hyphen inside a line too, is compared.
|
|
250
|
+
"""
|
|
251
|
+
forms = (criterion, getattr(criterion, "bare", criterion))
|
|
252
|
+
return " ".join(quote.split()) in {" ".join(form.split()) for form in forms}
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def quoted_criterion(item: str, criteria: Sequence[str]) -> Optional[Tuple[int, int]]:
|
|
256
|
+
"""Index of the criterion quoted at the start of `item`, and where the quote ends.
|
|
257
|
+
|
|
258
|
+
A criterion can contain ` — ` itself, so each separator and the end of
|
|
259
|
+
`item` is a candidate, the longest quote first. A criterion as written
|
|
260
|
+
wins over any criterion read without its sub-bullet markers.
|
|
261
|
+
"""
|
|
262
|
+
ends = [match.start() for match in re.finditer("(?= —(?: |$))|$", item)][::-1]
|
|
263
|
+
for bare in (False, True):
|
|
264
|
+
for end in ends:
|
|
265
|
+
quote = " ".join(item[:end].split())
|
|
266
|
+
for index, criterion in enumerate(criteria):
|
|
267
|
+
form = getattr(criterion, "bare", criterion) if bare else criterion
|
|
268
|
+
if quote == " ".join(form.split()):
|
|
269
|
+
return index, end
|
|
270
|
+
return None
|
|
271
|
+
|
|
272
|
+
|
|
172
273
|
def task_verify_command(task_text: str) -> str:
|
|
173
274
|
"""The Verify shell text, excluding the closing fence's separator newline."""
|
|
174
275
|
body = section_body(task_text, "Verify")
|
|
@@ -176,6 +277,11 @@ def task_verify_command(task_text: str) -> str:
|
|
|
176
277
|
return block.group("block").removesuffix("\n") if block else ""
|
|
177
278
|
|
|
178
279
|
|
|
280
|
+
def fix_verify(commands: Iterable[str]) -> str:
|
|
281
|
+
"""The Verify of a fix task: each distinct source Verify in its own subshell, under `set -e`."""
|
|
282
|
+
return "set -e\n" + "\n".join(f"(\n{command}\n)" for command in dict.fromkeys(commands))
|
|
283
|
+
|
|
284
|
+
|
|
179
285
|
def latest_verify_entry(entries: list, command: str, commit: str,
|
|
180
286
|
repo: Optional[str] = None) -> Optional[dict]:
|
|
181
287
|
"""Rows are keyed by (command, repo, commit); a row without `repo` is a coordinator row."""
|
|
@@ -22,6 +22,10 @@ MARKER = "build recovery: "
|
|
|
22
22
|
RECOVERY_KEYS = frozenset({"base", "branch", "kind", "source"})
|
|
23
23
|
|
|
24
24
|
|
|
25
|
+
# Only the value of a cap line is exempt; a comment or a line separator after it stays in the compared text.
|
|
26
|
+
CAP_VALUE = re.compile(r"(?m)^(-[ \t]*(?:max_review_cycles|wave_budget):)[ \t\w.,$/:]*")
|
|
27
|
+
|
|
28
|
+
|
|
25
29
|
def runtime():
|
|
26
30
|
if __package__:
|
|
27
31
|
from scripts import pipeline_state
|
|
@@ -58,8 +62,17 @@ def _parse_recovery_record(value: object) -> dict:
|
|
|
58
62
|
return {**value, "source": source}
|
|
59
63
|
|
|
60
64
|
|
|
61
|
-
def _recovery_base_proves_milestone(before, current, source: str) -> bool:
|
|
62
|
-
if before.branch != current.branch
|
|
65
|
+
def _recovery_base_proves_milestone(before, current, source: str, active: bool = True) -> bool:
|
|
66
|
+
if before.branch != current.branch:
|
|
67
|
+
return False
|
|
68
|
+
if before.milestone != current.milestone and (
|
|
69
|
+
# Issue #374: a legacy base records milestone null when define never
|
|
70
|
+
# named it. The owner names it at ship, after the build resumed, so
|
|
71
|
+
# only a closed build recovery accepts an unset base milestone; the
|
|
72
|
+
# bound branch already encodes M00N. A milestone named by the base
|
|
73
|
+
# must still equal the current one.
|
|
74
|
+
active or source != "build" or before.milestone is not None
|
|
75
|
+
):
|
|
63
76
|
return False
|
|
64
77
|
if source == "plan":
|
|
65
78
|
return (before.phase, before.status) in {("plan", "active"), ("plan", "blocked")}
|
|
@@ -99,7 +112,9 @@ def context(repo: Path, text: str | None = None) -> dict | None:
|
|
|
99
112
|
if base_text is None:
|
|
100
113
|
raise state.PipelineStateError("build recovery base has no STATE.md")
|
|
101
114
|
before = state._state_from_text(base_text)
|
|
102
|
-
if not _recovery_base_proves_milestone(
|
|
115
|
+
if not _recovery_base_proves_milestone(
|
|
116
|
+
before, current, recovery.get("source", "build"), recovery["active"]
|
|
117
|
+
):
|
|
103
118
|
raise state.PipelineStateError("build recovery base does not prove this blocked milestone")
|
|
104
119
|
return recovery
|
|
105
120
|
|
|
@@ -289,61 +304,78 @@ def validate_plan(repo: Path) -> None:
|
|
|
289
304
|
intent = checkpoint_runtime()._git_text_at(repo, recovery["base"], ".project/intent/INTENT.md")
|
|
290
305
|
if state._read_real_file(repo / ".project/intent/INTENT.md", "INTENT.md") != intent:
|
|
291
306
|
raise state.PipelineStateError("plan repair cannot change approved intent")
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
match = re.match(r"wave-([1-9]\d*)[.]", path.name)
|
|
295
|
-
if match and int(match[1]) not in unchanged:
|
|
296
|
-
raise state.PipelineStateError("changed wave must be reviewed after build resumes")
|
|
307
|
+
if stale_review_reasons(repo, recovery, _review_waves(repo / ".project/review")):
|
|
308
|
+
raise state.PipelineStateError("changed wave must be reviewed after build resumes")
|
|
297
309
|
|
|
298
310
|
|
|
299
|
-
def
|
|
300
|
-
|
|
311
|
+
def _review_waves(directory: Path) -> set[int]:
|
|
312
|
+
return {int(match[1]) for path in directory.iterdir()
|
|
313
|
+
if (match := re.match(r"wave-([1-9]\d*)[.]", path.name))}
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
def stale_review_reasons(repo: Path, recovery: dict, waves: set[int]) -> dict[int, str]:
|
|
317
|
+
"""Say why a wave's preserved review is stale; a wave not returned has matching intent, plan, and task bytes."""
|
|
301
318
|
state = runtime()
|
|
302
319
|
project = repo / ".project"
|
|
303
320
|
old_intent = checkpoint_runtime()._git_text_at(repo, recovery["base"], ".project/intent/INTENT.md")
|
|
304
321
|
if state._read_real_file(project / "intent/INTENT.md", "INTENT.md") != old_intent:
|
|
305
|
-
return
|
|
322
|
+
return dict.fromkeys(waves, "INTENT.md changed")
|
|
306
323
|
old_plan = checkpoint_runtime()._git_text_at(repo, recovery["base"], ".project/plan/PLAN.md") or ""
|
|
307
324
|
new_plan = state._read_real_file(project / "plan/PLAN.md", "PLAN.md")
|
|
325
|
+
panel_lines = r"(?m)^-[ \t]*review_panel:.*$"
|
|
326
|
+
if re.findall(panel_lines, old_plan) != re.findall(panel_lines, new_plan):
|
|
327
|
+
return dict.fromkeys(waves, "PLAN.md `review_panel` setting changed")
|
|
308
328
|
|
|
309
329
|
def sections(text):
|
|
310
330
|
return {match[1]: match[2] for match in re.finditer(
|
|
311
331
|
r"(?ms)^## ([^\n]+)\n(.*?)(?=^## |\Z)", text,
|
|
312
332
|
)}
|
|
313
333
|
|
|
334
|
+
def governing(plan):
|
|
335
|
+
# Non-wave settings govern every wave, including coverage and surfaces. Caps only bound
|
|
336
|
+
# later work and Dependency notes are prose, so neither changes what a passed review judged.
|
|
337
|
+
return {title: CAP_VALUE.sub(r"\1", body) if title == "Config" else body
|
|
338
|
+
for title, body in plan.items()
|
|
339
|
+
if not title.startswith("Wave ") and title != "Dependency notes"}
|
|
340
|
+
|
|
314
341
|
old_sections, new_sections = sections(old_plan), sections(new_plan)
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
342
|
+
old_governing, new_governing = governing(old_sections), governing(new_sections)
|
|
343
|
+
for title in sorted(old_governing.keys() | new_governing.keys()):
|
|
344
|
+
if old_governing.get(title) != new_governing.get(title):
|
|
345
|
+
return dict.fromkeys(waves, f"PLAN.md section `{title}` changed")
|
|
319
346
|
old_tasks = checkpoint_runtime()._task_contracts_at(repo, recovery["base"], ".project/tasks")
|
|
320
347
|
current = settled_tasks(repo)
|
|
321
|
-
|
|
322
|
-
for
|
|
323
|
-
|
|
324
|
-
if not
|
|
348
|
+
reasons = {}
|
|
349
|
+
for wave in waves:
|
|
350
|
+
section = {title: body for title, body in new_sections.items() if re.match(rf"Wave {wave}\b", title)}
|
|
351
|
+
if not section or any(old_sections.get(title) != body for title, body in section.items()):
|
|
352
|
+
reasons[wave] = f"PLAN.md section `Wave {wave}` changed"
|
|
325
353
|
continue
|
|
326
|
-
wave = match[1]
|
|
327
354
|
before = {Path(path).name: text for path, text in old_tasks.values()
|
|
328
|
-
if task_fields(text, path).get("wave") == wave}
|
|
329
|
-
after = {name: text for name, (text, fields) in current.items() if fields.get("wave") == wave}
|
|
330
|
-
if before
|
|
331
|
-
|
|
332
|
-
return
|
|
355
|
+
if task_fields(text, path).get("wave") == str(wave)}
|
|
356
|
+
after = {name: text for name, (text, fields) in current.items() if fields.get("wave") == str(wave)}
|
|
357
|
+
if not before or before != after:
|
|
358
|
+
reasons[wave] = f"wave {wave} task contracts changed"
|
|
359
|
+
return reasons
|
|
333
360
|
|
|
334
361
|
|
|
335
|
-
def restore_unchanged_reviews(repo: Path) -> None:
|
|
362
|
+
def restore_unchanged_reviews(repo: Path) -> dict | None:
|
|
363
|
+
"""Restore reusable wave reviews and report every preserved wave, so a dropped review is visible at approval."""
|
|
336
364
|
recovery = context(repo)
|
|
337
365
|
if not recovery or not recovery["active"]:
|
|
338
|
-
return
|
|
339
|
-
|
|
340
|
-
|
|
366
|
+
return None
|
|
367
|
+
backup = review_backup(repo, recovery)
|
|
368
|
+
waves = _review_waves(backup)
|
|
369
|
+
reasons = stale_review_reasons(repo, recovery, waves)
|
|
370
|
+
for path in backup.iterdir():
|
|
341
371
|
match = re.match(r"wave-([1-9]\d*)[.]", path.name)
|
|
342
|
-
if match and int(match[1]) in
|
|
372
|
+
if match and int(match[1]) not in reasons:
|
|
343
373
|
destination = repo / ".project/review" / path.name
|
|
344
374
|
content = path.read_text(encoding="utf-8")
|
|
345
375
|
if not destination.exists() or runtime()._read_real_file(destination, destination.name) != content:
|
|
346
376
|
runtime()._atomic_write(destination, content)
|
|
377
|
+
return {"restored": sorted(waves - reasons.keys()),
|
|
378
|
+
"review_again": [{"wave": wave, "reason": reasons[wave]} for wave in sorted(reasons)]}
|
|
347
379
|
|
|
348
380
|
|
|
349
381
|
def inventory_checkpoint(repo: Path, head: str) -> bool:
|
|
@@ -161,7 +161,8 @@ def _reject_placeholder(value: str, label: str) -> None:
|
|
|
161
161
|
if placeholder is not None:
|
|
162
162
|
raise HandoffError(
|
|
163
163
|
f"{label} still contains the placeholder {placeholder.group(0)}; "
|
|
164
|
-
"replace it with the real value"
|
|
164
|
+
"replace it with the real value. A value cannot be only a placeholder; "
|
|
165
|
+
"literal text such as an HTML tag must be inside a sentence, in backticks"
|
|
165
166
|
)
|
|
166
167
|
|
|
167
168
|
|
|
@@ -667,22 +668,30 @@ def _strip_comments(text: str) -> str:
|
|
|
667
668
|
return COMMENT_PATTERN.sub("", text)
|
|
668
669
|
|
|
669
670
|
|
|
670
|
-
def
|
|
671
|
-
|
|
672
|
-
|
|
671
|
+
def _numbered_entries(section: str, *, continuations: bool = False) -> List[Tuple[int, str]]:
|
|
672
|
+
"""Each numbered item in document order; a repeated number stays a separate entry."""
|
|
673
|
+
entries: List[Tuple[int, str]] = []
|
|
674
|
+
open_item = False
|
|
673
675
|
for line in _strip_comments(section).splitlines():
|
|
674
676
|
match = NUMBERED_ITEM_PATTERN.fullmatch(line.strip())
|
|
675
677
|
if not match:
|
|
676
|
-
if continuations and
|
|
677
|
-
|
|
678
|
+
if continuations and open_item and line[:1].isspace() and line.strip():
|
|
679
|
+
number, text = entries[-1]
|
|
680
|
+
entries[-1] = (number, text + " " + line.strip())
|
|
678
681
|
elif line.strip():
|
|
679
|
-
|
|
682
|
+
open_item = False
|
|
680
683
|
continue
|
|
681
|
-
|
|
684
|
+
entries.append((int(match.group(1)), match.group(2).strip()))
|
|
685
|
+
open_item = True
|
|
686
|
+
return entries
|
|
687
|
+
|
|
688
|
+
|
|
689
|
+
def _numbered_items(section: str, *, continuations: bool = False) -> Dict[int, str]:
|
|
690
|
+
items: Dict[int, str] = {}
|
|
691
|
+
for number, text in _numbered_entries(section, continuations=continuations):
|
|
682
692
|
if number in items:
|
|
683
693
|
raise HandoffError(f"repeated numbered item {number}")
|
|
684
|
-
items[number] =
|
|
685
|
-
current = number
|
|
694
|
+
items[number] = text
|
|
686
695
|
return items
|
|
687
696
|
|
|
688
697
|
|
|
@@ -693,7 +702,11 @@ def _success_criteria(intent: str) -> Dict[str, str]:
|
|
|
693
702
|
expected = list(range(1, max(items) + 1))
|
|
694
703
|
if sorted(items) != expected:
|
|
695
704
|
raise HandoffError("INTENT.md success criteria must be contiguous from 1")
|
|
696
|
-
|
|
705
|
+
# Issue #392: a review can quote a criterion without the markers of its sub-bullet lines.
|
|
706
|
+
bare = _numbered_items(
|
|
707
|
+
_section(_common.without_sub_bullet_markers(intent), "Success criteria"), continuations=True
|
|
708
|
+
)
|
|
709
|
+
return {f"SC{number}": _common.Criterion(text, bare[number]) for number, text in items.items()}
|
|
697
710
|
|
|
698
711
|
|
|
699
712
|
def _split_top_level_commas(value: str) -> List[str]:
|
|
@@ -1297,18 +1310,22 @@ def _normalize_ws(value: str) -> str:
|
|
|
1297
1310
|
|
|
1298
1311
|
|
|
1299
1312
|
def task_review_observation(item: str, task_text: str) -> str:
|
|
1300
|
-
"""Separate
|
|
1313
|
+
"""Separate a quoted acceptance criterion from its observed evidence."""
|
|
1301
1314
|
item = _normalize_ws(item)
|
|
1302
1315
|
section = _common.section_body(task_text, "Acceptance criteria")
|
|
1303
1316
|
if section is not None:
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1317
|
+
# Issue #374: frozen landed bytes may repeat a criterion number. Only the
|
|
1318
|
+
# criterion text matters here, so each repeated entry is still split
|
|
1319
|
+
# from its evidence and the evidence gets the same checks.
|
|
1320
|
+
criteria = [criterion for _number, criterion in _numbered_entries(section, continuations=True)]
|
|
1321
|
+
quoted = _common.quoted_criterion(item, criteria)
|
|
1322
|
+
if quoted is not None:
|
|
1323
|
+
return item[quoted[1] + len(" — ") :]
|
|
1324
|
+
# Issue #392: a quote can leave out the `- ` markers of the criterion's
|
|
1325
|
+
# sub-bullet lines, so read the criteria again without them.
|
|
1326
|
+
bare = _common.without_sub_bullet_markers(task_text)
|
|
1327
|
+
return item if bare == task_text else task_review_observation(item, bare)
|
|
1328
|
+
|
|
1312
1329
|
|
|
1313
1330
|
|
|
1314
1331
|
def _verify_command(task_text: str) -> str:
|
|
@@ -1528,11 +1545,30 @@ def _validate_task_graph(
|
|
|
1528
1545
|
if _task_wave(tasks[dependency], dependency) > wave:
|
|
1529
1546
|
raise HandoffError(f"{task_id} depends on later-wave {dependency}")
|
|
1530
1547
|
|
|
1548
|
+
# `supersedes` retires the regression Verify of an earlier task: the same overlap rule selects both.
|
|
1549
|
+
for task_id, text in tasks.items():
|
|
1550
|
+
superseded = _strict_frontmatter(text, task_id).get("supersedes", [])
|
|
1551
|
+
if (
|
|
1552
|
+
not isinstance(superseded, list)
|
|
1553
|
+
or any(not re.fullmatch(r"T\d{3}", item) for item in superseded)
|
|
1554
|
+
or len(superseded) != len(set(superseded))
|
|
1555
|
+
):
|
|
1556
|
+
raise HandoffError(f"{task_id} supersedes must be a list of different task ids, for example [T001]")
|
|
1557
|
+
for earlier in superseded:
|
|
1558
|
+
if earlier not in tasks:
|
|
1559
|
+
raise HandoffError(f"{task_id} supersedes unknown task {earlier}")
|
|
1560
|
+
if task_waves[earlier] >= task_waves[task_id]:
|
|
1561
|
+
raise HandoffError(f"{task_id} supersedes {earlier}, which is not in an earlier wave")
|
|
1562
|
+
if task_repos[earlier] != task_repos[task_id]:
|
|
1563
|
+
raise HandoffError(f"{task_id} supersedes {earlier}, which is in another repo")
|
|
1564
|
+
if not _common.overlapping_paths(task_files[earlier], task_files[task_id]):
|
|
1565
|
+
raise HandoffError(f"{task_id} supersedes {earlier}, which shares no declared file with it")
|
|
1566
|
+
|
|
1531
1567
|
for index, left in enumerate(task_ids):
|
|
1532
1568
|
for right in task_ids[index + 1 :]:
|
|
1533
1569
|
if task_waves[left] != task_waves[right] or task_repos[left] != task_repos[right]:
|
|
1534
1570
|
continue
|
|
1535
|
-
overlap =
|
|
1571
|
+
overlap = _common.overlapping_paths(task_files[left], task_files[right])
|
|
1536
1572
|
if overlap:
|
|
1537
1573
|
if (
|
|
1538
1574
|
_task_scalar(tasks[left], left, "status") == "done"
|
|
@@ -1746,7 +1782,24 @@ def validate_roadmap(
|
|
|
1746
1782
|
) -> Dict[str, object]:
|
|
1747
1783
|
"""Validate roadmap structure and dependency ordering."""
|
|
1748
1784
|
|
|
1749
|
-
|
|
1785
|
+
# A milestone-boundary re-slice keeps its entering inspect or define state.
|
|
1786
|
+
_require_state_one_of(
|
|
1787
|
+
root,
|
|
1788
|
+
{("roadmap", "active"), ("inspect", "active"), ("define", "active")},
|
|
1789
|
+
project_dir,
|
|
1790
|
+
)
|
|
1791
|
+
if _require_pipeline(root, project_dir)["phase"] == "define":
|
|
1792
|
+
if __package__:
|
|
1793
|
+
from . import build_recovery
|
|
1794
|
+
else:
|
|
1795
|
+
import build_recovery
|
|
1796
|
+
try:
|
|
1797
|
+
# Define entered again after intent approval is not a boundary.
|
|
1798
|
+
reopened = (build_recovery.context(root.resolve()) or {}).get("active")
|
|
1799
|
+
except PipelineStateError as error:
|
|
1800
|
+
raise HandoffError(str(error)) from error
|
|
1801
|
+
if reopened:
|
|
1802
|
+
raise HandoffError("roadmap re-slice is not legal after intent approval")
|
|
1750
1803
|
relative = f"{project_dir}/ROADMAP.md"
|
|
1751
1804
|
text = _read(root, relative)
|
|
1752
1805
|
if re.search(r"(?m)^## Wave |^\|\s*Task\s*\||^Files:\s*", text):
|
|
@@ -2045,25 +2098,28 @@ def validate_wave_evidence(
|
|
|
2045
2098
|
if enforce_plan_tasks:
|
|
2046
2099
|
if reviewed_tasks != expected_tasks:
|
|
2047
2100
|
raise HandoffError(
|
|
2048
|
-
f"{name} must review exactly {', '.join(expected_tasks)} in
|
|
2101
|
+
f"{name} must review exactly {', '.join(expected_tasks)} in task id order"
|
|
2049
2102
|
)
|
|
2050
2103
|
else:
|
|
2051
2104
|
unknown = [task_id for task_id in reviewed_tasks if task_id not in expected_tasks]
|
|
2052
2105
|
if unknown:
|
|
2053
2106
|
raise HandoffError(
|
|
2054
|
-
f"{name} must review exactly {', '.join(expected_tasks)} in
|
|
2107
|
+
f"{name} must review exactly {', '.join(expected_tasks)} in task id order"
|
|
2055
2108
|
)
|
|
2056
2109
|
plan_order = [task_id for task_id in expected_tasks if task_id in reviewed_tasks]
|
|
2057
2110
|
if reviewed_tasks != plan_order:
|
|
2058
2111
|
raise HandoffError(
|
|
2059
|
-
f"{name} must review exactly {', '.join(expected_tasks)} in
|
|
2112
|
+
f"{name} must review exactly {', '.join(expected_tasks)} in task id order"
|
|
2060
2113
|
)
|
|
2061
2114
|
task_verdicts = []
|
|
2062
2115
|
for heading_index, heading in task_headings:
|
|
2063
2116
|
task_id = heading.group("task")
|
|
2064
2117
|
expected_title = _normalize_ws(_task_scalar(tasks[task_id], task_id, "title"))
|
|
2065
2118
|
if _normalize_ws(heading.group("title")) != expected_title:
|
|
2066
|
-
raise HandoffError(
|
|
2119
|
+
raise HandoffError(
|
|
2120
|
+
f"{name} title for {task_id} differs from task file; "
|
|
2121
|
+
f"expected the exact `title:` value: {expected_title}"
|
|
2122
|
+
)
|
|
2067
2123
|
end = next(
|
|
2068
2124
|
(
|
|
2069
2125
|
index
|
|
@@ -2080,7 +2136,10 @@ def validate_wave_evidence(
|
|
|
2080
2136
|
if line.strip().startswith(f"- {marker} ")
|
|
2081
2137
|
]
|
|
2082
2138
|
if not evidence:
|
|
2083
|
-
raise HandoffError(
|
|
2139
|
+
raise HandoffError(
|
|
2140
|
+
f"{name} task {task_id} lacks {verdict} evidence; "
|
|
2141
|
+
f"add a line that starts with `- {marker} ` under its heading"
|
|
2142
|
+
)
|
|
2084
2143
|
for item in evidence:
|
|
2085
2144
|
item = task_review_observation(item, tasks[task_id])
|
|
2086
2145
|
_non_placeholder(item, f"{name} task {task_id} {verdict} evidence")
|
|
@@ -2132,7 +2191,7 @@ def validate_wave_evidence(
|
|
|
2132
2191
|
sc_id in criteria
|
|
2133
2192
|
and (
|
|
2134
2193
|
not separator
|
|
2135
|
-
or
|
|
2194
|
+
or not _common.quotes_criterion(heading_text, criteria[sc_id])
|
|
2136
2195
|
)
|
|
2137
2196
|
):
|
|
2138
2197
|
raise HandoffError(f"{name} {sc_id} heading text differs from INTENT.md")
|
|
@@ -2150,7 +2209,10 @@ def validate_wave_evidence(
|
|
|
2150
2209
|
if line.strip().startswith(f"- {marker} ")
|
|
2151
2210
|
]
|
|
2152
2211
|
if not evidence:
|
|
2153
|
-
raise HandoffError(
|
|
2212
|
+
raise HandoffError(
|
|
2213
|
+
f"{name} {sc_id} lacks {verdict} evidence; "
|
|
2214
|
+
f"add a line that starts with `- {marker} ` under its heading"
|
|
2215
|
+
)
|
|
2154
2216
|
for item in evidence:
|
|
2155
2217
|
_non_placeholder(item, f"{name} {sc_id} {verdict} evidence")
|
|
2156
2218
|
verdicts[sc_id] = verdict
|
|
@@ -2230,7 +2292,7 @@ def validate_final(
|
|
|
2230
2292
|
verdicts: Dict[str, str] = {}
|
|
2231
2293
|
for index, heading in enumerate(blocks):
|
|
2232
2294
|
sc_id = heading.group("id")
|
|
2233
|
-
if
|
|
2295
|
+
if not _common.quotes_criterion(heading.group("title"), criteria[sc_id]):
|
|
2234
2296
|
raise HandoffError(f"FINAL.md {sc_id} heading text differs from INTENT.md")
|
|
2235
2297
|
end = blocks[index + 1].start() if index + 1 < len(blocks) else len(text)
|
|
2236
2298
|
block = text[heading.end() : end]
|
|
@@ -100,13 +100,15 @@ def _supplied_contract(repo: Path, task: Path, token: str) -> bool:
|
|
|
100
100
|
return path.is_file() and path.resolve() == repo.resolve() / token
|
|
101
101
|
|
|
102
102
|
|
|
103
|
+
def _pipeline_workspace_path(repo: Path, task: Path, token: str) -> bool:
|
|
104
|
+
track = _project_track(task, repo)
|
|
105
|
+
return track is not None and token.startswith(
|
|
106
|
+
tuple(f"{track}/{name}/" for name in PIPELINE_WORKSPACE_DIRS))
|
|
107
|
+
|
|
108
|
+
|
|
103
109
|
def _pipeline_workspace_file(repo: Path, task: Path, token: str) -> bool:
|
|
104
110
|
"""Pipeline-owned paths may exist only in the working tree before checkpoint."""
|
|
105
|
-
|
|
106
|
-
if track is None:
|
|
107
|
-
return False
|
|
108
|
-
prefixes = tuple(f"{track}/{name}/" for name in PIPELINE_WORKSPACE_DIRS)
|
|
109
|
-
if not any(token.startswith(prefix) for prefix in prefixes):
|
|
111
|
+
if not _pipeline_workspace_path(repo, task, token):
|
|
110
112
|
return False
|
|
111
113
|
path = repo / token
|
|
112
114
|
return path.is_file() and path.resolve() == repo.resolve() / token
|
|
@@ -277,7 +279,7 @@ def _verify_tokens(
|
|
|
277
279
|
|
|
278
280
|
|
|
279
281
|
def _member_bases(root: Path):
|
|
280
|
-
"""Resolve a member name to (checkout,
|
|
282
|
+
"""Resolve a member name to (checkout, base SHA), reading MEMBERS.md once."""
|
|
281
283
|
cache: Dict[str, Optional[Tuple[Path, str]]] = {}
|
|
282
284
|
|
|
283
285
|
def locate(name: str) -> Optional[Tuple[Path, str]]:
|
|
@@ -290,21 +292,22 @@ def _member_bases(root: Path):
|
|
|
290
292
|
except ImportError: # pragma: no cover - package import used by tests
|
|
291
293
|
from scripts import members
|
|
292
294
|
try:
|
|
293
|
-
listed = {member["name"]:
|
|
294
|
-
|
|
295
|
-
if checkout is None:
|
|
295
|
+
listed = {member["name"]: member for member in members.read_members(root)}
|
|
296
|
+
if name not in listed:
|
|
296
297
|
cache[name] = None
|
|
297
298
|
else:
|
|
299
|
+
checkout = Path(listed[name]["checkout"])
|
|
298
300
|
# A build lock selects the bound tip, including during Plan recovery;
|
|
299
|
-
# without the lock, use
|
|
301
|
+
# without the lock, use the member's recorded default branch on origin
|
|
302
|
+
# before the first build start.
|
|
300
303
|
lock = root / members.LOCK_PATH
|
|
301
304
|
locked = [entry for entry in (json.loads(lock.read_text(encoding="utf-8"))["members"]
|
|
302
305
|
if lock.is_file() else []) if entry["name"] == name]
|
|
303
306
|
if locked:
|
|
304
307
|
cache[name] = (checkout, _resolve_base(checkout, f"refs/heads/{locked[0]['branch']}"))
|
|
305
308
|
else:
|
|
306
|
-
members.
|
|
307
|
-
|
|
309
|
+
cache[name] = (checkout, members.require_origin_branch(
|
|
310
|
+
checkout, members.default_branch(listed[name])))
|
|
308
311
|
except (members.MembersError, BriefError) as error:
|
|
309
312
|
raise BriefError(f"member {name} is unavailable: {error}") from error
|
|
310
313
|
return cache[name]
|
|
@@ -328,8 +331,8 @@ def _lint_task(
|
|
|
328
331
|
checked = 0
|
|
329
332
|
if fields is None:
|
|
330
333
|
return task_id, [error or "invalid frontmatter"], None, checked
|
|
331
|
-
# A member task's paths resolve in the member at its origin
|
|
332
|
-
# member bound branch starts; `repo:` absent means the coordinator.
|
|
334
|
+
# A member task's paths resolve in the member at its default branch on origin,
|
|
335
|
+
# where the member bound branch starts; `repo:` absent means the coordinator.
|
|
333
336
|
git_root = repo
|
|
334
337
|
member = fields.get("repo")
|
|
335
338
|
if member is not None:
|
|
@@ -397,6 +400,8 @@ def _lint_task(
|
|
|
397
400
|
and token not in supplied
|
|
398
401
|
and not _supplied_contract(repo, path, token)
|
|
399
402
|
and not _pipeline_workspace_file(repo, path, token)
|
|
403
|
+
# A landed brief is history: the pipeline moves its own files (build recovery, archive) later.
|
|
404
|
+
and not (task_id in (landed_bases or {}) and _pipeline_workspace_path(repo, path, token))
|
|
400
405
|
and not _base_exists(git_root, base, token)
|
|
401
406
|
):
|
|
402
407
|
problems.append(f"## {name} names a path missing at the layer base: {token}")
|
|
@@ -1291,10 +1291,17 @@ def project_slug(root: Path) -> str:
|
|
|
1291
1291
|
return slug or "project"
|
|
1292
1292
|
|
|
1293
1293
|
|
|
1294
|
-
def filled_state_template(
|
|
1294
|
+
def filled_state_template(
|
|
1295
|
+
template: str, slug: str, phase: str, default_branch: str = "main"
|
|
1296
|
+
) -> str:
|
|
1295
1297
|
if "<slug>" not in template:
|
|
1296
1298
|
raise DetectError("state template missing slug placeholder")
|
|
1297
1299
|
text = template.replace("<slug>", slug)
|
|
1300
|
+
text, count = re.subn(
|
|
1301
|
+
r"(?m)^default_branch: main\b", lambda _: f"default_branch: {default_branch}", text, count=1
|
|
1302
|
+
)
|
|
1303
|
+
if count != 1:
|
|
1304
|
+
raise DetectError("state template missing default_branch: main line")
|
|
1298
1305
|
text, count = re.subn(r"(?m)^phase: define\b", f"phase: {phase}", text, count=1)
|
|
1299
1306
|
if count != 1:
|
|
1300
1307
|
raise DetectError("state template missing phase: define line")
|
|
@@ -2030,6 +2037,14 @@ def initialize(
|
|
|
2030
2037
|
)
|
|
2031
2038
|
if git.returncode != 0 or git.stdout.strip() != "true":
|
|
2032
2039
|
return {**payload, "route": "setup-repository", "wrote_state": False}
|
|
2040
|
+
default_branch = _common.origin_default_branch(root)
|
|
2041
|
+
if not _common.valid_default_branch(default_branch):
|
|
2042
|
+
return {**payload, "wrote_state": False, "error": (
|
|
2043
|
+
f"origin default branch {default_branch!r} cannot be recorded in STATE.md: "
|
|
2044
|
+
"it is a GSD Path reserved name or it has a character that STATE frontmatter "
|
|
2045
|
+
"cannot hold. Rename the default branch, then run "
|
|
2046
|
+
"`git remote set-head origin --auto`."
|
|
2047
|
+
)}
|
|
2033
2048
|
expected = "inspect" if payload["verdict"] == "brownfield" else "define"
|
|
2034
2049
|
if phase is None:
|
|
2035
2050
|
phase = expected
|
|
@@ -2043,7 +2058,7 @@ def initialize(
|
|
|
2043
2058
|
return payload
|
|
2044
2059
|
write_state_anchored(
|
|
2045
2060
|
root,
|
|
2046
|
-
filled_state_template(raw, project_slug(root), phase),
|
|
2061
|
+
filled_state_template(raw, project_slug(root), phase, default_branch),
|
|
2047
2062
|
root_status,
|
|
2048
2063
|
project_status,
|
|
2049
2064
|
)
|