@opengsd/gsd-path 1.5.0 → 1.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +1 -1
- package/CHANGELOG.md +169 -0
- package/DOCS.md +4 -3
- package/FULL.md +68 -7
- package/HOOKS.md +81 -4
- package/MIGRATE.md +12 -0
- package/QUICK.md +4 -2
- package/README.md +69 -16
- package/RUNTIME.md +23 -5
- package/UPDATE.md +35 -0
- package/WORKFLOW.md +66 -7
- package/package.json +1 -1
- package/scripts/_common.py +150 -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 +27 -19
- 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 +174 -36
- package/scripts/git_guard.py +42 -7
- package/scripts/guard_hook.py +1216 -47
- package/scripts/install.mjs +76 -3
- package/scripts/install.py +182 -37
- package/scripts/integration.py +115 -68
- package/scripts/isolation.py +192 -23
- package/scripts/members.py +68 -25
- package/scripts/migrate_core.py +2 -0
- package/scripts/model_policy.py +13 -2
- package/scripts/pipeline_diagnose.py +17 -2
- package/scripts/pipeline_git.py +87 -35
- package/scripts/pipeline_state.py +31 -7
- 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 +92 -5
- 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 +6 -2
- package/skills/gsd-path/ROADMAP.md +16 -6
- package/skills/gsd-path/SHIP.md +38 -11
- 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/model-policy.md +3 -1
- package/skills/gsd-path/references/operating-rules.md +14 -5
- 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 +10 -0
- package/skills/gsd-path/scripts/_common.py +150 -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 +27 -19
- 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 +174 -36
- package/skills/gsd-path/scripts/git_guard.py +42 -7
- package/skills/gsd-path/scripts/integration.py +115 -68
- package/skills/gsd-path/scripts/isolation.py +192 -23
- package/skills/gsd-path/scripts/members.py +68 -25
- package/skills/gsd-path/scripts/model_policy.py +13 -2
- package/skills/gsd-path/scripts/pipeline_git.py +87 -35
- package/skills/gsd-path/scripts/pipeline_state.py +31 -7
- 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 +92 -5
- 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/model-policy.md +3 -1
- package/skills/gsd-path-build/references/operating-rules.md +14 -5
- 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 +10 -0
- package/skills/gsd-path-build/scripts/_common.py +150 -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 +27 -19
- 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 +174 -36
- package/skills/gsd-path-build/scripts/integration.py +115 -68
- package/skills/gsd-path-build/scripts/isolation.py +192 -23
- package/skills/gsd-path-build/scripts/members.py +68 -25
- package/skills/gsd-path-build/scripts/model_policy.py +13 -2
- package/skills/gsd-path-build/scripts/pipeline_git.py +87 -35
- package/skills/gsd-path-build/scripts/pipeline_state.py +31 -7
- 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 +92 -5
- 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/model-policy.md +3 -1
- package/skills/gsd-path-decide/references/operating-rules.md +14 -5
- package/skills/gsd-path-decide/references/runtime-selection.md +10 -0
- package/skills/gsd-path-decide/scripts/_common.py +150 -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 +27 -19
- 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 +115 -68
- package/skills/gsd-path-decide/scripts/isolation.py +192 -23
- package/skills/gsd-path-decide/scripts/members.py +68 -25
- package/skills/gsd-path-decide/scripts/model_policy.py +13 -2
- package/skills/gsd-path-decide/scripts/pipeline_git.py +87 -35
- package/skills/gsd-path-decide/scripts/pipeline_state.py +31 -7
- 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 +92 -5
- 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 +14 -5
- package/skills/gsd-path-define/references/runtime-selection.md +10 -0
- package/skills/gsd-path-define/scripts/_common.py +150 -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 +27 -19
- 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 +115 -68
- package/skills/gsd-path-define/scripts/isolation.py +192 -23
- package/skills/gsd-path-define/scripts/members.py +68 -25
- package/skills/gsd-path-define/scripts/model_policy.py +13 -2
- package/skills/gsd-path-define/scripts/pipeline_git.py +87 -35
- package/skills/gsd-path-define/scripts/pipeline_state.py +31 -7
- 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 +92 -5
- package/skills/gsd-path-define/templates/state.md +1 -0
- package/skills/gsd-path-discuss/references/operating-rules.md +14 -5
- package/skills/gsd-path-discuss/references/runtime-selection.md +10 -0
- package/skills/gsd-path-discuss/scripts/_common.py +150 -1
- package/skills/gsd-path-discuss/scripts/discussion_records.py +9 -1
- package/skills/gsd-path-discuss/scripts/model_policy.py +13 -2
- package/skills/gsd-path-docs-audit/references/model-policy.md +3 -1
- package/skills/gsd-path-docs-audit/references/operating-rules.md +14 -5
- package/skills/gsd-path-docs-audit/references/runtime-selection.md +10 -0
- package/skills/gsd-path-docs-audit/scripts/_common.py +150 -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 +27 -19
- 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 +115 -68
- package/skills/gsd-path-docs-audit/scripts/isolation.py +192 -23
- package/skills/gsd-path-docs-audit/scripts/members.py +68 -25
- package/skills/gsd-path-docs-audit/scripts/model_policy.py +13 -2
- package/skills/gsd-path-docs-audit/scripts/pipeline_git.py +87 -35
- package/skills/gsd-path-docs-audit/scripts/pipeline_state.py +31 -7
- 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 +92 -5
- package/skills/gsd-path-forensics/references/operating-rules.md +14 -5
- package/skills/gsd-path-forensics/references/runtime-selection.md +10 -0
- package/skills/gsd-path-forensics/scripts/_common.py +150 -1
- package/skills/gsd-path-forensics/scripts/discussion_records.py +9 -1
- package/skills/gsd-path-forensics/scripts/model_policy.py +13 -2
- 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/model-policy.md +3 -1
- package/skills/gsd-path-inspect/references/operating-rules.md +14 -5
- package/skills/gsd-path-inspect/references/runtime-selection.md +10 -0
- package/skills/gsd-path-inspect/scripts/_common.py +150 -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 +27 -19
- 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 +115 -68
- package/skills/gsd-path-inspect/scripts/isolation.py +192 -23
- package/skills/gsd-path-inspect/scripts/members.py +68 -25
- package/skills/gsd-path-inspect/scripts/model_policy.py +13 -2
- package/skills/gsd-path-inspect/scripts/pipeline_git.py +87 -35
- package/skills/gsd-path-inspect/scripts/pipeline_state.py +31 -7
- 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 +92 -5
- 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 +14 -5
- package/skills/gsd-path-loop/references/runtime-selection.md +10 -0
- package/skills/gsd-path-loop/scripts/_common.py +150 -1
- package/skills/gsd-path-loop/scripts/discussion_records.py +9 -1
- package/skills/gsd-path-loop/scripts/model_policy.py +13 -2
- package/skills/gsd-path-migrate/SKILL.md +8 -0
- package/skills/gsd-path-migrate/references/operating-rules.md +14 -5
- package/skills/gsd-path-migrate/references/runtime-selection.md +10 -0
- package/skills/gsd-path-migrate/scripts/_common.py +150 -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 +13 -2
- package/skills/gsd-path-plan/SKILL.md +6 -2
- package/skills/gsd-path-plan/references/build-recovery.md +13 -0
- package/skills/gsd-path-plan/references/model-policy.md +3 -1
- package/skills/gsd-path-plan/references/operating-rules.md +14 -5
- package/skills/gsd-path-plan/references/planner.md +13 -1
- package/skills/gsd-path-plan/references/runtime-selection.md +10 -0
- package/skills/gsd-path-plan/scripts/_common.py +150 -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 +27 -19
- 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 +115 -68
- package/skills/gsd-path-plan/scripts/isolation.py +192 -23
- package/skills/gsd-path-plan/scripts/members.py +68 -25
- package/skills/gsd-path-plan/scripts/model_policy.py +13 -2
- package/skills/gsd-path-plan/scripts/pipeline_git.py +87 -35
- package/skills/gsd-path-plan/scripts/pipeline_state.py +31 -7
- 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 +92 -5
- 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/model-policy.md +3 -1
- package/skills/gsd-path-research/references/operating-rules.md +14 -5
- package/skills/gsd-path-research/references/runtime-selection.md +10 -0
- package/skills/gsd-path-research/scripts/_common.py +150 -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 +27 -19
- 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 +115 -68
- package/skills/gsd-path-research/scripts/isolation.py +192 -23
- package/skills/gsd-path-research/scripts/members.py +68 -25
- package/skills/gsd-path-research/scripts/model_policy.py +13 -2
- package/skills/gsd-path-research/scripts/pipeline_git.py +87 -35
- package/skills/gsd-path-research/scripts/pipeline_state.py +31 -7
- 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 +92 -5
- package/skills/gsd-path-roadmap/SKILL.md +16 -6
- package/skills/gsd-path-roadmap/references/model-policy.md +3 -1
- package/skills/gsd-path-roadmap/references/operating-rules.md +14 -5
- package/skills/gsd-path-roadmap/references/runtime-selection.md +10 -0
- package/skills/gsd-path-roadmap/scripts/_common.py +150 -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 +27 -19
- 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 +115 -68
- package/skills/gsd-path-roadmap/scripts/isolation.py +192 -23
- package/skills/gsd-path-roadmap/scripts/members.py +68 -25
- package/skills/gsd-path-roadmap/scripts/model_policy.py +13 -2
- package/skills/gsd-path-roadmap/scripts/pipeline_git.py +87 -35
- package/skills/gsd-path-roadmap/scripts/pipeline_state.py +31 -7
- 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 +92 -5
- package/skills/gsd-path-ship/SKILL.md +38 -11
- package/skills/gsd-path-ship/references/model-policy.md +3 -1
- package/skills/gsd-path-ship/references/operating-rules.md +14 -5
- 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 +10 -0
- package/skills/gsd-path-ship/scripts/_common.py +150 -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 +27 -19
- 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 +115 -68
- package/skills/gsd-path-ship/scripts/isolation.py +192 -23
- package/skills/gsd-path-ship/scripts/members.py +68 -25
- package/skills/gsd-path-ship/scripts/model_policy.py +13 -2
- package/skills/gsd-path-ship/scripts/pipeline_git.py +87 -35
- package/skills/gsd-path-ship/scripts/pipeline_state.py +31 -7
- 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 +92 -5
- 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 +14 -5
- package/skills/gsd-path-undo/references/runtime-selection.md +10 -0
- package/skills/gsd-path-undo/scripts/_common.py +150 -1
- package/skills/gsd-path-undo/scripts/discussion_records.py +9 -1
- package/skills/gsd-path-undo/scripts/model_policy.py +13 -2
- package/skills/gsd-path-undo/scripts/pipeline_undo.py +17 -8
- package/skills/path/DEFINE.md +6 -2
- package/skills/path/PLAN.md +6 -2
- package/skills/path/ROADMAP.md +16 -6
- package/skills/path/SHIP.md +38 -11
- 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/model-policy.md +3 -1
- package/skills/path/references/operating-rules.md +14 -5
- 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 +10 -0
- package/skills/path/scripts/_common.py +150 -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 +27 -19
- 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 +174 -36
- package/skills/path/scripts/git_guard.py +42 -7
- package/skills/path/scripts/integration.py +115 -68
- package/skills/path/scripts/isolation.py +192 -23
- package/skills/path/scripts/members.py +68 -25
- package/skills/path/scripts/model_policy.py +13 -2
- package/skills/path/scripts/pipeline_git.py +87 -35
- package/skills/path/scripts/pipeline_state.py +31 -7
- 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 +92 -5
- 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
|
@@ -343,6 +343,67 @@ def _completed_promotion(
|
|
|
343
343
|
}
|
|
344
344
|
|
|
345
345
|
|
|
346
|
+
def _ruling_rows(text: str) -> dict[str, list[str]]:
|
|
347
|
+
"""Map Queue # to the cells of each `## User rulings` table row."""
|
|
348
|
+
rows: dict[str, list[str]] = {}
|
|
349
|
+
in_rulings = False
|
|
350
|
+
for line in text.splitlines():
|
|
351
|
+
if line == "## User rulings":
|
|
352
|
+
in_rulings = True
|
|
353
|
+
continue
|
|
354
|
+
if in_rulings and line.startswith("## "):
|
|
355
|
+
break
|
|
356
|
+
if in_rulings and line.lstrip().startswith("|"):
|
|
357
|
+
cells = [cell.strip() for cell in line.strip().strip("|").split("|")]
|
|
358
|
+
if len(cells) == 4 and cells[0].isdigit():
|
|
359
|
+
rows[cells[0]] = cells
|
|
360
|
+
return rows
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
def _superseded_carry_forward(
|
|
364
|
+
repo: Path,
|
|
365
|
+
active_state: PipelineState,
|
|
366
|
+
source: Path,
|
|
367
|
+
destination: Path,
|
|
368
|
+
) -> bool:
|
|
369
|
+
"""True when active research is only the shipped DOCS-AUDIT carry-forward
|
|
370
|
+
and the lookahead audit keeps every one of its rulings."""
|
|
371
|
+
if not active_state.archive or destination.is_symlink() or not destination.is_dir():
|
|
372
|
+
return False
|
|
373
|
+
archived = repo / active_state.archive / "research" / "DOCS-AUDIT.md"
|
|
374
|
+
active = destination / "DOCS-AUDIT.md"
|
|
375
|
+
lookahead = source / "DOCS-AUDIT.md"
|
|
376
|
+
entries = [
|
|
377
|
+
path for path in destination.iterdir() if not state_checkpoint._is_junk_file(path)
|
|
378
|
+
]
|
|
379
|
+
if entries != [active] or any(
|
|
380
|
+
path.is_symlink() or not path.is_file() for path in (archived, active, lookahead)
|
|
381
|
+
):
|
|
382
|
+
return False
|
|
383
|
+
carried = active.read_bytes()
|
|
384
|
+
if carried != archived.read_bytes():
|
|
385
|
+
return False
|
|
386
|
+
rulings = _ruling_rows(carried.decode("utf-8"))
|
|
387
|
+
if not any(cells[3] == "no" for cells in rulings.values()):
|
|
388
|
+
return False
|
|
389
|
+
kept = _ruling_rows(lookahead.read_text(encoding="utf-8"))
|
|
390
|
+
for number, cells in rulings.items():
|
|
391
|
+
other = kept.get(number)
|
|
392
|
+
# Plan may record a T### for a pending ruling; nothing else may change.
|
|
393
|
+
if (
|
|
394
|
+
other is None
|
|
395
|
+
or other[:3] != cells[:3]
|
|
396
|
+
or (
|
|
397
|
+
other[3] != cells[3]
|
|
398
|
+
and not (cells[3] == "no" and re.fullmatch(r"T\d{3}", other[3]))
|
|
399
|
+
)
|
|
400
|
+
):
|
|
401
|
+
raise PipelineStateError(
|
|
402
|
+
f"lookahead DOCS-AUDIT.md does not keep carry-forward ruling #{number}"
|
|
403
|
+
)
|
|
404
|
+
return True
|
|
405
|
+
|
|
406
|
+
|
|
346
407
|
def _prepare_promotion(
|
|
347
408
|
repo: Path,
|
|
348
409
|
project: Path,
|
|
@@ -377,17 +438,22 @@ def _prepare_promotion(
|
|
|
377
438
|
head = pipeline_state._run_git(repo, "rev-parse", "HEAD").stdout.strip()
|
|
378
439
|
if head != base:
|
|
379
440
|
raise PipelineStateError(f"promotion must start at base SHA: {head} != {base}")
|
|
380
|
-
|
|
441
|
+
active_state, active_text, _ = pipeline_state.load_state(repo)
|
|
442
|
+
remote_default = f"origin/{active_state.default_branch}"
|
|
443
|
+
remote = pipeline_state._run_git(
|
|
444
|
+
repo, "rev-parse", "--verify", f"{remote_default}^{{commit}}"
|
|
445
|
+
).stdout.strip()
|
|
381
446
|
if remote != base:
|
|
382
447
|
raise PipelineStateError(
|
|
383
|
-
f"base SHA is not current
|
|
448
|
+
f"base SHA is not current {remote_default}: {base} != {remote}"
|
|
384
449
|
)
|
|
385
450
|
if not pipeline_state._is_ancestor(repo, landing, base):
|
|
386
|
-
raise PipelineStateError(
|
|
451
|
+
raise PipelineStateError(
|
|
452
|
+
f"landing is not an ancestor of the current {active_state.default_branch} base"
|
|
453
|
+
)
|
|
387
454
|
if pipeline_state._run_git(repo, "status", "--porcelain", "--untracked-files=all").stdout:
|
|
388
455
|
raise PipelineStateError("promotion must start from a clean worktree")
|
|
389
456
|
|
|
390
|
-
active_state, active_text, _ = pipeline_state.load_state(repo)
|
|
391
457
|
if active_state.phase != "shipped" or active_state.status != "done":
|
|
392
458
|
raise PipelineStateError("promotion requires active STATE shipped/done")
|
|
393
459
|
if active_state.milestone is None:
|
|
@@ -456,6 +522,12 @@ def _prepare_promotion(
|
|
|
456
522
|
raise PipelineStateError(
|
|
457
523
|
"lookahead integration_default does not match the active project"
|
|
458
524
|
)
|
|
525
|
+
if next_state.default_branch != active_state.default_branch:
|
|
526
|
+
raise PipelineStateError(
|
|
527
|
+
"lookahead default_branch does not match the active project: copy "
|
|
528
|
+
f"`default_branch: {active_state.default_branch}` from .project/STATE.md "
|
|
529
|
+
"into .project/next/STATE.md"
|
|
530
|
+
)
|
|
459
531
|
if next_state.milestone != milestone:
|
|
460
532
|
raise PipelineStateError(
|
|
461
533
|
f"requested milestone {milestone} != next STATE milestone {next_state.milestone}"
|
|
@@ -464,12 +536,17 @@ def _prepare_promotion(
|
|
|
464
536
|
raise PipelineStateError("lookahead STATE branch and archive must be null")
|
|
465
537
|
|
|
466
538
|
tracks: dict[str, str] = {}
|
|
539
|
+
carry_forward: Optional[str] = None
|
|
467
540
|
for name in PROMOTION_TRACKS:
|
|
468
541
|
source = next_root / name
|
|
469
542
|
destination = project / name
|
|
470
543
|
if source.exists() or source.is_symlink():
|
|
471
544
|
if destination.exists() or destination.is_symlink():
|
|
472
|
-
|
|
545
|
+
if name != "research" or not _superseded_carry_forward(
|
|
546
|
+
repo, active_state, source, destination
|
|
547
|
+
):
|
|
548
|
+
raise PipelineStateError(f"promotion destination already exists: {destination}")
|
|
549
|
+
carry_forward = state_checkpoint._tree_digest(destination)
|
|
473
550
|
tracks[name] = state_checkpoint._tree_digest(source)
|
|
474
551
|
if not tracks:
|
|
475
552
|
raise PipelineStateError("lookahead track has no promotable artifacts")
|
|
@@ -496,6 +573,7 @@ def _prepare_promotion(
|
|
|
496
573
|
"landing": landing,
|
|
497
574
|
"base": base,
|
|
498
575
|
"tracks": tracks,
|
|
576
|
+
"carry_forward_sha256": carry_forward,
|
|
499
577
|
"residual_sha256": state_checkpoint._tree_digest(next_root, tuple(tracks)),
|
|
500
578
|
"next_state_sha256": state_checkpoint._sha256(next_text),
|
|
501
579
|
"active_state_sha256": state_checkpoint._sha256(active_text),
|
|
@@ -548,6 +626,15 @@ def _resume_track_moves(project: Path, journal: dict[str, object], path: Path) -
|
|
|
548
626
|
destination = project / name
|
|
549
627
|
source_exists = source.exists() or source.is_symlink()
|
|
550
628
|
destination_exists = destination.exists() or destination.is_symlink()
|
|
629
|
+
carry_forward = journal.get("carry_forward_sha256")
|
|
630
|
+
if name == "research" and source_exists and destination_exists and carry_forward:
|
|
631
|
+
if state_checkpoint._tree_digest(destination) != carry_forward:
|
|
632
|
+
raise PipelineStateError("DOCS-AUDIT carry-forward drifted during promotion")
|
|
633
|
+
# The archive and the ship commit keep the carry-forward copy.
|
|
634
|
+
shutil.rmtree(destination)
|
|
635
|
+
journal["stage"] = "removed:carry-forward"
|
|
636
|
+
pipeline_state._write_json(path, journal)
|
|
637
|
+
destination_exists = False
|
|
551
638
|
if source_exists and destination_exists:
|
|
552
639
|
raise PipelineStateError(f"promotion has both source and destination: {name}")
|
|
553
640
|
if not source_exists and not destination_exists:
|
|
@@ -210,8 +210,14 @@ def run_workflow(repo: Path, action: str, project_dir: str, expected_head: str =
|
|
|
210
210
|
"--base", expected_head, "--task-id", task_id,
|
|
211
211
|
"--round-size", str(round_size))
|
|
212
212
|
if task["mode"] == "serial":
|
|
213
|
+
name = f"task-{task_id.lower()}-verify"
|
|
214
|
+
stale = isolation.sidecar_root(repo, "verify", name)
|
|
215
|
+
if os.path.lexists(stale / ".git"):
|
|
216
|
+
# An earlier prepare-task left this sidecar. Only one that is proven untouched is replaced.
|
|
217
|
+
steps.append({"script": "isolation.py", "result": isolation.retire_stale_verify(
|
|
218
|
+
repo, stale, isolation.verify_branch_name(name))})
|
|
213
219
|
step("isolation.py", "isolate-verify", "--repo", str(repo),
|
|
214
|
-
"--base", expected_head, "--name",
|
|
220
|
+
"--base", expected_head, "--name", name)
|
|
215
221
|
elif action == "build-evidence":
|
|
216
222
|
proof = step("build_state.py", "verify-landed", *common, "--head", expected_head)
|
|
217
223
|
# The landing proof has one canonical home; lean final-review reuse allows only this path.
|
|
@@ -278,7 +284,8 @@ def run_workflow(repo: Path, action: str, project_dir: str, expected_head: str =
|
|
|
278
284
|
if action == "approve-plan":
|
|
279
285
|
step("pipeline_state.py", "approve", *common, "--kind", "plan",
|
|
280
286
|
"--expected-head", expected_head)
|
|
281
|
-
except (StepFailed, OSError, subprocess.CalledProcessError,
|
|
287
|
+
except (StepFailed, isolation.IsolationError, OSError, subprocess.CalledProcessError,
|
|
288
|
+
ValueError, KeyError, TypeError) as error:
|
|
282
289
|
return {"status": "blocked", "reason": str(error), "steps": steps,
|
|
283
290
|
"next": next_step}
|
|
284
291
|
return {"status": "complete", "steps": steps, **outputs}
|
|
@@ -21,12 +21,12 @@ Overall verdict: <pass | blocked>
|
|
|
21
21
|
|
|
22
22
|
## Success criteria
|
|
23
23
|
|
|
24
|
-
<!-- Each `### SCn —` title
|
|
25
|
-
|
|
26
|
-
space
|
|
27
|
-
|
|
24
|
+
<!-- Each `### SCn —` title has every word of the criterion in INTENT.md
|
|
25
|
+
`## Success criteria`: join continuation lines and sub-bullets with a
|
|
26
|
+
single space (the `- ` markers of sub-bullets are optional). The gate
|
|
27
|
+
compares every other word. -->
|
|
28
28
|
|
|
29
|
-
### SC1 — <criterion
|
|
29
|
+
### SC1 — <criterion from INTENT.md>
|
|
30
30
|
|
|
31
31
|
- **Verdict**: <met | not-met | unverifiable>
|
|
32
32
|
- **Surface**: <exactly the surface name PLAN.md's Surface contract lists this criterion under, with no detail after it; omit the whole field for any other criterion>
|
|
@@ -26,9 +26,17 @@ Tasks reviewed: <count>
|
|
|
26
26
|
|
|
27
27
|
## T001 — <title>: <pass | fail>
|
|
28
28
|
|
|
29
|
-
<!--
|
|
30
|
-
|
|
31
|
-
|
|
29
|
+
<!-- Task sections go in task id order (T001 before T002), not in the order
|
|
30
|
+
of the PLAN.md wave table. The heading title is the exact `title:` of
|
|
31
|
+
the task file. An evidence line starts with `- ✅ ` (pass) or `- ❌ `
|
|
32
|
+
(fail). A section needs one or more lines with the marker of its own
|
|
33
|
+
verdict, so a failed task needs a `- ❌ ` criterion line.
|
|
34
|
+
Per task criterion: copy every word of the acceptance/interface
|
|
35
|
+
criterion before the em dash; join wrapped lines and sub-bullets with
|
|
36
|
+
spaces (the `- ` markers of sub-bullets are optional). An AC id or
|
|
37
|
+
summary alone cannot be matched by review_findings.py.
|
|
38
|
+
Write literal markup and code in backticks inside a sentence: text
|
|
39
|
+
such as <b> outside backticks reads as an unfilled placeholder. -->
|
|
32
40
|
- ✅ <criterion> — <evidence: verify output / file:line checked>
|
|
33
41
|
- ❌ <criterion> — found: <what exists instead, file:line>
|
|
34
42
|
fix: <concrete direction a coder can execute without re-investigating>
|
|
@@ -47,13 +55,14 @@ Contract violations (blocking):
|
|
|
47
55
|
wave, ending `: pass` or `: fail`. `scripts/check_handoffs.py wave
|
|
48
56
|
--review <this file>` gates the SC id set and verdicts. Omit this
|
|
49
57
|
section when no task in the wave owns an SC.
|
|
50
|
-
Copy the criterion
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
58
|
+
Copy every word of the criterion from INTENT.md `## Success criteria`:
|
|
59
|
+
join indented continuation lines and sub-bullets with a single space
|
|
60
|
+
(the `- ` markers of sub-bullets are optional). The gate compares every
|
|
61
|
+
other word: no paraphrase, no dropped sub-bullet.
|
|
62
|
+
Each SC block needs an evidence line with the marker of its own
|
|
63
|
+
verdict: `- ✅ ` (pass) or `- ❌ ` (fail). -->
|
|
55
64
|
|
|
56
|
-
### SC1 — <criterion
|
|
65
|
+
### SC1 — <criterion from INTENT.md>: <pass | fail>
|
|
57
66
|
- ✅ <evidence: verify output / file:line checked>
|
|
58
67
|
- ❌ <criterion> — found: <what exists instead, file:line>
|
|
59
68
|
fix: <concrete direction a coder can execute without re-investigating>
|
|
@@ -38,8 +38,8 @@ bundles this file; spawned agents follow their role brief instead.
|
|
|
38
38
|
the lookahead planning track for the next milestone; it follows the same
|
|
39
39
|
marker rule, owns no task frontmatter, and never binds a branch.
|
|
40
40
|
- STATE.integration_default is the project closeout choice and
|
|
41
|
-
STATE.integration is the current milestone choice. Both are `direct
|
|
42
|
-
`pull-request`; STATE.integration_source is `default` or `milestone` and
|
|
41
|
+
STATE.integration is the current milestone choice. Both are `direct`,
|
|
42
|
+
`pull-request`, or `external-landing`; STATE.integration_source is `default` or `milestone` and
|
|
43
43
|
preserves explicit override provenance. Older v2 state defaults to `direct`,
|
|
44
44
|
and modes may be changed only through `pipeline_state.py
|
|
45
45
|
configure-integration` before build. A next milestone resets its current
|
|
@@ -82,7 +82,9 @@ bundles this file; spawned agents follow their role brief instead.
|
|
|
82
82
|
`isolation.py attest` may stand in for it: one `.project/`-only commit that
|
|
83
83
|
names the base, the attested HEAD, the declared files that changed, the
|
|
84
84
|
task Verify recorded as passing at that HEAD, and the ruling verbatim.
|
|
85
|
-
`recover` reports it as `attested`, never as a proven landing.
|
|
85
|
+
`recover` reports it as `attested`, never as a proven landing. Attestation
|
|
86
|
+
matches the declared files literally: work that is only below a declared
|
|
87
|
+
directory can land but cannot be attested.
|
|
86
88
|
- All pipeline work for a milestone lives on `gsd-path/M00N` (M001, M002,
|
|
87
89
|
…) bound in STATE.branch. The next milestone binds a new unused
|
|
88
90
|
`gsd-path/M00N` at the remote default after the previous ship integrates.
|
|
@@ -90,6 +92,12 @@ bundles this file; spawned agents follow their role brief instead.
|
|
|
90
92
|
branch — either Path's direct merge or a Path-owned PR merged by the user.
|
|
91
93
|
The bound branch never receives merges or back-merges and is never the
|
|
92
94
|
GitHub default.
|
|
95
|
+
- The default branch is the one that STATE `default_branch` records at
|
|
96
|
+
initialization; a STATE file without the field means `main`. `main` and
|
|
97
|
+
`origin/main` in these rules mean that branch. Never change the record and
|
|
98
|
+
never take the name from a later `origin/HEAD` lookup. For a member
|
|
99
|
+
repository, the branch is the one that its `.project/MEMBERS.md` section
|
|
100
|
+
records as `Default branch`; a section without the field means `main`.
|
|
93
101
|
- After initialization writes STATE.md, and before entering the first pipeline
|
|
94
102
|
phase in an existing Git repository, the router calls
|
|
95
103
|
`scripts/pipeline_git.py bind-initial` with the selected M00N and exact
|
|
@@ -128,8 +136,9 @@ bundles this file; spawned agents follow their role brief instead.
|
|
|
128
136
|
for the current STATE.branch milestone must be an ancestor of origin/main;
|
|
129
137
|
if it is not, that milestone's integration is incomplete and control routes
|
|
130
138
|
to ship, not build. Direct mode requires the matching canonical `integrate:`
|
|
131
|
-
commit; PR mode requires tag metadata and a two-parent merge whose
|
|
132
|
-
parent is the ship commit
|
|
139
|
+
commit; PR mode requires tag metadata and either a two-parent merge whose
|
|
140
|
+
second parent is the ship commit or a proven rebase merge of the ship
|
|
141
|
+
commits. A null branch returns
|
|
133
142
|
to the router; build never creates, selects, switches, or rebinds it.
|
|
134
143
|
- The ship phase makes exactly one commit on the bound branch — the
|
|
135
144
|
`.project/`-only ship commit recording
|
|
@@ -20,6 +20,16 @@ references to bundled copies of those helpers. Other skill helpers, templates,
|
|
|
20
20
|
and role briefs remain in the installed skill bundle. Pass `-B` to Python helper
|
|
21
21
|
invocations so execution does not create bytecode in the runtime store.
|
|
22
22
|
|
|
23
|
+
The selected runtime is authoritative for each helper in its `manifest.json`;
|
|
24
|
+
this includes `review_findings.py` and `task_context.py`. A selected runtime
|
|
25
|
+
that was published before a helper became a runtime file does not list that
|
|
26
|
+
helper: run the bundled copy until the project runtime is upgraded.
|
|
27
|
+
`dispatch_driver.py` and `workflow_run.py` always run from the installed skill
|
|
28
|
+
bundle, because they read role briefs and templates from it; a skill update
|
|
29
|
+
replaces them, a runtime upgrade does not. The driver reloads the helper modules
|
|
30
|
+
that it imports from the selected runtime, and runs the subprocess helpers that
|
|
31
|
+
the runtime lists (such as the fix-task brief lint) from it.
|
|
32
|
+
|
|
23
33
|
With no declaration, retain the legacy helper lookup. An existing
|
|
24
34
|
`.gsd-path/runtime/` requires the explicit `--runtime-migrate --project PATH`
|
|
25
35
|
operation before adopting external storage; review the resulting Git diff.
|
|
@@ -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."""
|
|
@@ -255,6 +361,49 @@ def run_git(
|
|
|
255
361
|
return _run_exact(git_command(repo, *arguments), input=input)
|
|
256
362
|
|
|
257
363
|
|
|
364
|
+
def _patch_ids(repo: Path, commits: Sequence[str]) -> list[Optional[str]]:
|
|
365
|
+
"""`git patch-id --stable` per commit, in order; None for an empty commit."""
|
|
366
|
+
log = run_git(repo, "log", "--no-walk=unsorted", "-p", "--binary", "--no-color",
|
|
367
|
+
"--no-ext-diff", *commits)
|
|
368
|
+
if log.returncode != 0:
|
|
369
|
+
raise ValueError("could not read commit patches")
|
|
370
|
+
ids = run_git(repo, "patch-id", "--stable", input=log.stdout)
|
|
371
|
+
if ids.returncode != 0:
|
|
372
|
+
raise ValueError("could not compute patch ids")
|
|
373
|
+
by_commit = {commit: patch for patch, commit in (line.split() for line in ids.stdout.splitlines())}
|
|
374
|
+
return [by_commit.get(commit) for commit in commits]
|
|
375
|
+
|
|
376
|
+
|
|
377
|
+
def rebase_landing_problem(repo: Path, ship: str, landing: str) -> Optional[str]:
|
|
378
|
+
"""Why landing is not a forge rebase merge of ship, or None when it is (#418).
|
|
379
|
+
|
|
380
|
+
A rebase merge replays ship's own commits onto the default branch. Proof:
|
|
381
|
+
the first-parent run that ends at landing has one non-merge commit per
|
|
382
|
+
non-merge commit of landing..ship, with equal patch ids in the same order;
|
|
383
|
+
when that run starts at ship's fork point, landing's tree equals ship's.
|
|
384
|
+
"""
|
|
385
|
+
listed = run_git(repo, "rev-list", "--reverse", "--topo-order", "--no-merges", f"{landing}..{ship}")
|
|
386
|
+
if listed.returncode != 0:
|
|
387
|
+
return "could not list the ship commits"
|
|
388
|
+
originals = listed.stdout.split()
|
|
389
|
+
if not originals:
|
|
390
|
+
return "landing already contains the ship commit"
|
|
391
|
+
walked = run_git(repo, "rev-list", "--first-parent", "--parents", "-n", str(len(originals)), landing)
|
|
392
|
+
rows = [line.split() for line in walked.stdout.splitlines()][::-1]
|
|
393
|
+
if walked.returncode != 0 or len(rows) != len(originals) or any(len(row) != 2 for row in rows):
|
|
394
|
+
return "landing is not a linear run of rebased commits"
|
|
395
|
+
try:
|
|
396
|
+
if _patch_ids(repo, originals) != _patch_ids(repo, [row[0] for row in rows]):
|
|
397
|
+
return "rebased commits do not replay the ship commits in order"
|
|
398
|
+
except ValueError as error:
|
|
399
|
+
return str(error)
|
|
400
|
+
fork = run_git(repo, "merge-base", ship, landing).stdout.strip()
|
|
401
|
+
trees = run_git(repo, "rev-parse", f"{ship}^{{tree}}", f"{landing}^{{tree}}").stdout.split()
|
|
402
|
+
if rows[0][1] == fork and (len(trees) != 2 or trees[0] != trees[1]):
|
|
403
|
+
return "rebased tree differs from the ship tree"
|
|
404
|
+
return None
|
|
405
|
+
|
|
406
|
+
|
|
258
407
|
def git_command(repo: Path, *arguments: str) -> tuple[str, ...]:
|
|
259
408
|
"""Git argv for repo, including WSL drvfs workarounds when needed."""
|
|
260
409
|
return ("git", "-C", str(repo), *git_drvfs_config_flags(repo), *arguments)
|
|
@@ -7,6 +7,7 @@ import json
|
|
|
7
7
|
import os
|
|
8
8
|
import shutil
|
|
9
9
|
import sys
|
|
10
|
+
import traceback
|
|
10
11
|
|
|
11
12
|
# Runtime helpers must not modify their immutable installation.
|
|
12
13
|
sys.dont_write_bytecode = True
|
|
@@ -33,7 +34,7 @@ def _load_pipeline_modules():
|
|
|
33
34
|
try:
|
|
34
35
|
from scripts import archive_milestone, pipeline_state, _common
|
|
35
36
|
return _common, archive_milestone, pipeline_state
|
|
36
|
-
except
|
|
37
|
+
except ImportError as error: # a foreign `scripts` package
|
|
37
38
|
if error.name not in {
|
|
38
39
|
"scripts",
|
|
39
40
|
"scripts.archive_milestone",
|
|
@@ -41,6 +42,13 @@ def _load_pipeline_modules():
|
|
|
41
42
|
"scripts._common",
|
|
42
43
|
}:
|
|
43
44
|
raise
|
|
45
|
+
# A plain ImportError raised while a `scripts` module loaded is not a
|
|
46
|
+
# foreign package. A missing module falls back, as before.
|
|
47
|
+
if not isinstance(error, ModuleNotFoundError) and any(
|
|
48
|
+
frame.f_globals.get("__name__", "").startswith("scripts.")
|
|
49
|
+
for frame, _ in traceback.walk_tb(error.__traceback__)
|
|
50
|
+
):
|
|
51
|
+
raise
|
|
44
52
|
shared_scripts = Path(__file__).resolve().parents[2] / "gsd-path" / "scripts"
|
|
45
53
|
sys.path.insert(0, str(shared_scripts))
|
|
46
54
|
import archive_milestone
|
|
@@ -196,16 +196,27 @@ def task_settings(path):
|
|
|
196
196
|
|
|
197
197
|
def native_record(args):
|
|
198
198
|
"""The parent owns lifecycle; release requires its recorded terminal-result receipt."""
|
|
199
|
+
# A relative path would resolve against cwd, which may be the immutable runtime store.
|
|
200
|
+
if not args.record.is_absolute():
|
|
201
|
+
raise PolicyError(f'--record must be an absolute path: {args.record}')
|
|
199
202
|
record = args.record.resolve()
|
|
203
|
+
missing = f'release requires an existing record and terminal-result evidence: {record}'
|
|
204
|
+
# Check before log_lock creates the record directory and its lock file.
|
|
205
|
+
if args.action == 'release' and not record.exists():
|
|
206
|
+
raise PolicyError(missing)
|
|
200
207
|
with log_lock({'log': str(record)}):
|
|
201
208
|
data = read_json(record) if record.exists() else None
|
|
202
209
|
identity = {'repo': str(args.repo.resolve()), 'project_dir': args.project_dir,
|
|
203
210
|
'scope': args.scope, 'assignment': args.assignment, 'role': args.role}
|
|
204
211
|
if data is not None and data.get('identity') != identity:
|
|
205
|
-
|
|
212
|
+
recorded = data.get('identity')
|
|
213
|
+
recorded = recorded if isinstance(recorded, dict) else {}
|
|
214
|
+
differing = ', '.join(f'{field}: {value}, record has {recorded.get(field)}'
|
|
215
|
+
for field, value in identity.items() if recorded.get(field) != value)
|
|
216
|
+
raise PolicyError(f'record belongs to another assignment or milestone/track/cycle ({differing})')
|
|
206
217
|
if args.action == 'release':
|
|
207
218
|
if data is None or not args.terminal_result:
|
|
208
|
-
raise PolicyError(
|
|
219
|
+
raise PolicyError(missing)
|
|
209
220
|
data.update(active=False, terminal_result=args.terminal_result)
|
|
210
221
|
else:
|
|
211
222
|
if args.capabilities is None:
|
|
@@ -22,6 +22,7 @@ from datetime import date
|
|
|
22
22
|
# Runtime helpers must not modify their immutable installation.
|
|
23
23
|
sys.dont_write_bytecode = True
|
|
24
24
|
import tempfile
|
|
25
|
+
import traceback
|
|
25
26
|
from pathlib import Path, PurePosixPath
|
|
26
27
|
from typing import Optional, Sequence
|
|
27
28
|
|
|
@@ -53,7 +54,7 @@ def _load_pipeline_modules():
|
|
|
53
54
|
try:
|
|
54
55
|
from scripts import archive_milestone, isolation, pipeline_git, pipeline_state, state_checkpoint
|
|
55
56
|
return archive_milestone, isolation, pipeline_git, pipeline_state, state_checkpoint
|
|
56
|
-
except
|
|
57
|
+
except ImportError as error: # a foreign `scripts` package
|
|
57
58
|
if error.name not in {
|
|
58
59
|
"scripts",
|
|
59
60
|
"scripts.archive_milestone",
|
|
@@ -63,6 +64,13 @@ def _load_pipeline_modules():
|
|
|
63
64
|
"scripts.state_checkpoint",
|
|
64
65
|
}:
|
|
65
66
|
raise
|
|
67
|
+
# A plain ImportError raised while a `scripts` module loaded is not a
|
|
68
|
+
# foreign package. A missing module falls back, as before.
|
|
69
|
+
if not isinstance(error, ModuleNotFoundError) and any(
|
|
70
|
+
frame.f_globals.get("__name__", "").startswith("scripts.")
|
|
71
|
+
for frame, _ in traceback.walk_tb(error.__traceback__)
|
|
72
|
+
):
|
|
73
|
+
raise
|
|
66
74
|
shared = Path(__file__).resolve().parents[2] / "gsd-path" / "scripts"
|
|
67
75
|
sys.path.insert(0, str(shared))
|
|
68
76
|
import archive_milestone
|
|
@@ -772,14 +780,15 @@ def _member_undo_parent(repo: Path, member: str, landing: str) -> str:
|
|
|
772
780
|
bound = entry["branch"]
|
|
773
781
|
if _run_git(checkout, "rev-parse", f"refs/heads/{bound}").stdout.strip() != landing:
|
|
774
782
|
raise UndoError(f"member {member} {bound} moved past this landing")
|
|
775
|
-
_require_member_unpublished(checkout, member, landing, bound)
|
|
783
|
+
_require_member_unpublished(repo, checkout, member, landing, bound)
|
|
776
784
|
return _run_git(checkout, "rev-parse", f"{landing}^").stdout.strip()
|
|
777
785
|
|
|
778
786
|
|
|
779
|
-
def _require_member_unpublished(checkout: Path, member: str, landing: str, bound: str) -> None:
|
|
780
|
-
for
|
|
787
|
+
def _require_member_unpublished(repo: Path, checkout: Path, member: str, landing: str, bound: str) -> None:
|
|
788
|
+
for branch in (isolation.member_default_branch(repo, member), bound):
|
|
789
|
+
ref = f"refs/remotes/origin/{branch}"
|
|
781
790
|
if _run_git(checkout, "merge-base", "--is-ancestor", landing, ref, check=False).returncode == 0:
|
|
782
|
-
raise UndoError(f"member {member} landing is already on {
|
|
791
|
+
raise UndoError(f"member {member} landing is already on origin/{branch}")
|
|
783
792
|
|
|
784
793
|
|
|
785
794
|
def _require_no_member_landing_journal(repo: Path) -> None:
|
|
@@ -794,7 +803,7 @@ def _undo_member_landing(repo: Path, transaction: dict[str, object]) -> None:
|
|
|
794
803
|
checkout, _, entry = isolation._member_context(repo, member)
|
|
795
804
|
tip = _run_git(checkout, "rev-parse", f"refs/heads/{entry['branch']}").stdout.strip()
|
|
796
805
|
if tip == transaction["member_parent"]:
|
|
797
|
-
_require_member_unpublished(checkout, member, str(transaction["landing"]), entry["branch"])
|
|
806
|
+
_require_member_unpublished(repo, checkout, member, str(transaction["landing"]), entry["branch"])
|
|
798
807
|
return
|
|
799
808
|
if tip != transaction["landing"]:
|
|
800
809
|
raise UndoError(f"member {member} bound branch moved during undo")
|
|
@@ -861,7 +870,7 @@ def classify_undo(repo: Path) -> dict[str, object]:
|
|
|
861
870
|
[f"an in-progress helper journal exists ({names}); resume that transaction first"]
|
|
862
871
|
)
|
|
863
872
|
if git.get("ancestor_of_origin_main"):
|
|
864
|
-
return _blocked(["HEAD is an ancestor of origin
|
|
873
|
+
return _blocked(["HEAD is an ancestor of the origin default branch; undo would rewrite shipped history"])
|
|
865
874
|
|
|
866
875
|
product = _product_dirty(dirty)
|
|
867
876
|
if product:
|
|
@@ -1081,7 +1090,7 @@ def _require_unpublished_reset(repo: Path, expected_branch: str) -> None:
|
|
|
1081
1090
|
if git.get("published"):
|
|
1082
1091
|
raise UndoError("HEAD became published on its recorded remote branch")
|
|
1083
1092
|
if git.get("ancestor_of_origin_main"):
|
|
1084
|
-
raise UndoError("HEAD became published through origin
|
|
1093
|
+
raise UndoError("HEAD became published through the origin default branch")
|
|
1085
1094
|
|
|
1086
1095
|
|
|
1087
1096
|
def _remove_untracked_tree(repo: Path, relative: str) -> None:
|
package/skills/path/DEFINE.md
CHANGED
|
@@ -168,8 +168,12 @@ State ownership. There is no re-interview of charter or roadmap scope:
|
|
|
168
168
|
and prohibition ruling for the user to accept or change; it never rules on
|
|
169
169
|
them silently. A requested change that contradicts the
|
|
170
170
|
charter or the resolved roadmap entry is a scope change — this phase never
|
|
171
|
-
edits the roadmap; surface it to the user for a `$gsd-path-roadmap`
|
|
172
|
-
|
|
171
|
+
edits the roadmap; surface it to the user for a `$gsd-path-roadmap`
|
|
172
|
+
re-slice. Before intent approval, that re-slice keeps `define/active` and
|
|
173
|
+
can change the scope of this milestone's `active` entry; after its
|
|
174
|
+
approval, draft INTENT.md again from the changed entry. In `corrections`
|
|
175
|
+
mode the intent is already approved, so the re-slice waits for the next
|
|
176
|
+
milestone boundary.
|
|
173
177
|
4. On approval, finalize per the Output contract, setting `milestone` to the
|
|
174
178
|
resolved entry's slug. The router continues with milestone-scoped research
|
|
175
179
|
when the entry lists open questions, otherwise planning.
|