@opengsd/gsd-path 1.4.0 → 1.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +460 -341
- package/DOCS.md +16 -5
- package/FULL.md +67 -6
- package/HOOKS.md +158 -10
- package/MIGRATE.md +12 -0
- package/QUICK.md +4 -2
- package/README.md +136 -116
- package/RUNTIME.md +24 -6
- package/UPDATE.md +39 -3
- package/WORKFLOW.md +80 -4
- package/package.json +5 -2
- package/platforms/shared-agents/dispatch.md +13 -0
- package/scripts/_common.py +256 -2
- package/scripts/archive_milestone.py +14 -13
- package/scripts/build_recovery.py +155 -58
- package/scripts/build_state.py +274 -8
- package/scripts/check_docs_audit.py +77 -4
- package/scripts/check_handoffs.py +635 -57
- package/scripts/check_task_briefs.py +293 -32
- package/scripts/detect_project.py +260 -20
- package/scripts/discussion_records.py +9 -1
- package/scripts/discussion_validate.py +93 -35
- package/scripts/dispatch_driver.py +549 -53
- package/scripts/git_guard.py +27 -8
- package/scripts/guard_hook.py +2630 -210
- package/scripts/install.mjs +99 -11
- package/scripts/install.py +231 -46
- package/scripts/integration.py +244 -65
- package/scripts/isolation.py +539 -70
- package/scripts/loop_run.py +16 -4
- package/scripts/members.py +77 -32
- package/scripts/migrate_core.py +2 -0
- package/scripts/model_policy.py +5 -1
- package/scripts/path_config.py +3 -3
- package/scripts/pipeline_diagnose.py +17 -2
- package/scripts/pipeline_git.py +131 -42
- package/scripts/pipeline_state.py +139 -39
- package/scripts/pipeline_undo.py +118 -16
- package/scripts/promote_lookahead.py +3 -3
- package/scripts/review_findings.py +180 -44
- package/scripts/roadmap.py +9 -1
- package/scripts/runtime_store.py +21 -2
- package/scripts/skill-resources.json +9 -2
- package/scripts/state_checkpoint.py +39 -36
- package/scripts/state_promote.py +15 -4
- package/scripts/status_runtime.py +22 -4
- package/scripts/task_context.py +14 -3
- package/scripts/wizard.mjs +1 -0
- package/scripts/workflow_run.py +14 -4
- package/scripts/worktree_paths.py +54 -2
- package/skills/gsd-path/DEFINE.md +34 -5
- package/skills/gsd-path/PLAN.md +14 -3
- package/skills/gsd-path/RESEARCH.md +11 -0
- package/skills/gsd-path/ROADMAP.md +16 -6
- package/skills/gsd-path/SHIP.md +32 -5
- package/skills/gsd-path/SKILL.md +52 -24
- package/skills/gsd-path/references/build-native.md +112 -10
- package/skills/gsd-path/references/build-recovery.md +13 -0
- package/skills/gsd-path/references/coder.md +7 -3
- package/skills/gsd-path/references/dispatch.md +13 -0
- package/skills/gsd-path/references/docs-auditor.md +5 -0
- package/skills/gsd-path/references/operating-rules.md +11 -3
- package/skills/gsd-path/references/planner.md +26 -4
- package/skills/gsd-path/references/reviewer-final.md +20 -2
- package/skills/gsd-path/references/reviewer-wave.md +56 -12
- package/skills/gsd-path/references/reviewer.md +3 -1
- package/skills/gsd-path/references/runtime-selection.md +9 -0
- package/skills/gsd-path/references/spec-probes.md +150 -0
- package/skills/gsd-path/scripts/_common.py +256 -2
- package/skills/gsd-path/scripts/archive_milestone.py +14 -13
- package/skills/gsd-path/scripts/build_recovery.py +155 -58
- package/skills/gsd-path/scripts/build_state.py +274 -8
- package/skills/gsd-path/scripts/check_docs_audit.py +77 -4
- package/skills/gsd-path/scripts/check_handoffs.py +635 -57
- package/skills/gsd-path/scripts/check_task_briefs.py +293 -32
- package/skills/gsd-path/scripts/detect_project.py +260 -20
- package/skills/gsd-path/scripts/discussion_records.py +9 -1
- package/skills/gsd-path/scripts/discussion_validate.py +93 -35
- package/skills/gsd-path/scripts/dispatch_driver.py +549 -53
- package/skills/gsd-path/scripts/git_guard.py +27 -8
- package/skills/gsd-path/scripts/integration.py +244 -65
- package/skills/gsd-path/scripts/isolation.py +539 -70
- package/skills/gsd-path/scripts/loop_run.py +16 -4
- package/skills/gsd-path/scripts/members.py +77 -32
- package/skills/gsd-path/scripts/model_policy.py +5 -1
- package/skills/gsd-path/scripts/path_config.py +3 -3
- package/skills/gsd-path/scripts/pipeline_git.py +131 -42
- package/skills/gsd-path/scripts/pipeline_state.py +139 -39
- package/skills/gsd-path/scripts/promote_lookahead.py +3 -3
- package/skills/gsd-path/scripts/review_findings.py +180 -44
- package/skills/gsd-path/scripts/roadmap.py +9 -1
- package/skills/gsd-path/scripts/state_checkpoint.py +39 -36
- package/skills/gsd-path/scripts/state_promote.py +15 -4
- package/skills/gsd-path/scripts/status_runtime.py +22 -4
- package/skills/gsd-path/scripts/task_context.py +14 -3
- package/skills/gsd-path/scripts/workflow_run.py +14 -4
- package/skills/gsd-path/scripts/worktree_paths.py +54 -2
- package/skills/gsd-path/templates/docs-audit.md +5 -1
- package/skills/gsd-path/templates/final-review.md +11 -2
- package/skills/gsd-path/templates/intent.md +30 -0
- package/skills/gsd-path/templates/patch-findings.md +3 -3
- package/skills/gsd-path/templates/plan.md +17 -2
- package/skills/gsd-path/templates/research-handoff.md +14 -3
- package/skills/gsd-path/templates/state.md +2 -1
- package/skills/gsd-path/templates/task.md +1 -0
- package/skills/gsd-path/templates/wave-review.md +28 -9
- package/skills/gsd-path-build/references/build-native.md +112 -10
- package/skills/gsd-path-build/references/build-recovery.md +13 -0
- package/skills/gsd-path-build/references/coder.md +7 -3
- package/skills/gsd-path-build/references/dispatch.md +13 -0
- package/skills/gsd-path-build/references/operating-rules.md +11 -3
- package/skills/gsd-path-build/references/reviewer-final.md +20 -2
- package/skills/gsd-path-build/references/reviewer-wave.md +56 -12
- package/skills/gsd-path-build/references/reviewer.md +3 -1
- package/skills/gsd-path-build/references/runtime-selection.md +9 -0
- package/skills/gsd-path-build/references/spec-probes.md +150 -0
- package/skills/gsd-path-build/scripts/_common.py +256 -2
- package/skills/gsd-path-build/scripts/archive_milestone.py +14 -13
- package/skills/gsd-path-build/scripts/build_recovery.py +155 -58
- package/skills/gsd-path-build/scripts/build_state.py +274 -8
- package/skills/gsd-path-build/scripts/check_handoffs.py +635 -57
- package/skills/gsd-path-build/scripts/check_task_briefs.py +293 -32
- package/skills/gsd-path-build/scripts/detect_project.py +260 -20
- package/skills/gsd-path-build/scripts/discussion_records.py +9 -1
- package/skills/gsd-path-build/scripts/discussion_validate.py +93 -35
- package/skills/gsd-path-build/scripts/dispatch_driver.py +549 -53
- package/skills/gsd-path-build/scripts/integration.py +244 -65
- package/skills/gsd-path-build/scripts/isolation.py +539 -70
- package/skills/gsd-path-build/scripts/loop_run.py +16 -4
- package/skills/gsd-path-build/scripts/members.py +77 -32
- package/skills/gsd-path-build/scripts/model_policy.py +5 -1
- package/skills/gsd-path-build/scripts/path_config.py +3 -3
- package/skills/gsd-path-build/scripts/pipeline_git.py +131 -42
- package/skills/gsd-path-build/scripts/pipeline_state.py +139 -39
- package/skills/gsd-path-build/scripts/promote_lookahead.py +3 -3
- package/skills/gsd-path-build/scripts/review_findings.py +180 -44
- package/skills/gsd-path-build/scripts/roadmap.py +9 -1
- package/skills/gsd-path-build/scripts/state_checkpoint.py +39 -36
- package/skills/gsd-path-build/scripts/state_promote.py +15 -4
- package/skills/gsd-path-build/scripts/task_context.py +14 -3
- package/skills/gsd-path-build/scripts/workflow_run.py +14 -4
- package/skills/gsd-path-build/scripts/worktree_paths.py +54 -2
- package/skills/gsd-path-build/templates/task.md +1 -0
- package/skills/gsd-path-build/templates/wave-review.md +28 -9
- package/skills/gsd-path-decide/references/dispatch.md +13 -0
- package/skills/gsd-path-decide/references/operating-rules.md +11 -3
- package/skills/gsd-path-decide/references/runtime-selection.md +9 -0
- package/skills/gsd-path-decide/scripts/_common.py +256 -2
- package/skills/gsd-path-decide/scripts/archive_milestone.py +14 -13
- package/skills/gsd-path-decide/scripts/build_recovery.py +155 -58
- package/skills/gsd-path-decide/scripts/check_handoffs.py +635 -57
- package/skills/gsd-path-decide/scripts/check_task_briefs.py +293 -32
- package/skills/gsd-path-decide/scripts/detect_project.py +260 -20
- package/skills/gsd-path-decide/scripts/discussion_records.py +9 -1
- package/skills/gsd-path-decide/scripts/discussion_validate.py +93 -35
- package/skills/gsd-path-decide/scripts/integration.py +244 -65
- package/skills/gsd-path-decide/scripts/isolation.py +539 -70
- package/skills/gsd-path-decide/scripts/loop_run.py +16 -4
- package/skills/gsd-path-decide/scripts/members.py +77 -32
- package/skills/gsd-path-decide/scripts/model_policy.py +5 -1
- package/skills/gsd-path-decide/scripts/path_config.py +3 -3
- package/skills/gsd-path-decide/scripts/pipeline_git.py +131 -42
- package/skills/gsd-path-decide/scripts/pipeline_state.py +139 -39
- package/skills/gsd-path-decide/scripts/promote_lookahead.py +3 -3
- package/skills/gsd-path-decide/scripts/roadmap.py +9 -1
- package/skills/gsd-path-decide/scripts/state_checkpoint.py +39 -36
- package/skills/gsd-path-decide/scripts/state_promote.py +15 -4
- package/skills/gsd-path-decide/scripts/worktree_paths.py +54 -2
- package/skills/gsd-path-define/SKILL.md +34 -5
- package/skills/gsd-path-define/references/build-recovery.md +13 -0
- package/skills/gsd-path-define/references/operating-rules.md +11 -3
- package/skills/gsd-path-define/references/runtime-selection.md +9 -0
- package/skills/gsd-path-define/references/spec-probes.md +150 -0
- package/skills/gsd-path-define/scripts/_common.py +256 -2
- package/skills/gsd-path-define/scripts/archive_milestone.py +14 -13
- package/skills/gsd-path-define/scripts/build_recovery.py +155 -58
- package/skills/gsd-path-define/scripts/check_handoffs.py +635 -57
- package/skills/gsd-path-define/scripts/check_task_briefs.py +293 -32
- package/skills/gsd-path-define/scripts/detect_project.py +260 -20
- package/skills/gsd-path-define/scripts/discussion_records.py +9 -1
- package/skills/gsd-path-define/scripts/discussion_validate.py +93 -35
- package/skills/gsd-path-define/scripts/integration.py +244 -65
- package/skills/gsd-path-define/scripts/isolation.py +539 -70
- package/skills/gsd-path-define/scripts/loop_run.py +16 -4
- package/skills/gsd-path-define/scripts/members.py +77 -32
- package/skills/gsd-path-define/scripts/model_policy.py +5 -1
- package/skills/gsd-path-define/scripts/path_config.py +3 -3
- package/skills/gsd-path-define/scripts/pipeline_git.py +131 -42
- package/skills/gsd-path-define/scripts/pipeline_state.py +139 -39
- package/skills/gsd-path-define/scripts/promote_lookahead.py +3 -3
- package/skills/gsd-path-define/scripts/roadmap.py +9 -1
- package/skills/gsd-path-define/scripts/state_checkpoint.py +39 -36
- package/skills/gsd-path-define/scripts/state_promote.py +15 -4
- package/skills/gsd-path-define/scripts/worktree_paths.py +54 -2
- package/skills/gsd-path-define/templates/intent.md +30 -0
- package/skills/gsd-path-define/templates/state.md +2 -1
- package/skills/gsd-path-discuss/references/operating-rules.md +11 -3
- package/skills/gsd-path-discuss/references/runtime-selection.md +9 -0
- package/skills/gsd-path-discuss/scripts/_common.py +256 -2
- package/skills/gsd-path-discuss/scripts/discussion_records.py +9 -1
- package/skills/gsd-path-discuss/scripts/loop_run.py +16 -4
- package/skills/gsd-path-discuss/scripts/model_policy.py +5 -1
- package/skills/gsd-path-discuss/scripts/path_config.py +3 -3
- package/skills/gsd-path-discuss/scripts/worktree_paths.py +54 -2
- package/skills/gsd-path-docs-audit/references/dispatch.md +13 -0
- package/skills/gsd-path-docs-audit/references/docs-auditor.md +5 -0
- package/skills/gsd-path-docs-audit/references/operating-rules.md +11 -3
- package/skills/gsd-path-docs-audit/references/runtime-selection.md +9 -0
- package/skills/gsd-path-docs-audit/scripts/_common.py +256 -2
- package/skills/gsd-path-docs-audit/scripts/archive_milestone.py +14 -13
- package/skills/gsd-path-docs-audit/scripts/build_recovery.py +155 -58
- package/skills/gsd-path-docs-audit/scripts/check_docs_audit.py +77 -4
- package/skills/gsd-path-docs-audit/scripts/check_handoffs.py +635 -57
- package/skills/gsd-path-docs-audit/scripts/check_task_briefs.py +293 -32
- package/skills/gsd-path-docs-audit/scripts/detect_project.py +260 -20
- package/skills/gsd-path-docs-audit/scripts/discussion_records.py +9 -1
- package/skills/gsd-path-docs-audit/scripts/discussion_validate.py +93 -35
- package/skills/gsd-path-docs-audit/scripts/integration.py +244 -65
- package/skills/gsd-path-docs-audit/scripts/isolation.py +539 -70
- package/skills/gsd-path-docs-audit/scripts/loop_run.py +16 -4
- package/skills/gsd-path-docs-audit/scripts/members.py +77 -32
- package/skills/gsd-path-docs-audit/scripts/model_policy.py +5 -1
- package/skills/gsd-path-docs-audit/scripts/path_config.py +3 -3
- package/skills/gsd-path-docs-audit/scripts/pipeline_git.py +131 -42
- package/skills/gsd-path-docs-audit/scripts/pipeline_state.py +139 -39
- package/skills/gsd-path-docs-audit/scripts/promote_lookahead.py +3 -3
- package/skills/gsd-path-docs-audit/scripts/roadmap.py +9 -1
- package/skills/gsd-path-docs-audit/scripts/state_checkpoint.py +39 -36
- package/skills/gsd-path-docs-audit/scripts/state_promote.py +15 -4
- package/skills/gsd-path-docs-audit/scripts/worktree_paths.py +54 -2
- package/skills/gsd-path-docs-audit/templates/docs-audit.md +5 -1
- package/skills/gsd-path-forensics/references/operating-rules.md +11 -3
- package/skills/gsd-path-forensics/references/runtime-selection.md +9 -0
- package/skills/gsd-path-forensics/scripts/_common.py +256 -2
- package/skills/gsd-path-forensics/scripts/discussion_records.py +9 -1
- package/skills/gsd-path-forensics/scripts/loop_run.py +16 -4
- package/skills/gsd-path-forensics/scripts/model_policy.py +5 -1
- package/skills/gsd-path-forensics/scripts/path_config.py +3 -3
- package/skills/gsd-path-forensics/scripts/pipeline_diagnose.py +17 -2
- package/skills/gsd-path-forensics/scripts/pipeline_undo.py +118 -16
- package/skills/gsd-path-forensics/scripts/worktree_paths.py +54 -2
- package/skills/gsd-path-inspect/references/dispatch.md +13 -0
- package/skills/gsd-path-inspect/references/docs-auditor.md +5 -0
- package/skills/gsd-path-inspect/references/operating-rules.md +11 -3
- package/skills/gsd-path-inspect/references/runtime-selection.md +9 -0
- package/skills/gsd-path-inspect/scripts/_common.py +256 -2
- package/skills/gsd-path-inspect/scripts/archive_milestone.py +14 -13
- package/skills/gsd-path-inspect/scripts/build_recovery.py +155 -58
- package/skills/gsd-path-inspect/scripts/check_docs_audit.py +77 -4
- package/skills/gsd-path-inspect/scripts/check_handoffs.py +635 -57
- package/skills/gsd-path-inspect/scripts/check_task_briefs.py +293 -32
- package/skills/gsd-path-inspect/scripts/detect_project.py +260 -20
- package/skills/gsd-path-inspect/scripts/discussion_records.py +9 -1
- package/skills/gsd-path-inspect/scripts/discussion_validate.py +93 -35
- package/skills/gsd-path-inspect/scripts/integration.py +244 -65
- package/skills/gsd-path-inspect/scripts/isolation.py +539 -70
- package/skills/gsd-path-inspect/scripts/loop_run.py +16 -4
- package/skills/gsd-path-inspect/scripts/members.py +77 -32
- package/skills/gsd-path-inspect/scripts/model_policy.py +5 -1
- package/skills/gsd-path-inspect/scripts/path_config.py +3 -3
- package/skills/gsd-path-inspect/scripts/pipeline_git.py +131 -42
- package/skills/gsd-path-inspect/scripts/pipeline_state.py +139 -39
- package/skills/gsd-path-inspect/scripts/promote_lookahead.py +3 -3
- package/skills/gsd-path-inspect/scripts/roadmap.py +9 -1
- package/skills/gsd-path-inspect/scripts/state_checkpoint.py +39 -36
- package/skills/gsd-path-inspect/scripts/state_promote.py +15 -4
- package/skills/gsd-path-inspect/scripts/workflow_run.py +14 -4
- package/skills/gsd-path-inspect/scripts/worktree_paths.py +54 -2
- package/skills/gsd-path-inspect/templates/docs-audit.md +5 -1
- package/skills/gsd-path-inspect/templates/state.md +2 -1
- package/skills/gsd-path-loop/references/operating-rules.md +11 -3
- package/skills/gsd-path-loop/references/runtime-selection.md +9 -0
- package/skills/gsd-path-loop/scripts/_common.py +256 -2
- package/skills/gsd-path-loop/scripts/discussion_records.py +9 -1
- package/skills/gsd-path-loop/scripts/loop_run.py +16 -4
- package/skills/gsd-path-loop/scripts/model_policy.py +5 -1
- package/skills/gsd-path-loop/scripts/path_config.py +3 -3
- package/skills/gsd-path-loop/scripts/worktree_paths.py +54 -2
- package/skills/gsd-path-migrate/SKILL.md +8 -0
- package/skills/gsd-path-migrate/references/operating-rules.md +11 -3
- package/skills/gsd-path-migrate/references/runtime-selection.md +9 -0
- package/skills/gsd-path-migrate/scripts/_common.py +256 -2
- package/skills/gsd-path-migrate/scripts/discussion_records.py +9 -1
- package/skills/gsd-path-migrate/scripts/loop_run.py +16 -4
- package/skills/gsd-path-migrate/scripts/migrate_core.py +2 -0
- package/skills/gsd-path-migrate/scripts/model_policy.py +5 -1
- package/skills/gsd-path-migrate/scripts/path_config.py +3 -3
- package/skills/gsd-path-plan/SKILL.md +14 -3
- package/skills/gsd-path-plan/references/build-recovery.md +13 -0
- package/skills/gsd-path-plan/references/dispatch.md +13 -0
- package/skills/gsd-path-plan/references/operating-rules.md +11 -3
- package/skills/gsd-path-plan/references/planner.md +26 -4
- package/skills/gsd-path-plan/references/runtime-selection.md +9 -0
- package/skills/gsd-path-plan/references/spec-probes.md +150 -0
- package/skills/gsd-path-plan/scripts/_common.py +256 -2
- package/skills/gsd-path-plan/scripts/archive_milestone.py +14 -13
- package/skills/gsd-path-plan/scripts/build_recovery.py +155 -58
- package/skills/gsd-path-plan/scripts/check_handoffs.py +635 -57
- package/skills/gsd-path-plan/scripts/check_task_briefs.py +293 -32
- package/skills/gsd-path-plan/scripts/detect_project.py +260 -20
- package/skills/gsd-path-plan/scripts/discussion_records.py +9 -1
- package/skills/gsd-path-plan/scripts/discussion_validate.py +93 -35
- package/skills/gsd-path-plan/scripts/integration.py +244 -65
- package/skills/gsd-path-plan/scripts/isolation.py +539 -70
- package/skills/gsd-path-plan/scripts/loop_run.py +16 -4
- package/skills/gsd-path-plan/scripts/members.py +77 -32
- package/skills/gsd-path-plan/scripts/model_policy.py +5 -1
- package/skills/gsd-path-plan/scripts/path_config.py +3 -3
- package/skills/gsd-path-plan/scripts/pipeline_git.py +131 -42
- package/skills/gsd-path-plan/scripts/pipeline_state.py +139 -39
- package/skills/gsd-path-plan/scripts/promote_lookahead.py +3 -3
- package/skills/gsd-path-plan/scripts/roadmap.py +9 -1
- package/skills/gsd-path-plan/scripts/state_checkpoint.py +39 -36
- package/skills/gsd-path-plan/scripts/state_promote.py +15 -4
- package/skills/gsd-path-plan/scripts/workflow_run.py +14 -4
- package/skills/gsd-path-plan/scripts/worktree_paths.py +54 -2
- package/skills/gsd-path-plan/templates/patch-findings.md +3 -3
- package/skills/gsd-path-plan/templates/plan.md +17 -2
- package/skills/gsd-path-plan/templates/task.md +1 -0
- package/skills/gsd-path-research/SKILL.md +11 -0
- package/skills/gsd-path-research/references/dispatch.md +13 -0
- package/skills/gsd-path-research/references/operating-rules.md +11 -3
- package/skills/gsd-path-research/references/runtime-selection.md +9 -0
- package/skills/gsd-path-research/scripts/_common.py +256 -2
- package/skills/gsd-path-research/scripts/archive_milestone.py +14 -13
- package/skills/gsd-path-research/scripts/build_recovery.py +155 -58
- package/skills/gsd-path-research/scripts/check_handoffs.py +635 -57
- package/skills/gsd-path-research/scripts/check_task_briefs.py +293 -32
- package/skills/gsd-path-research/scripts/detect_project.py +260 -20
- package/skills/gsd-path-research/scripts/discussion_records.py +9 -1
- package/skills/gsd-path-research/scripts/discussion_validate.py +93 -35
- package/skills/gsd-path-research/scripts/integration.py +244 -65
- package/skills/gsd-path-research/scripts/isolation.py +539 -70
- package/skills/gsd-path-research/scripts/loop_run.py +16 -4
- package/skills/gsd-path-research/scripts/members.py +77 -32
- package/skills/gsd-path-research/scripts/model_policy.py +5 -1
- package/skills/gsd-path-research/scripts/path_config.py +3 -3
- package/skills/gsd-path-research/scripts/pipeline_git.py +131 -42
- package/skills/gsd-path-research/scripts/pipeline_state.py +139 -39
- package/skills/gsd-path-research/scripts/promote_lookahead.py +3 -3
- package/skills/gsd-path-research/scripts/roadmap.py +9 -1
- package/skills/gsd-path-research/scripts/state_checkpoint.py +39 -36
- package/skills/gsd-path-research/scripts/state_promote.py +15 -4
- package/skills/gsd-path-research/scripts/worktree_paths.py +54 -2
- package/skills/gsd-path-research/templates/research-handoff.md +14 -3
- package/skills/gsd-path-roadmap/SKILL.md +16 -6
- package/skills/gsd-path-roadmap/references/dispatch.md +13 -0
- package/skills/gsd-path-roadmap/references/operating-rules.md +11 -3
- package/skills/gsd-path-roadmap/references/runtime-selection.md +9 -0
- package/skills/gsd-path-roadmap/scripts/_common.py +256 -2
- package/skills/gsd-path-roadmap/scripts/archive_milestone.py +14 -13
- package/skills/gsd-path-roadmap/scripts/build_recovery.py +155 -58
- package/skills/gsd-path-roadmap/scripts/check_handoffs.py +635 -57
- package/skills/gsd-path-roadmap/scripts/check_task_briefs.py +293 -32
- package/skills/gsd-path-roadmap/scripts/detect_project.py +260 -20
- package/skills/gsd-path-roadmap/scripts/discussion_records.py +9 -1
- package/skills/gsd-path-roadmap/scripts/discussion_validate.py +93 -35
- package/skills/gsd-path-roadmap/scripts/integration.py +244 -65
- package/skills/gsd-path-roadmap/scripts/isolation.py +539 -70
- package/skills/gsd-path-roadmap/scripts/loop_run.py +16 -4
- package/skills/gsd-path-roadmap/scripts/members.py +77 -32
- package/skills/gsd-path-roadmap/scripts/model_policy.py +5 -1
- package/skills/gsd-path-roadmap/scripts/path_config.py +3 -3
- package/skills/gsd-path-roadmap/scripts/pipeline_git.py +131 -42
- package/skills/gsd-path-roadmap/scripts/pipeline_state.py +139 -39
- package/skills/gsd-path-roadmap/scripts/promote_lookahead.py +3 -3
- package/skills/gsd-path-roadmap/scripts/roadmap.py +9 -1
- package/skills/gsd-path-roadmap/scripts/state_checkpoint.py +39 -36
- package/skills/gsd-path-roadmap/scripts/state_promote.py +15 -4
- package/skills/gsd-path-roadmap/scripts/worktree_paths.py +54 -2
- package/skills/gsd-path-ship/SKILL.md +32 -5
- package/skills/gsd-path-ship/references/dispatch.md +13 -0
- package/skills/gsd-path-ship/references/operating-rules.md +11 -3
- package/skills/gsd-path-ship/references/reviewer-final.md +20 -2
- package/skills/gsd-path-ship/references/reviewer-wave.md +56 -12
- package/skills/gsd-path-ship/references/reviewer.md +3 -1
- package/skills/gsd-path-ship/references/runtime-selection.md +9 -0
- package/skills/gsd-path-ship/references/spec-probes.md +150 -0
- package/skills/gsd-path-ship/scripts/_common.py +256 -2
- package/skills/gsd-path-ship/scripts/archive_milestone.py +14 -13
- package/skills/gsd-path-ship/scripts/build_recovery.py +155 -58
- package/skills/gsd-path-ship/scripts/build_state.py +274 -8
- package/skills/gsd-path-ship/scripts/check_handoffs.py +635 -57
- package/skills/gsd-path-ship/scripts/check_task_briefs.py +293 -32
- package/skills/gsd-path-ship/scripts/detect_project.py +260 -20
- package/skills/gsd-path-ship/scripts/discussion_records.py +9 -1
- package/skills/gsd-path-ship/scripts/discussion_validate.py +93 -35
- package/skills/gsd-path-ship/scripts/integration.py +244 -65
- package/skills/gsd-path-ship/scripts/isolation.py +539 -70
- package/skills/gsd-path-ship/scripts/loop_run.py +16 -4
- package/skills/gsd-path-ship/scripts/members.py +77 -32
- package/skills/gsd-path-ship/scripts/model_policy.py +5 -1
- package/skills/gsd-path-ship/scripts/path_config.py +3 -3
- package/skills/gsd-path-ship/scripts/pipeline_git.py +131 -42
- package/skills/gsd-path-ship/scripts/pipeline_state.py +139 -39
- package/skills/gsd-path-ship/scripts/promote_lookahead.py +3 -3
- package/skills/gsd-path-ship/scripts/roadmap.py +9 -1
- package/skills/gsd-path-ship/scripts/state_checkpoint.py +39 -36
- package/skills/gsd-path-ship/scripts/state_promote.py +15 -4
- package/skills/gsd-path-ship/scripts/workflow_run.py +14 -4
- package/skills/gsd-path-ship/scripts/worktree_paths.py +54 -2
- package/skills/gsd-path-ship/templates/final-review.md +11 -2
- package/skills/gsd-path-ship/templates/patch-findings.md +3 -3
- package/skills/gsd-path-ship/templates/wave-review.md +28 -9
- package/skills/gsd-path-undo/references/operating-rules.md +11 -3
- package/skills/gsd-path-undo/references/runtime-selection.md +9 -0
- package/skills/gsd-path-undo/scripts/_common.py +256 -2
- package/skills/gsd-path-undo/scripts/discussion_records.py +9 -1
- package/skills/gsd-path-undo/scripts/loop_run.py +16 -4
- package/skills/gsd-path-undo/scripts/model_policy.py +5 -1
- package/skills/gsd-path-undo/scripts/path_config.py +3 -3
- package/skills/gsd-path-undo/scripts/pipeline_undo.py +118 -16
- package/skills/gsd-path-undo/scripts/worktree_paths.py +54 -2
- package/skills/path/DEFINE.md +34 -5
- package/skills/path/PLAN.md +14 -3
- package/skills/path/RESEARCH.md +11 -0
- package/skills/path/ROADMAP.md +16 -6
- package/skills/path/SHIP.md +32 -5
- package/skills/path/SKILL.md +52 -24
- package/skills/path/references/build-native.md +112 -10
- package/skills/path/references/build-recovery.md +13 -0
- package/skills/path/references/coder.md +7 -3
- package/skills/path/references/dispatch.md +13 -0
- package/skills/path/references/docs-auditor.md +5 -0
- package/skills/path/references/operating-rules.md +11 -3
- package/skills/path/references/planner.md +26 -4
- package/skills/path/references/reviewer-final.md +20 -2
- package/skills/path/references/reviewer-wave.md +56 -12
- package/skills/path/references/reviewer.md +3 -1
- package/skills/path/references/runtime-selection.md +9 -0
- package/skills/path/references/spec-probes.md +150 -0
- package/skills/path/scripts/_common.py +256 -2
- package/skills/path/scripts/archive_milestone.py +14 -13
- package/skills/path/scripts/build_recovery.py +155 -58
- package/skills/path/scripts/build_state.py +274 -8
- package/skills/path/scripts/check_docs_audit.py +77 -4
- package/skills/path/scripts/check_handoffs.py +635 -57
- package/skills/path/scripts/check_task_briefs.py +293 -32
- package/skills/path/scripts/detect_project.py +260 -20
- package/skills/path/scripts/discussion_records.py +9 -1
- package/skills/path/scripts/discussion_validate.py +93 -35
- package/skills/path/scripts/dispatch_driver.py +549 -53
- package/skills/path/scripts/git_guard.py +27 -8
- package/skills/path/scripts/integration.py +244 -65
- package/skills/path/scripts/isolation.py +539 -70
- package/skills/path/scripts/loop_run.py +16 -4
- package/skills/path/scripts/members.py +77 -32
- package/skills/path/scripts/model_policy.py +5 -1
- package/skills/path/scripts/path_config.py +3 -3
- package/skills/path/scripts/pipeline_git.py +131 -42
- package/skills/path/scripts/pipeline_state.py +139 -39
- package/skills/path/scripts/promote_lookahead.py +3 -3
- package/skills/path/scripts/review_findings.py +180 -44
- package/skills/path/scripts/roadmap.py +9 -1
- package/skills/path/scripts/state_checkpoint.py +39 -36
- package/skills/path/scripts/state_promote.py +15 -4
- package/skills/path/scripts/status_runtime.py +22 -4
- package/skills/path/scripts/task_context.py +14 -3
- package/skills/path/scripts/workflow_run.py +14 -4
- package/skills/path/scripts/worktree_paths.py +54 -2
- package/skills/path/templates/docs-audit.md +5 -1
- package/skills/path/templates/final-review.md +11 -2
- package/skills/path/templates/intent.md +30 -0
- package/skills/path/templates/patch-findings.md +3 -3
- package/skills/path/templates/plan.md +17 -2
- package/skills/path/templates/research-handoff.md +14 -3
- package/skills/path/templates/state.md +2 -1
- package/skills/path/templates/task.md +1 -0
- package/skills/path/templates/wave-review.md +28 -9
- package/skills/gsd-path/scripts/__pycache__/_common.cpython-312.pyc +0 -0
- package/skills/gsd-path/scripts/__pycache__/check_handoffs.cpython-312.pyc +0 -0
- package/skills/gsd-path-build/scripts/__pycache__/_common.cpython-312.pyc +0 -0
- package/skills/gsd-path-build/scripts/__pycache__/check_handoffs.cpython-312.pyc +0 -0
- package/skills/gsd-path-inspect/scripts/__pycache__/_common.cpython-312.pyc +0 -0
- package/skills/gsd-path-plan/scripts/__pycache__/_common.cpython-312.pyc +0 -0
- package/skills/gsd-path-ship/scripts/__pycache__/_common.cpython-312.pyc +0 -0
package/scripts/pipeline_undo.py
CHANGED
|
@@ -17,10 +17,12 @@ import os
|
|
|
17
17
|
import re
|
|
18
18
|
import shutil
|
|
19
19
|
import sys
|
|
20
|
+
from datetime import date
|
|
20
21
|
|
|
21
22
|
# Runtime helpers must not modify their immutable installation.
|
|
22
23
|
sys.dont_write_bytecode = True
|
|
23
24
|
import tempfile
|
|
25
|
+
import traceback
|
|
24
26
|
from pathlib import Path, PurePosixPath
|
|
25
27
|
from typing import Optional, Sequence
|
|
26
28
|
|
|
@@ -52,7 +54,7 @@ def _load_pipeline_modules():
|
|
|
52
54
|
try:
|
|
53
55
|
from scripts import archive_milestone, isolation, pipeline_git, pipeline_state, state_checkpoint
|
|
54
56
|
return archive_milestone, isolation, pipeline_git, pipeline_state, state_checkpoint
|
|
55
|
-
except
|
|
57
|
+
except ImportError as error: # a foreign `scripts` package
|
|
56
58
|
if error.name not in {
|
|
57
59
|
"scripts",
|
|
58
60
|
"scripts.archive_milestone",
|
|
@@ -62,6 +64,13 @@ def _load_pipeline_modules():
|
|
|
62
64
|
"scripts.state_checkpoint",
|
|
63
65
|
}:
|
|
64
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
|
|
65
74
|
shared = Path(__file__).resolve().parents[2] / "gsd-path" / "scripts"
|
|
66
75
|
sys.path.insert(0, str(shared))
|
|
67
76
|
import archive_milestone
|
|
@@ -106,6 +115,16 @@ KINDS = ("checkpoint", "task", "member-task", "uncommitted-archive", "lookahead"
|
|
|
106
115
|
PROMOTION_PREFIX = "router: promote lookahead milestone "
|
|
107
116
|
ABANDON_PREFIX = "build: abandon milestone "
|
|
108
117
|
INTEGRATE_PREFIX = "integrate: "
|
|
118
|
+
SHIP_LOG_ONLY_LINE = re.compile(
|
|
119
|
+
r"- (?P<date>\d{4}-\d{2}-\d{2}) — ship — (?P<event>[^\r\n]+)\n\Z"
|
|
120
|
+
)
|
|
121
|
+
SHIPMENT_RECORD_LINE = re.compile(
|
|
122
|
+
r"- (?P<date>\d{4}-\d{2}-\d{2}) — shipped — (?P<event>[^\r\n]+)\n\Z"
|
|
123
|
+
)
|
|
124
|
+
SHIPMENT_EVENTS = {
|
|
125
|
+
"archive preflight passed; shipment recorded",
|
|
126
|
+
"archive preflight passed; shipment recorded; program complete",
|
|
127
|
+
}
|
|
109
128
|
|
|
110
129
|
|
|
111
130
|
class UndoError(RuntimeError):
|
|
@@ -375,6 +394,80 @@ def pending_transaction(repo: Path) -> Optional[dict[str, object]]:
|
|
|
375
394
|
raise UndoError(str(error)) from error
|
|
376
395
|
|
|
377
396
|
|
|
397
|
+
def _valid_state_event_line(line: str, phase: str) -> bool:
|
|
398
|
+
pattern = SHIP_LOG_ONLY_LINE if phase == "ship" else SHIPMENT_RECORD_LINE
|
|
399
|
+
match = pattern.fullmatch(line)
|
|
400
|
+
if match is None:
|
|
401
|
+
return False
|
|
402
|
+
try:
|
|
403
|
+
event_date = date.fromisoformat(match.group("date"))
|
|
404
|
+
except ValueError:
|
|
405
|
+
return False
|
|
406
|
+
event = match.group("event")
|
|
407
|
+
return event_date.isoformat() == match.group("date") and event == " ".join(event.split())
|
|
408
|
+
|
|
409
|
+
|
|
410
|
+
def _owned_ship_log_events(
|
|
411
|
+
prepared_state: str,
|
|
412
|
+
current_state: str,
|
|
413
|
+
phase: object,
|
|
414
|
+
status: object,
|
|
415
|
+
) -> Optional[str]:
|
|
416
|
+
"""Return only ship log lines appended around a helper-owned archive event.
|
|
417
|
+
|
|
418
|
+
The prepared text must remain byte-for-byte unchanged. For shipped/done,
|
|
419
|
+
record-shipment changes only phase and status before appending its one
|
|
420
|
+
generated shipment event, so validate that exact prefix and event shape.
|
|
421
|
+
"""
|
|
422
|
+
if (phase, status) == ("ship", "active"):
|
|
423
|
+
if current_state == prepared_state:
|
|
424
|
+
return ""
|
|
425
|
+
if not current_state.startswith(prepared_state):
|
|
426
|
+
return None
|
|
427
|
+
suffix = current_state[len(prepared_state):]
|
|
428
|
+
separator = "" if prepared_state.endswith("\n") else "\n"
|
|
429
|
+
if separator:
|
|
430
|
+
if not suffix.startswith(separator):
|
|
431
|
+
return None
|
|
432
|
+
suffix = suffix[len(separator):]
|
|
433
|
+
lines = suffix.splitlines(keepends=True)
|
|
434
|
+
if not lines or "".join(lines) != suffix:
|
|
435
|
+
return None
|
|
436
|
+
if not all(_valid_state_event_line(line, "ship") for line in lines):
|
|
437
|
+
return None
|
|
438
|
+
return separator + suffix
|
|
439
|
+
|
|
440
|
+
if (phase, status) != ("shipped", "done"):
|
|
441
|
+
return None
|
|
442
|
+
try:
|
|
443
|
+
shipped_prefix = pipeline_state._set_frontmatter(
|
|
444
|
+
prepared_state,
|
|
445
|
+
{"phase": "shipped", "status": "done"},
|
|
446
|
+
)
|
|
447
|
+
except PipelineStateError:
|
|
448
|
+
return None
|
|
449
|
+
if not current_state.startswith(shipped_prefix):
|
|
450
|
+
return None
|
|
451
|
+
suffix = current_state[len(shipped_prefix):]
|
|
452
|
+
separator = "" if shipped_prefix.endswith("\n") else "\n"
|
|
453
|
+
if separator:
|
|
454
|
+
if not suffix.startswith(separator):
|
|
455
|
+
return None
|
|
456
|
+
suffix = suffix[len(separator):]
|
|
457
|
+
lines = suffix.splitlines(keepends=True)
|
|
458
|
+
if not lines or "".join(lines) != suffix:
|
|
459
|
+
return None
|
|
460
|
+
shipment_event = lines[-1]
|
|
461
|
+
if not _valid_state_event_line(shipment_event, "shipped"):
|
|
462
|
+
return None
|
|
463
|
+
if SHIPMENT_RECORD_LINE.fullmatch(shipment_event).group("event") not in SHIPMENT_EVENTS:
|
|
464
|
+
return None
|
|
465
|
+
ship_events = lines[:-1]
|
|
466
|
+
if not all(_valid_state_event_line(line, "ship") for line in ship_events):
|
|
467
|
+
return None
|
|
468
|
+
return separator + "".join(ship_events) if ship_events else ""
|
|
469
|
+
|
|
470
|
+
|
|
378
471
|
def _archive_metadata_error(
|
|
379
472
|
repo: Path,
|
|
380
473
|
state_fields: dict[str, object],
|
|
@@ -420,8 +513,13 @@ def _archive_metadata_error(
|
|
|
420
513
|
if final_review.exists() or gap_reviews:
|
|
421
514
|
reviewed_head, _ = archive_milestone.parse_final_review(archive_path)
|
|
422
515
|
archive_milestone.validate_gap_reviews(archive_path, reviewed_head)
|
|
423
|
-
if
|
|
424
|
-
|
|
516
|
+
if not archive_milestone.reviewed_head_covers(
|
|
517
|
+
repo, reviewed_head, _optional_rev(repo, "HEAD")
|
|
518
|
+
):
|
|
519
|
+
return (
|
|
520
|
+
"archive undo final review does not match current HEAD; "
|
|
521
|
+
f"{archive_milestone._common.RUNTIME_PIN_ALLOWANCE}"
|
|
522
|
+
)
|
|
425
523
|
expected_inventory.add("review/FINAL.md")
|
|
426
524
|
expected_inventory.update(
|
|
427
525
|
path.relative_to(archive_path).as_posix() for path in gap_reviews
|
|
@@ -476,10 +574,14 @@ def _archive_metadata_error(
|
|
|
476
574
|
"archive",
|
|
477
575
|
configured,
|
|
478
576
|
)
|
|
479
|
-
current_state = (repo / ".project" / "STATE.md").
|
|
577
|
+
current_state = (repo / ".project" / "STATE.md").read_bytes().decode("utf-8")
|
|
480
578
|
phase = state_fields.get("phase")
|
|
481
579
|
status = state_fields.get("status")
|
|
580
|
+
ship_events = _owned_ship_log_events(prepared_state, current_state, phase, status)
|
|
482
581
|
if (phase, status) == ("ship", "active"):
|
|
582
|
+
if ship_events is None:
|
|
583
|
+
return "archive undo found unowned STATE.md changes"
|
|
584
|
+
prepared_state += ship_events
|
|
483
585
|
if current_state != prepared_state:
|
|
484
586
|
return "archive undo found unowned STATE.md changes"
|
|
485
587
|
if ".project/ROADMAP.md" in dirty:
|
|
@@ -487,14 +589,13 @@ def _archive_metadata_error(
|
|
|
487
589
|
return None
|
|
488
590
|
if (phase, status) != ("shipped", "done"):
|
|
489
591
|
return "archive undo requires ship/active or shipped/done"
|
|
592
|
+
if ship_events is None:
|
|
593
|
+
return "archive undo found unowned shipment STATE.md changes"
|
|
594
|
+
prepared_state += ship_events
|
|
490
595
|
|
|
491
596
|
prepared = _state_from_text(prepared_state, "prepared archive STATE.md")
|
|
492
|
-
events = (
|
|
493
|
-
"archive preflight passed; shipment recorded",
|
|
494
|
-
"archive preflight passed; shipment recorded; program complete",
|
|
495
|
-
)
|
|
496
597
|
rendered_states = []
|
|
497
|
-
for event in
|
|
598
|
+
for event in SHIPMENT_EVENTS:
|
|
498
599
|
try:
|
|
499
600
|
_, _, rendered = _render_transition(
|
|
500
601
|
prepared,
|
|
@@ -679,14 +780,15 @@ def _member_undo_parent(repo: Path, member: str, landing: str) -> str:
|
|
|
679
780
|
bound = entry["branch"]
|
|
680
781
|
if _run_git(checkout, "rev-parse", f"refs/heads/{bound}").stdout.strip() != landing:
|
|
681
782
|
raise UndoError(f"member {member} {bound} moved past this landing")
|
|
682
|
-
_require_member_unpublished(checkout, member, landing, bound)
|
|
783
|
+
_require_member_unpublished(repo, checkout, member, landing, bound)
|
|
683
784
|
return _run_git(checkout, "rev-parse", f"{landing}^").stdout.strip()
|
|
684
785
|
|
|
685
786
|
|
|
686
|
-
def _require_member_unpublished(checkout: Path, member: str, landing: str, bound: str) -> None:
|
|
687
|
-
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}"
|
|
688
790
|
if _run_git(checkout, "merge-base", "--is-ancestor", landing, ref, check=False).returncode == 0:
|
|
689
|
-
raise UndoError(f"member {member} landing is already on {
|
|
791
|
+
raise UndoError(f"member {member} landing is already on origin/{branch}")
|
|
690
792
|
|
|
691
793
|
|
|
692
794
|
def _require_no_member_landing_journal(repo: Path) -> None:
|
|
@@ -701,7 +803,7 @@ def _undo_member_landing(repo: Path, transaction: dict[str, object]) -> None:
|
|
|
701
803
|
checkout, _, entry = isolation._member_context(repo, member)
|
|
702
804
|
tip = _run_git(checkout, "rev-parse", f"refs/heads/{entry['branch']}").stdout.strip()
|
|
703
805
|
if tip == transaction["member_parent"]:
|
|
704
|
-
_require_member_unpublished(checkout, member, str(transaction["landing"]), entry["branch"])
|
|
806
|
+
_require_member_unpublished(repo, checkout, member, str(transaction["landing"]), entry["branch"])
|
|
705
807
|
return
|
|
706
808
|
if tip != transaction["landing"]:
|
|
707
809
|
raise UndoError(f"member {member} bound branch moved during undo")
|
|
@@ -768,7 +870,7 @@ def classify_undo(repo: Path) -> dict[str, object]:
|
|
|
768
870
|
[f"an in-progress helper journal exists ({names}); resume that transaction first"]
|
|
769
871
|
)
|
|
770
872
|
if git.get("ancestor_of_origin_main"):
|
|
771
|
-
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"])
|
|
772
874
|
|
|
773
875
|
product = _product_dirty(dirty)
|
|
774
876
|
if product:
|
|
@@ -988,7 +1090,7 @@ def _require_unpublished_reset(repo: Path, expected_branch: str) -> None:
|
|
|
988
1090
|
if git.get("published"):
|
|
989
1091
|
raise UndoError("HEAD became published on its recorded remote branch")
|
|
990
1092
|
if git.get("ancestor_of_origin_main"):
|
|
991
|
-
raise UndoError("HEAD became published through origin
|
|
1093
|
+
raise UndoError("HEAD became published through the origin default branch")
|
|
992
1094
|
|
|
993
1095
|
|
|
994
1096
|
def _remove_untracked_tree(repo: Path, relative: str) -> None:
|
|
@@ -198,10 +198,10 @@ def select_fetched_base(
|
|
|
198
198
|
use_lookahead: bool,
|
|
199
199
|
) -> dict:
|
|
200
200
|
root = repo.resolve()
|
|
201
|
-
if not
|
|
201
|
+
if not remote_default.startswith("origin/") or not _common.valid_default_branch(
|
|
202
|
+
remote_default.removeprefix("origin/")
|
|
203
|
+
):
|
|
202
204
|
raise LookaheadError("remote default must be an origin branch")
|
|
203
|
-
if ".." in remote_default or "//" in remote_default:
|
|
204
|
-
raise LookaheadError("remote default is invalid")
|
|
205
205
|
resolved = subprocess.run(
|
|
206
206
|
["git", "-C", str(root), "rev-parse", "--verify", f"{remote_default}^{{commit}}"],
|
|
207
207
|
capture_output=True,
|
|
@@ -8,20 +8,29 @@ canonical review files, and every skeptic file for this wave. It groups
|
|
|
8
8
|
blocking findings by canonical criterion locator (`t001_ac2`, `sc3`,
|
|
9
9
|
`t001_files`, `t001_interface_contract`), collapses true duplicate
|
|
10
10
|
observations, selects the groups that need a skeptic, carries refutations
|
|
11
|
-
forward across cycles, batches fix-task groups by disjoint file
|
|
12
|
-
structural blockers out of skeptic accounting, and reports
|
|
13
|
-
cap is reached. It never decides the wave verdict.
|
|
11
|
+
forward across cycles, batches fix-task groups by repo and disjoint file
|
|
12
|
+
scope, keeps structural blockers out of skeptic accounting, and reports
|
|
13
|
+
whether the cycle cap is reached. It never decides the wave verdict.
|
|
14
14
|
"""
|
|
15
15
|
|
|
16
16
|
from __future__ import annotations
|
|
17
17
|
|
|
18
18
|
import argparse
|
|
19
|
+
import functools
|
|
19
20
|
import json
|
|
20
21
|
import re
|
|
21
22
|
import sys
|
|
23
|
+
|
|
24
|
+
# Runtime helpers must not modify their immutable installation.
|
|
25
|
+
sys.dont_write_bytecode = True
|
|
22
26
|
from pathlib import Path
|
|
23
27
|
from typing import Dict, List, Optional, Sequence
|
|
24
28
|
|
|
29
|
+
try:
|
|
30
|
+
from . import _common
|
|
31
|
+
except ImportError: # installed standalone helper
|
|
32
|
+
import _common
|
|
33
|
+
|
|
25
34
|
DEFAULT_PROJECT_DIR = ".project"
|
|
26
35
|
DEFAULT_MAX_REVIEW_CYCLES = 3
|
|
27
36
|
CONFIG_KEYS = ("max_review_cycles", "wave_budget", "review_panel", "finding_skeptics")
|
|
@@ -143,15 +152,25 @@ def _frontmatter_list(frontmatter: str, key: str) -> List[str]:
|
|
|
143
152
|
|
|
144
153
|
|
|
145
154
|
def _numbered_items(section: Optional[str]) -> List[str]:
|
|
146
|
-
|
|
155
|
+
"""Numbered criteria; each also carries its text without sub-bullet markers (issue #392)."""
|
|
147
156
|
if section is None:
|
|
148
|
-
return
|
|
157
|
+
return []
|
|
158
|
+
bare = _numbered_texts(_common.without_sub_bullet_markers(section))
|
|
159
|
+
return [_common.Criterion(text, other) for text, other in zip(_numbered_texts(section), bare)]
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def _numbered_texts(section: str) -> List[str]:
|
|
163
|
+
items: List[str] = []
|
|
164
|
+
open_item = False
|
|
149
165
|
for line in _strip_comments(section).splitlines():
|
|
150
166
|
match = NUMBERED_ITEM.match(line.strip())
|
|
151
167
|
if match:
|
|
152
168
|
items.append(_normalize(match.group(2)))
|
|
153
|
-
|
|
169
|
+
open_item = True
|
|
170
|
+
elif open_item and line[:1].isspace() and line.strip():
|
|
154
171
|
items[-1] = _normalize(items[-1] + " " + line.strip())
|
|
172
|
+
elif line.strip():
|
|
173
|
+
open_item = False
|
|
155
174
|
return items
|
|
156
175
|
|
|
157
176
|
|
|
@@ -183,9 +202,11 @@ def load_wave_tasks(project: Path, wave: int) -> Dict[str, dict]:
|
|
|
183
202
|
if not task_wave or int(task_wave.group(1)) != wave:
|
|
184
203
|
continue
|
|
185
204
|
body = parts[2]
|
|
205
|
+
member = re.search(r"(?m)^repo:[ \t]*(?P<name>[^\s#]+)", frontmatter)
|
|
186
206
|
tasks[named.group("id")] = {
|
|
187
207
|
"id": named.group("id"),
|
|
188
208
|
"path": str(path),
|
|
209
|
+
"repo": member.group("name").strip("\"'") if member else None, # None is the coordinator
|
|
189
210
|
"files": _frontmatter_list(frontmatter, "files"),
|
|
190
211
|
"acceptance": _numbered_items(_section(body, "Acceptance criteria")),
|
|
191
212
|
"interface": [
|
|
@@ -270,17 +291,20 @@ def parse_lens(text: str, lens: str, tasks: Dict[str, dict]) -> dict:
|
|
|
270
291
|
continue
|
|
271
292
|
zones[zone].append(line)
|
|
272
293
|
for observation in _fail_items(zones["criteria"]):
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
elif criterion in task["interface"]:
|
|
278
|
-
locator = f"{task_id.lower()}_interface_contract"
|
|
279
|
-
if locator is None:
|
|
294
|
+
# Issue #392: match as the wave gate does, then report the task's own text.
|
|
295
|
+
criteria = task["acceptance"] + task["interface"]
|
|
296
|
+
quoted = _common.quoted_criterion(observation, criteria)
|
|
297
|
+
if quoted is None:
|
|
280
298
|
problems.append(
|
|
281
|
-
f"{lens} lens {task_id} finding names no task criterion:
|
|
299
|
+
f"{lens} lens {task_id} finding names no task criterion: "
|
|
300
|
+
f"{_criterion_of(observation)}"
|
|
282
301
|
)
|
|
283
302
|
continue
|
|
303
|
+
criterion = criteria[quoted[0]]
|
|
304
|
+
if quoted[0] < len(task["acceptance"]):
|
|
305
|
+
locator = f"{task_id.lower()}_ac{quoted[0] + 1}"
|
|
306
|
+
else:
|
|
307
|
+
locator = f"{task_id.lower()}_interface_contract"
|
|
284
308
|
findings.append(
|
|
285
309
|
{"locator": locator, "criterion": criterion, "task": task_id,
|
|
286
310
|
"lens": lens, "observation": observation}
|
|
@@ -433,21 +457,27 @@ def _earlier_lens_locators(project: Path, wave: int, cycle: int, depth: str, tas
|
|
|
433
457
|
|
|
434
458
|
|
|
435
459
|
def _fix_batches(groups: Sequence[dict], tasks: Dict[str, dict]) -> List[dict]:
|
|
436
|
-
"""One batch per disjoint file scope: groups whose task files overlap share a batch.
|
|
460
|
+
"""One batch per repo and disjoint file scope: groups whose task files overlap in one repo share a batch.
|
|
461
|
+
|
|
462
|
+
A member batch names its `repo`; a coordinator batch has no such key.
|
|
463
|
+
"""
|
|
437
464
|
|
|
438
465
|
batches: List[dict] = []
|
|
439
466
|
for group in groups:
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
467
|
+
owners = [tasks.get(task_id, {}) for task_id in group["tasks"]]
|
|
468
|
+
for repo in sorted({task.get("repo") or "" for task in owners}) or [""]:
|
|
469
|
+
files = sorted({path for task in owners if (task.get("repo") or "") == repo
|
|
470
|
+
for path in task.get("files", [])})
|
|
471
|
+
merged = {"locators": [group["locator"]], "files": files, **({"repo": repo} if repo else {})}
|
|
472
|
+
keep: List[dict] = []
|
|
473
|
+
for batch in batches:
|
|
474
|
+
if batch.get("repo") == merged.get("repo") and _common.overlapping_paths(batch["files"], files):
|
|
475
|
+
merged["locators"] = batch["locators"] + merged["locators"]
|
|
476
|
+
merged["files"] = sorted(set(batch["files"]) | set(merged["files"]))
|
|
477
|
+
else:
|
|
478
|
+
keep.append(batch)
|
|
479
|
+
keep.append(merged)
|
|
480
|
+
batches = keep
|
|
451
481
|
return batches
|
|
452
482
|
|
|
453
483
|
|
|
@@ -604,7 +634,7 @@ def repair_evidence(repo: Path, project_dir: str, wave: int, cycle: int, task_id
|
|
|
604
634
|
)["tasks"][0]
|
|
605
635
|
if proof["verdict"] != "recovered":
|
|
606
636
|
raise ReviewFindingsError("repair requires a proven landing")
|
|
607
|
-
base
|
|
637
|
+
base = proof["base"] # in the coordinator, where the source review is
|
|
608
638
|
findings = _section(text, "Review findings") or ""
|
|
609
639
|
headings = list(re.finditer(r"(?m)^### ([a-z0-9_]+)\s*$", findings))
|
|
610
640
|
locators = [heading.group(1) for heading in headings]
|
|
@@ -613,10 +643,28 @@ def repair_evidence(repo: Path, project_dir: str, wave: int, cycle: int, task_id
|
|
|
613
643
|
source = compute(repo, project_dir, wave, cycle)
|
|
614
644
|
if source["structural_blockers"]:
|
|
615
645
|
raise ReviewFindingsError("source findings have structural blockers")
|
|
646
|
+
member = fields.get("repo") # a finding in two repos has one batch per repo with the same locators
|
|
616
647
|
batches = [batch for batch in source["fix_batches"]
|
|
617
|
-
if set(batch["locators"]) == set(locators)]
|
|
648
|
+
if batch.get("repo") == member and set(batch["locators"]) == set(locators)]
|
|
618
649
|
if len(batches) != 1:
|
|
619
650
|
raise ReviewFindingsError("repair must carry one eligible source finding batch")
|
|
651
|
+
# A member task's product is in the member repository: its commits, its bound branch, its Verify rows.
|
|
652
|
+
where, tip = repo, [] # the coordinator product is judged in the working tree
|
|
653
|
+
if member:
|
|
654
|
+
where, _, lock = isolation._member_context(repo, member)
|
|
655
|
+
tip = [f"refs/heads/{lock['branch']}"]
|
|
656
|
+
|
|
657
|
+
def entry(landing: dict, path: Path, paths: List[str]) -> dict:
|
|
658
|
+
"""A proven landing as the receipt shows it; a member landing adds its commits in the member."""
|
|
659
|
+
item = {"task": landing["task_id"], "base": landing["base"], "commit": landing["commit"], "files": paths}
|
|
660
|
+
if member:
|
|
661
|
+
item.update(member_base=isolation.task_frontmatter(_read(path, "task"))[0]["member_base"],
|
|
662
|
+
member_commit=landing["landing"])
|
|
663
|
+
return item
|
|
664
|
+
|
|
665
|
+
def at(item: dict, point: str = "commit") -> str:
|
|
666
|
+
return item.get(f"member_{point}", item[point])
|
|
667
|
+
|
|
620
668
|
groups = [group for group in source["groups"] if group["locator"] in locators]
|
|
621
669
|
for index, heading in enumerate(headings):
|
|
622
670
|
end = headings[index + 1].start() if index + 1 < len(headings) else len(findings)
|
|
@@ -627,22 +675,101 @@ def repair_evidence(repo: Path, project_dir: str, wave: int, cycle: int, task_id
|
|
|
627
675
|
raise ReviewFindingsError("repair does not carry the exact criterion and observations")
|
|
628
676
|
frontmatter = text.split("---", 2)[1]
|
|
629
677
|
files = _frontmatter_list(frontmatter, "files")
|
|
630
|
-
|
|
678
|
+
sources = load_wave_tasks(project, wave)
|
|
679
|
+
originals = sorted({task for group in groups for task in group["tasks"] if sources[task]["repo"] == member})
|
|
680
|
+
# A repair names the files it needs. They can be fewer than the batch files, and a plan repair
|
|
681
|
+
# can add a fix site that the source tasks did not declare.
|
|
631
682
|
if (task_id in originals or not set(originals) <= set(_frontmatter_list(frontmatter, "deps"))
|
|
632
|
-
or
|
|
683
|
+
or not files):
|
|
633
684
|
raise ReviewFindingsError("repair dependencies/files do not match its source batch")
|
|
634
|
-
|
|
685
|
+
repair = {"task_file": repair_path.relative_to(repo).as_posix(), **entry(proof, repair_path, files),
|
|
686
|
+
**({"member": member} if member else {})}
|
|
687
|
+
|
|
688
|
+
def differ(start: str, end: List[str], paths: List[str]) -> bool:
|
|
689
|
+
return bool(isolation.run_git(where, "diff", "--quiet", start, *end, "--", *paths).returncode)
|
|
690
|
+
|
|
691
|
+
def before(start: str, end: str) -> bool:
|
|
692
|
+
return not isolation.run_git(where, "merge-base", "--is-ancestor", start, end).returncode
|
|
693
|
+
|
|
694
|
+
@functools.lru_cache(maxsize=None)
|
|
695
|
+
def linked() -> tuple:
|
|
696
|
+
"""Landed repairs of the same findings that `deps` join to this repair, in landing order.
|
|
697
|
+
|
|
698
|
+
A linked task is done, is in the same repo, carries a finding of this repair in its
|
|
699
|
+
`## Review findings`, and depends on this repair or on a linked task, or the reverse.
|
|
700
|
+
"""
|
|
701
|
+
tasks, carried = {task_id: (repair_path, fields)}, {}
|
|
702
|
+
for path in (project / "tasks").glob("T*-*.md"):
|
|
703
|
+
other = _read(path, "task")
|
|
704
|
+
other_fields = isolation.task_frontmatter(other)[0] or {}
|
|
705
|
+
tasks[str(other_fields.get("id"))] = (path, other_fields)
|
|
706
|
+
blocks = re.split(r"(?m)^### ([a-z0-9_]+)\s*$", _section(other, "Review findings") or "")
|
|
707
|
+
carried[str(other_fields.get("id"))] = {
|
|
708
|
+
(locator, _normalize(line)) for locator, block in zip(blocks[1::2], blocks[2::2])
|
|
709
|
+
for line in re.findall(r"(?m)^Criterion:(.*)$", block)}
|
|
710
|
+
|
|
711
|
+
def traced(finding: tuple, seen: frozenset = frozenset()) -> set:
|
|
712
|
+
"""The finding as it started: its locator and criterion in the review of the source tasks.
|
|
713
|
+
|
|
714
|
+
A finding on an acceptance criterion of a fix task (`fix-tasks` copies the carried criteria
|
|
715
|
+
there) is the finding with the same criterion that this fix task carries. Two findings with
|
|
716
|
+
the same criterion text and other locators stay two findings.
|
|
717
|
+
"""
|
|
718
|
+
owner = re.fullmatch(r"t(\d{3})_ac\d+", finding[0])
|
|
719
|
+
sources = () if not owner else {
|
|
720
|
+
item for item in carried.get(f"T{owner.group(1)}", ()) if item[1] == finding[1]} - seen
|
|
721
|
+
return {origin for item in sources for origin in traced(item, seen | {finding})} or {finding}
|
|
722
|
+
|
|
723
|
+
def origins(other: str) -> set:
|
|
724
|
+
return {origin for finding in carried.get(other, ()) for origin in traced(finding)}
|
|
725
|
+
|
|
726
|
+
repairs = {other: task for other, task in tasks.items() if other == task_id or (
|
|
727
|
+
task[1].get("status") == "done" and task[1].get("repo") == member
|
|
728
|
+
and origins(task_id) & origins(other))}
|
|
729
|
+
joined, more = set(), {task_id}
|
|
730
|
+
while more:
|
|
731
|
+
joined |= more
|
|
732
|
+
more = {other for other, (_, other_fields) in repairs.items() if other not in joined and (
|
|
733
|
+
joined & set(other_fields.get("deps") or [])
|
|
734
|
+
or any(other in (repairs[item][1].get("deps") or []) for item in joined))}
|
|
735
|
+
others = sorted(joined - {task_id})
|
|
736
|
+
proofs = isolation.verify_landed_task_files(
|
|
737
|
+
repo, [repairs[other][0] for other in others], f"{project_dir}/tasks", head)["tasks"] if others else []
|
|
738
|
+
links = []
|
|
739
|
+
for landing in proofs:
|
|
740
|
+
path, other_fields = repairs[landing["task_id"]]
|
|
741
|
+
if landing["verdict"] == "recovered":
|
|
742
|
+
links.append({"task_file": path.relative_to(repo).as_posix(),
|
|
743
|
+
**entry(landing, path, list(other_fields.get("files") or []))})
|
|
744
|
+
return tuple(sorted(links, key=lambda link: int(isolation.git_output(where, "rev-list", "--count", at(link)))))
|
|
745
|
+
|
|
746
|
+
def linked_changes(start: str, end: List[str], paths: List[str]) -> Optional[List[dict]]:
|
|
747
|
+
"""The linked repairs whose landings are every change to `paths` from `start` to `end`, else None."""
|
|
748
|
+
if not differ(start, end, paths):
|
|
749
|
+
return []
|
|
750
|
+
links = [link for link in linked()
|
|
751
|
+
if before(start, f"{at(link)}^") and (not end or before(at(link), end[0]))]
|
|
752
|
+
for link in links:
|
|
753
|
+
if differ(start, [f"{at(link)}^"], paths):
|
|
754
|
+
return None
|
|
755
|
+
start = at(link)
|
|
756
|
+
return None if differ(start, end, paths) else links
|
|
757
|
+
|
|
635
758
|
original_paths = [Path(sources[task]["path"]) for task in originals]
|
|
636
759
|
original_proofs = isolation.verify_landed_task_files(
|
|
637
760
|
repo, original_paths, f"{project_dir}/tasks", head
|
|
638
761
|
)["tasks"]
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
762
|
+
landed, earlier = [], set()
|
|
763
|
+
for landing in original_proofs:
|
|
764
|
+
links = None
|
|
765
|
+
if landing["verdict"] == "recovered":
|
|
766
|
+
original = entry(landing, Path(sources[landing["task_id"]]["path"]), sources[landing["task_id"]]["files"])
|
|
767
|
+
if before(at(original), at(repair, "base")):
|
|
768
|
+
links = linked_changes(at(original), [at(repair, "base")], original["files"])
|
|
769
|
+
if links is None:
|
|
645
770
|
raise ReviewFindingsError("repair base contains unlinked changes to original product")
|
|
771
|
+
landed.append(original)
|
|
772
|
+
earlier.update(link["task"] for link in links)
|
|
646
773
|
source_paths = list(review_paths(project, wave, cycle, source["depth"]).values())
|
|
647
774
|
source_paths.extend(Path(group["skeptic"]["path"]) for group in groups if group["skeptic"])
|
|
648
775
|
for path in set(source_paths):
|
|
@@ -650,19 +777,28 @@ def repair_evidence(repo: Path, project_dir: str, wave: int, cycle: int, task_id
|
|
|
650
777
|
at_base = isolation.run_git(repo, "show", f"{base}:{relative}")
|
|
651
778
|
if at_base.returncode or at_base.stdout != _read(path, "source review"):
|
|
652
779
|
raise ReviewFindingsError("source review must be unchanged and precede the repair")
|
|
653
|
-
|
|
780
|
+
# A later linked repair supersedes this one: the last one is the product at the tip, and its
|
|
781
|
+
# landing and its Verify are the evidence. The reviewer applies every patch of the chain, so the
|
|
782
|
+
# files of every later linked repair must also change only through the landings of the chain.
|
|
783
|
+
later = linked_changes(at(repair), tip, files)
|
|
784
|
+
chain_files = sorted({*files, *(path for link in later or [] for path in link["files"])})
|
|
785
|
+
last = later[-1] if later else repair
|
|
786
|
+
verification = build_state.verify_lookup(
|
|
787
|
+
str(repo), _common.task_verify_command(_read(repo / last["task_file"], "repair task")), at(last), member)
|
|
654
788
|
if not verification["reuse"]:
|
|
655
789
|
raise ReviewFindingsError("repair has no reusable passing Verify at its landing")
|
|
656
|
-
if
|
|
790
|
+
if later is None or (later and linked_changes(at(repair), tip, chain_files) is None):
|
|
657
791
|
raise ReviewFindingsError("product changed after the repair landing")
|
|
792
|
+
# A parallel landing can have a parent that is newer than the repair base. The reviewer builds the
|
|
793
|
+
# product from the base, so no other landing between the two can change a file of the chain.
|
|
794
|
+
if differ(at(repair, "base"), [f"{at(repair)}^"], chain_files):
|
|
795
|
+
raise ReviewFindingsError("product changed between the repair base and the repair landing")
|
|
658
796
|
return {
|
|
659
797
|
"status": "ok", "command": "repair-evidence", "head": head,
|
|
660
798
|
"source": {"wave": wave, "cycle": cycle}, "groups": groups,
|
|
661
|
-
"originals":
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
"repair": {"task": task_id, "task_file": repair_path.relative_to(repo).as_posix(),
|
|
665
|
-
"base": base, "commit": commit, "files": files},
|
|
799
|
+
"originals": landed, "repair": repair,
|
|
800
|
+
**({"earlier_repairs": [link for link in linked() if link["task"] in earlier]} if earlier else {}),
|
|
801
|
+
**({"later_repairs": later, "chain_files": chain_files} if later else {}),
|
|
666
802
|
"verification": verification,
|
|
667
803
|
"verdict": "evidence-ready; reviewer judges the original unchanged criteria",
|
|
668
804
|
}
|
package/scripts/roadmap.py
CHANGED
|
@@ -252,7 +252,12 @@ def activate_milestone(text: str, milestone: str) -> str:
|
|
|
252
252
|
return "".join(lines)
|
|
253
253
|
|
|
254
254
|
|
|
255
|
-
def
|
|
255
|
+
def _entry_identity(block: dict) -> tuple:
|
|
256
|
+
"""Heading, Status, Depends on, Archive and Integrated of one entry."""
|
|
257
|
+
return block["id"], block["title"], {name: value for name, (_, value) in block["fields"].items()}
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def reslice(before: str, candidate: str, phase: str, selected: str, amend_active: bool = False) -> str:
|
|
256
261
|
old_blocks = roadmap_blocks(before)
|
|
257
262
|
before_lines = before.splitlines(keepends=True)
|
|
258
263
|
candidate_lines = candidate.splitlines(keepends=True)
|
|
@@ -264,6 +269,9 @@ def reslice(before: str, candidate: str, phase: str, selected: str) -> str:
|
|
|
264
269
|
new = milestone_block(candidate, slug)
|
|
265
270
|
if status == "shipped":
|
|
266
271
|
changed = roadmap_contract(before, slug) != roadmap_contract(candidate, slug)
|
|
272
|
+
elif status == "active" and amend_active:
|
|
273
|
+
# Intent is not approved: the scope may change, the identity may not.
|
|
274
|
+
changed = _entry_identity(block) != _entry_identity(new)
|
|
267
275
|
else:
|
|
268
276
|
changed = before_lines[block["start"]:block["end"]] != candidate_lines[new["start"]:new["end"]]
|
|
269
277
|
if changed:
|
package/scripts/runtime_store.py
CHANGED
|
@@ -24,9 +24,19 @@ RUNTIME_FILES = (
|
|
|
24
24
|
"state_checkpoint.py", "state_promote.py", "discussion_validate.py", "integration.py",
|
|
25
25
|
"build_state.py", "lean_verification.py", "check_docs_audit.py",
|
|
26
26
|
"path_config.py", "model_policy.py", "loop_run.py", "members.py",
|
|
27
|
+
"review_findings.py", "task_context.py",
|
|
27
28
|
)
|
|
28
29
|
GUARDS = ("guard_hook.py", "git_guard.py")
|
|
29
30
|
|
|
31
|
+
# Optional owner-recorded provenance on .gsd-path/runtime.json declarations,
|
|
32
|
+
# e.g. {"source": "<npm version or package id>", "patch_ref": "<commit sha
|
|
33
|
+
# or patch list>", "note": "<owner label>"}. Provenance is display/audit
|
|
34
|
+
# metadata ONLY: it never joins the hashed manifest input (manifest_digest
|
|
35
|
+
# covers exactly {"version", "files"}), never acts as an alternate accepted
|
|
36
|
+
# digest, and never bypasses or weakens validate_runtime. A declaration
|
|
37
|
+
# without provenance is stock and stays exactly as valid as one with it.
|
|
38
|
+
PROVENANCE_KEYS = ("source", "patch_ref", "note")
|
|
39
|
+
|
|
30
40
|
# Access denied and sharing violation: on Windows a scanner or indexer that
|
|
31
41
|
# briefly opens a freshly written file blocks renaming the directory holding it.
|
|
32
42
|
_WINDOWS_BUSY_ERRORS = (5, 32)
|
|
@@ -220,7 +230,7 @@ def recover_migration(project):
|
|
|
220
230
|
journal.unlink()
|
|
221
231
|
|
|
222
232
|
|
|
223
|
-
def operate(source, project, action, *, dry_run=False):
|
|
233
|
+
def operate(source, project, action, *, dry_run=False, provenance=None):
|
|
224
234
|
if project.is_symlink() or (project / ".gsd-path").is_symlink():
|
|
225
235
|
raise ValueError("unsafe project runtime directory")
|
|
226
236
|
if action == "restore":
|
|
@@ -228,8 +238,17 @@ def operate(source, project, action, *, dry_run=False):
|
|
|
228
238
|
publish(source, expected=pin, dry_run=dry_run, repair=True)
|
|
229
239
|
return pin
|
|
230
240
|
if action == "upgrade":
|
|
231
|
-
status_runtime.declaration(project)
|
|
241
|
+
previous = status_runtime.declaration(project)
|
|
232
242
|
pin = publish(source, dry_run=dry_run)
|
|
243
|
+
# Explicit provenance replaces the recorded object. Without new flags,
|
|
244
|
+
# an existing entry carries forward only while the pinned digest is
|
|
245
|
+
# unchanged — a different source drops stale hotfix metadata instead
|
|
246
|
+
# of stamping it onto bytes it no longer describes. Metadata only —
|
|
247
|
+
# it never changes the pinned digest.
|
|
248
|
+
if provenance is None and previous.get("digest") == pin["digest"]:
|
|
249
|
+
provenance = previous.get("provenance")
|
|
250
|
+
if provenance:
|
|
251
|
+
pin = {**pin, "provenance": provenance}
|
|
233
252
|
if not dry_run:
|
|
234
253
|
launcher = project / ".gsd-path/status_runtime.py"
|
|
235
254
|
# Exact bytes: read_text() would turn CRLF into LF, leaving the
|