@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
|
@@ -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
|
|
@@ -507,7 +507,8 @@ def archived_intent_criteria(archive: Path) -> dict[int, str]:
|
|
|
507
507
|
criteria = check_handoffs._success_criteria(text)
|
|
508
508
|
except check_handoffs.HandoffError as error:
|
|
509
509
|
raise ArchiveError(str(error)) from error
|
|
510
|
-
|
|
510
|
+
# Each value keeps the form without sub-bullet markers that quotes_criterion reads.
|
|
511
|
+
return {int(key[2:]): value for key, value in criteria.items()}
|
|
511
512
|
|
|
512
513
|
|
|
513
514
|
def parse_final_review(archive: Path) -> tuple:
|
|
@@ -563,7 +564,7 @@ def _final_review(archive: Path) -> tuple:
|
|
|
563
564
|
if len(headings) != len(intent_criteria):
|
|
564
565
|
raise ArchiveError("FINAL.md success criteria do not exactly cover INTENT.md")
|
|
565
566
|
for _, number, criterion in headings:
|
|
566
|
-
if
|
|
567
|
+
if not _common.quotes_criterion(criterion, intent_criteria[number]):
|
|
567
568
|
raise ArchiveError(
|
|
568
569
|
f"FINAL.md SC{number} heading text differs from archived INTENT.md"
|
|
569
570
|
)
|
|
@@ -951,10 +952,10 @@ def validate_wave_review(
|
|
|
951
952
|
(index, match.group(1), " ".join(match.group(2).split()), match.group(3))
|
|
952
953
|
)
|
|
953
954
|
actual = [(sc_id, text) for _, sc_id, text, _ in criterion_headings]
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
955
|
+
if len(actual) != len(expected_criteria) or any(
|
|
956
|
+
sc_id != expected_id or not _common.quotes_criterion(text, expected)
|
|
957
|
+
for (sc_id, text), (expected_id, expected) in zip(actual, expected_criteria)
|
|
958
|
+
):
|
|
958
959
|
raise ArchiveError(
|
|
959
960
|
f"{path.name} Intent coverage does not match its owned success criteria"
|
|
960
961
|
)
|
|
@@ -27,7 +27,7 @@ import tempfile
|
|
|
27
27
|
import time
|
|
28
28
|
from datetime import datetime, timezone
|
|
29
29
|
from pathlib import Path
|
|
30
|
-
from typing import BinaryIO, Callable, Dict, List, Optional
|
|
30
|
+
from typing import BinaryIO, Callable, Dict, List, Optional, Tuple
|
|
31
31
|
|
|
32
32
|
try:
|
|
33
33
|
from scripts import (_common, archive_milestone, build_state, discussion_validate, isolation,
|
|
@@ -77,6 +77,9 @@ STOP_ERRORS = (DriverStop, isolation.IsolationError, build_state.BuildStateError
|
|
|
77
77
|
pipeline_state.PipelineStateError, archive_milestone.ArchiveError, model_policy.PolicyError)
|
|
78
78
|
_RUNTIME_ACTIVATION_ORDER = (
|
|
79
79
|
"_common",
|
|
80
|
+
"roadmap",
|
|
81
|
+
"review_panel",
|
|
82
|
+
"loop_run",
|
|
80
83
|
"pipeline_git",
|
|
81
84
|
"worktree_paths",
|
|
82
85
|
"build_recovery",
|
|
@@ -87,11 +90,16 @@ _RUNTIME_ACTIVATION_ORDER = (
|
|
|
87
90
|
"build_state",
|
|
88
91
|
"check_handoffs",
|
|
89
92
|
"task_context",
|
|
93
|
+
# discussion_validate and archive_milestone import each other: the second
|
|
94
|
+
# reload binds the reloaded ArchiveError that STOP_ERRORS catches.
|
|
95
|
+
"discussion_validate",
|
|
90
96
|
"archive_milestone",
|
|
97
|
+
"discussion_validate",
|
|
91
98
|
"review_findings",
|
|
92
99
|
"model_policy",
|
|
93
100
|
)
|
|
94
101
|
RECOVERY_BLOCKED = "recovery blocked"
|
|
102
|
+
LEFTOVER_ISOLATE = "task {} is landed; remove its leftover isolate: {}"
|
|
95
103
|
# The stops the build contract answers with build/blocked when the round raises them (steps 1 and 2);
|
|
96
104
|
# the same stops from review, panel, or skeptics stay with the parent.
|
|
97
105
|
BLOCKING_STOPS = {"dependency-deadlock": "dependency deadlock", RECOVERY_BLOCKED: "blocked recovery"}
|
|
@@ -109,9 +117,21 @@ def _refresh_stop_errors() -> None:
|
|
|
109
117
|
model_policy.PolicyError)
|
|
110
118
|
|
|
111
119
|
|
|
120
|
+
# The selected Project runtime directory once _reload_pinned_modules ran; None keeps bundled helpers.
|
|
121
|
+
_RUNTIME_ROOT: Optional[Path] = None
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def helper_script(name: str) -> Path:
|
|
125
|
+
"""A subprocess helper from the selected runtime when it ships one, else the bundled sibling."""
|
|
126
|
+
if _RUNTIME_ROOT is not None and (_RUNTIME_ROOT / name).is_file():
|
|
127
|
+
return _RUNTIME_ROOT / name
|
|
128
|
+
return Path(__file__).resolve().parent / name
|
|
129
|
+
|
|
130
|
+
|
|
112
131
|
def _reload_pinned_modules(runtime_root: Path) -> None:
|
|
113
132
|
global isolation, build_state, task_context, contracts, pipeline_state
|
|
114
|
-
global archive_milestone, review_findings, model_policy
|
|
133
|
+
global archive_milestone, review_findings, model_policy, _RUNTIME_ROOT
|
|
134
|
+
_RUNTIME_ROOT = runtime_root
|
|
115
135
|
if _bundled_helpers():
|
|
116
136
|
path = str(runtime_root)
|
|
117
137
|
if path not in sys.path:
|
|
@@ -640,6 +660,15 @@ def run_verify(command: str, cwd: Path) -> Dict[str, object]:
|
|
|
640
660
|
return {"exit_code": completed.returncode, "stdout": completed.stdout, "stderr": completed.stderr}
|
|
641
661
|
|
|
642
662
|
|
|
663
|
+
def landing_verify(command: str, regressions: List[Dict[str, object]],
|
|
664
|
+
cwd: Path) -> Tuple[Dict[str, object], List[Dict[str, object]]]:
|
|
665
|
+
"""The task's own Verify, then every regression Verify when the own one passes."""
|
|
666
|
+
execution = run_verify(command, cwd)
|
|
667
|
+
if execution["exit_code"]:
|
|
668
|
+
return execution, []
|
|
669
|
+
return execution, [{**item, **run_verify(str(item["command"]), cwd)} for item in regressions]
|
|
670
|
+
|
|
671
|
+
|
|
643
672
|
def state_from_task(primary: Path, task_id: str) -> Dict[str, object]:
|
|
644
673
|
"""A finish-able state derived from the task frontmatter alone, for coders dispatched by hand."""
|
|
645
674
|
tasks_dir = primary / ".project/tasks"
|
|
@@ -693,6 +722,22 @@ def state_from_task(primary: Path, task_id: str) -> Dict[str, object]:
|
|
|
693
722
|
|
|
694
723
|
def finish_task(primary: Path, state: Dict[str, object]) -> Dict[str, object]:
|
|
695
724
|
"""Verify in the isolate, land, record the ledger, retire. Returns the landing receipt."""
|
|
725
|
+
landing = land_task(primary, state)
|
|
726
|
+
try:
|
|
727
|
+
if state["mode"] == "member":
|
|
728
|
+
isolation.retire_member_task(primary, str(state["member"]), str(state["task_id"]))
|
|
729
|
+
else:
|
|
730
|
+
isolation.retire(primary, Path(str(state["worktree"])), state.get("task_branch") or None, False)
|
|
731
|
+
except isolation.IsolationError as error:
|
|
732
|
+
# The landing is proven, so a failed removal is a warning; Round.recover checks the isolate again.
|
|
733
|
+
landing["cleanup_warning"] = (f"{error}; the task is landed, but its isolate was not removed. The next "
|
|
734
|
+
"round or finish checks the isolate again and stops if the isolate is "
|
|
735
|
+
"still there")
|
|
736
|
+
return landing
|
|
737
|
+
|
|
738
|
+
|
|
739
|
+
def land_task(primary: Path, state: Dict[str, object]) -> Dict[str, object]:
|
|
740
|
+
"""Verify in the isolate, land, record the ledger. Returns the landing receipt."""
|
|
696
741
|
worktree = Path(str(state["worktree"]))
|
|
697
742
|
task_id = str(state["task_id"])
|
|
698
743
|
task_file = str(state["task_file"])
|
|
@@ -701,6 +746,11 @@ def finish_task(primary: Path, state: Dict[str, object]) -> Dict[str, object]:
|
|
|
701
746
|
command = _common.task_verify_command(task_path.read_text(encoding="utf-8"))
|
|
702
747
|
if not command:
|
|
703
748
|
raise DriverStop("task has no Verify command", task=task_id)
|
|
749
|
+
# A later task can break an earlier task that shares a file; that task's Verify also decides this landing.
|
|
750
|
+
regressions = build_state.regression_verifies(primary, task_id, command)
|
|
751
|
+
# A plan-approved `supersedes` retires an earlier Verify; the receipt and the Log show each one.
|
|
752
|
+
retired = build_state.retired_verifies(primary, task_id)
|
|
753
|
+
superseded = {"retired": retired} if retired else {}
|
|
704
754
|
if state["mode"] == "serial":
|
|
705
755
|
sidecar = state["sidecar"]
|
|
706
756
|
ensured = isolation.ensure_verify_sidecar(primary, base, str(sidecar["branch"]))
|
|
@@ -710,30 +760,45 @@ def finish_task(primary: Path, state: Dict[str, object]) -> Dict[str, object]:
|
|
|
710
760
|
isolation.reproduce_verify_sidecar(sidecar_path, tree)
|
|
711
761
|
except isolation.IsolationError as error:
|
|
712
762
|
raise DriverStop(str(error), task=task_id) from error
|
|
713
|
-
execution =
|
|
763
|
+
execution, reruns = landing_verify(command, regressions, sidecar_path)
|
|
714
764
|
location = f"sidecar {sidecar['branch']}"
|
|
715
765
|
isolation.clean_verify(primary, sidecar_path, base, str(sidecar["branch"]))
|
|
716
766
|
isolation.retire(primary, sidecar_path, str(sidecar["branch"]), False)
|
|
717
767
|
else:
|
|
718
|
-
execution =
|
|
768
|
+
execution, reruns = landing_verify(command, regressions, worktree)
|
|
719
769
|
location = f"isolate {state['task_branch']}"
|
|
720
770
|
passed = execution["exit_code"] == 0
|
|
771
|
+
broken = [item for item in reruns if item["exit_code"]]
|
|
721
772
|
execution.update({"worktree": str(worktree), "location": location})
|
|
722
773
|
evidence_dir = (Path(str(state["_path"])).parent if state.get("_path")
|
|
723
774
|
else records_root(primary) / task_id / f"manual-{now().replace(':', '')}")
|
|
724
775
|
evidence_dir.mkdir(parents=True, exist_ok=True)
|
|
725
776
|
_common.atomic_write(evidence_dir / "verify.json",
|
|
726
|
-
json.dumps(execution,
|
|
777
|
+
json.dumps({**execution, "regressions": reruns, **superseded},
|
|
778
|
+
indent=2, sort_keys=True) + "\n")
|
|
727
779
|
execution["evidence"] = str(evidence_dir / "verify.json")
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
+ (f"
|
|
732
|
-
|
|
780
|
+
# The Verify of a superseding task runs in the place of the retired Verify: the label names both.
|
|
781
|
+
for label, run in [("Verify", execution),
|
|
782
|
+
*((f"regression Verify of {', '.join(item['tasks'])}"
|
|
783
|
+
+ (f" (supersedes {', '.join(item['supersedes'])})" if item.get("supersedes") else ""),
|
|
784
|
+
item) for item in reruns)]:
|
|
785
|
+
output = "\n".join(f" {line}" for line in tail(run["stdout"] + run["stderr"]).splitlines())
|
|
786
|
+
append_log(task_path, f"- {now()[:10]} — orchestrator {label} ({location}): "
|
|
787
|
+
f"{'fail' if run['exit_code'] else 'pass'}, exit {run['exit_code']}"
|
|
788
|
+
+ (f"; output tail:\n ```\n{output}\n ```" if output else ""))
|
|
789
|
+
for item in retired:
|
|
790
|
+
part = f": part of {item['part']}" if item.get("part") else ""
|
|
791
|
+
append_log(task_path, f"- {now()[:10]} — orchestrator regression Verify of {item['task']}{part} retired: "
|
|
792
|
+
f"superseded by {item['superseded_by']}")
|
|
793
|
+
if not passed or broken:
|
|
733
794
|
if state["mode"] == "parallel":
|
|
734
795
|
isolation.deactivate_task(worktree, task_id, str(state["task_branch"]))
|
|
735
796
|
elif state["mode"] == "member":
|
|
736
797
|
isolation.deactivate_member_task(primary, str(state["member"]), task_id)
|
|
798
|
+
if passed:
|
|
799
|
+
names = ", ".join(name for item in broken for name in item["tasks"])
|
|
800
|
+
raise DriverStop(f"regression Verify of {names} failed in the isolate", task=task_id,
|
|
801
|
+
execution=execution, regressions=broken)
|
|
737
802
|
raise DriverStop("Verify failed in the isolate", task=task_id, execution=execution)
|
|
738
803
|
if state["mode"] == "member":
|
|
739
804
|
member = str(state["member"])
|
|
@@ -744,9 +809,9 @@ def finish_task(primary: Path, state: Dict[str, object]) -> Dict[str, object]:
|
|
|
744
809
|
ledger = isolation.git_output(worktree, "rev-parse", f"{landing}^") == str(state["member_base"])
|
|
745
810
|
if ledger:
|
|
746
811
|
build_state.verify_record(str(primary), command, landing, "pass", execution=execution, member=member)
|
|
747
|
-
isolation.retire_member_task(primary, member, task_id)
|
|
748
812
|
return {"task": task_id, "commit": str(landed["commit"]), "mode": "member",
|
|
749
|
-
"landing": landing, "verify": execution, "
|
|
813
|
+
"landing": landing, "verify": execution, "regressions": reruns, **superseded,
|
|
814
|
+
"ledger": ledger}
|
|
750
815
|
landed = isolation.land(primary, worktree, base, task_id, str(state["title"]), task_file,
|
|
751
816
|
list(state["files"]))
|
|
752
817
|
commit = str(landed["commit"])
|
|
@@ -754,9 +819,8 @@ def finish_task(primary: Path, state: Dict[str, object]) -> Dict[str, object]:
|
|
|
754
819
|
ledger = isolation.git_output(primary, "rev-parse", f"{commit}^") == base
|
|
755
820
|
if ledger:
|
|
756
821
|
build_state.verify_record(str(primary), command, commit, "pass", execution=execution)
|
|
757
|
-
isolation.retire(primary, worktree, state.get("task_branch") or None, False)
|
|
758
822
|
return {"task": task_id, "commit": commit, "mode": landed["mode"], "verify": execution,
|
|
759
|
-
"ledger": ledger}
|
|
823
|
+
"regressions": reruns, **superseded, "ledger": ledger}
|
|
760
824
|
|
|
761
825
|
|
|
762
826
|
# --- shared by round and review --------------------------------------------
|
|
@@ -772,6 +836,11 @@ def recover_report(primary: Path, project_dir: str, receipt: Dict[str, object])
|
|
|
772
836
|
report = isolation.recover(primary, Path(project_dir) / "tasks")
|
|
773
837
|
receipt["steps"].append({"script": "isolation.py recover", "result": report})
|
|
774
838
|
if report["verdict"] == "block":
|
|
839
|
+
blocked = [task for task in report["tasks"] if task["verdict"] == "block"]
|
|
840
|
+
if blocked and all(task.get("leftover") for task in blocked):
|
|
841
|
+
# Every blocked task is landed: the stop names only the isolates to remove, and the build stays active.
|
|
842
|
+
raise DriverStop("; ".join(LEFTOVER_ISOLATE.format(task["task_id"], task["leftover"])
|
|
843
|
+
for task in blocked), recover=blocked)
|
|
775
844
|
raise DriverStop(RECOVERY_BLOCKED, recover=report)
|
|
776
845
|
return report
|
|
777
846
|
|
|
@@ -907,7 +976,7 @@ class Round:
|
|
|
907
976
|
self.receipt["steps"].append({"script": "isolation.recover_member_landing", "task": journal.stem,
|
|
908
977
|
"result": result})
|
|
909
978
|
if result["state"] == "landed":
|
|
910
|
-
|
|
979
|
+
self.retire_member(str(result["member"]), journal.stem, str(result["landing"]))
|
|
911
980
|
retired_members.add(journal.stem)
|
|
912
981
|
report = recover_report(self.primary, self.project_dir, self.receipt)
|
|
913
982
|
for task in report["tasks"]:
|
|
@@ -924,7 +993,7 @@ class Round:
|
|
|
924
993
|
diagnose = shlex.join([
|
|
925
994
|
sys.executable,
|
|
926
995
|
"-B",
|
|
927
|
-
str(
|
|
996
|
+
str(helper_script("pipeline_diagnose.py")),
|
|
928
997
|
"diagnose",
|
|
929
998
|
"--repo",
|
|
930
999
|
str(self.primary),
|
|
@@ -936,16 +1005,36 @@ class Round:
|
|
|
936
1005
|
recover=task,
|
|
937
1006
|
)
|
|
938
1007
|
if verdict == "recovered" and task.get("member") and task.get("task_id") not in retired_members:
|
|
939
|
-
|
|
1008
|
+
self.retire_member(str(task["member"]), str(task["task_id"]), str(task["landing"]))
|
|
940
1009
|
if verdict == "recovered" and worktree and worktree["clean"] and task.get("task_branch"):
|
|
941
1010
|
isolation.retire(self.primary, Path(str(worktree["path"])),
|
|
942
1011
|
str(task["task_branch"]), False)
|
|
943
|
-
elif
|
|
944
|
-
|
|
945
|
-
|
|
1012
|
+
elif verdict == "recovered" and worktree is None and task.get("task_branch"):
|
|
1013
|
+
# The isolate is gone and the landing is proven: retire proves the branch, then deletes it.
|
|
1014
|
+
isolation.retire(self.primary, None, str(task["task_branch"]), True, str(task["commit"]))
|
|
1015
|
+
elif verdict == "resume" and (task.get("landing_retry") or task.get("dispatch_retry")):
|
|
1016
|
+
# ponytail: interrupted retries stay with the parent's documented
|
|
946
1017
|
# procedure; failed/blocked reconciliation surfaces through ready's typed error.
|
|
947
1018
|
raise DriverStop(f"task {task.get('task_id')} needs recovery: {verdict}", recover=task)
|
|
948
1019
|
|
|
1020
|
+
def retire_member(self, member: str, task_id: str, landing: str) -> None:
|
|
1021
|
+
"""Retire a landed member task's sidecar; a proven leftover of it is a stop that names only its path."""
|
|
1022
|
+
checkout, project, _ = isolation._member_context(self.primary, member)
|
|
1023
|
+
sidecar = isolation.sidecar_root(checkout, "task", f"{project}-{task_id}")
|
|
1024
|
+
branch = f"refs/heads/{isolation.TASK_BRANCH_PREFIX}{project}-{task_id}"
|
|
1025
|
+
tip = isolation.run_git(checkout, "rev-parse", "--verify", "--quiet", branch)
|
|
1026
|
+
commits = [landing, *tip.stdout.split()]
|
|
1027
|
+
try:
|
|
1028
|
+
isolation.retire_member_task(self.primary, member, task_id)
|
|
1029
|
+
except isolation.RemovalError as error:
|
|
1030
|
+
# Missing files are a removal that stopped part way; every other state keeps the refusal.
|
|
1031
|
+
if not (isolation.only_missing_files(sidecar)
|
|
1032
|
+
or isolation.holds_only_landed_files(checkout, sidecar, commits)):
|
|
1033
|
+
raise
|
|
1034
|
+
raise DriverStop(LEFTOVER_ISOLATE.format(task_id, sidecar), cleanup_error=str(error))
|
|
1035
|
+
if isolation.holds_only_landed_files(checkout, sidecar, commits):
|
|
1036
|
+
raise DriverStop(LEFTOVER_ISOLATE.format(task_id, sidecar))
|
|
1037
|
+
|
|
949
1038
|
# settle exited children ---------------------------------------------
|
|
950
1039
|
|
|
951
1040
|
def isolate_live(self, state: Dict[str, object]) -> bool:
|
|
@@ -1231,7 +1320,7 @@ class Round:
|
|
|
1231
1320
|
continue
|
|
1232
1321
|
prepared = self.runner("prepare-task", head, task["id"], round_size)
|
|
1233
1322
|
isolate = prepared[0]["result"]
|
|
1234
|
-
sidecar = prepared[1]["result"] if len(prepared) > 1 else None
|
|
1323
|
+
sidecar = prepared[-1]["result"] if len(prepared) > 1 else None # the isolate-verify step is last
|
|
1235
1324
|
worktree = Path(str(isolate["worktree"]))
|
|
1236
1325
|
isolation.activate_task(worktree, head, task["id"], f"build_{task['id'].lower()}",
|
|
1237
1326
|
task["task_file"], isolate["task_branch"])
|
|
@@ -1600,8 +1689,8 @@ class Review:
|
|
|
1600
1689
|
|
|
1601
1690
|
|
|
1602
1691
|
def helper_json(receipt: Dict[str, object], script: str, *arguments: str, cwd: Path) -> Dict[str, object]:
|
|
1603
|
-
"""Run a bundled
|
|
1604
|
-
command = [sys.executable, "-B", str(
|
|
1692
|
+
"""Run a helper (runtime-selected, else bundled) and return its JSON stdout; a non-zero exit is a typed stop."""
|
|
1693
|
+
command = [sys.executable, "-B", str(helper_script(script)), *arguments]
|
|
1605
1694
|
completed = subprocess.run(command, cwd=cwd, capture_output=True, encoding="utf-8", errors="replace")
|
|
1606
1695
|
step = {"script": script, "command": command, "exit_code": completed.returncode,
|
|
1607
1696
|
"stdout": completed.stdout, "stderr": completed.stderr}
|
|
@@ -2007,6 +2096,23 @@ def next_task_id(tasks_dir: Path) -> str:
|
|
|
2007
2096
|
return f"T{max(numbers, default=0) + 1:03d}"
|
|
2008
2097
|
|
|
2009
2098
|
|
|
2099
|
+
def replaced_review_base(primary: Path, project_dir: str, wave: int, cycle: int) -> Optional[str]:
|
|
2100
|
+
"""The recovery base when this cycle's review replaces the one a build recovery set aside, else None."""
|
|
2101
|
+
recovery = pipeline_state._build_recovery().context(primary)
|
|
2102
|
+
if not recovery:
|
|
2103
|
+
return None
|
|
2104
|
+
relative = f"{project_dir}/review/wave-{wave}.cycle{cycle}.md"
|
|
2105
|
+
before = isolation.run_git(primary, "show", f"{recovery['base']}:{relative}")
|
|
2106
|
+
current = primary / relative
|
|
2107
|
+
# Plan approval restores the review of an unchanged wave before its checkpoint, so no commit
|
|
2108
|
+
# since the base touches that file. A set-aside review is deleted by that checkpoint.
|
|
2109
|
+
rewritten = isolation.run_git(primary, "log", "-1", "--format=%H", f"{recovery['base']}..HEAD", "--", relative)
|
|
2110
|
+
if (not rewritten.stdout.strip() and before.returncode == 0 and current.is_file()
|
|
2111
|
+
and current.read_text(encoding="utf-8") == before.stdout):
|
|
2112
|
+
return None # the restored review keeps its repairs
|
|
2113
|
+
return recovery["base"]
|
|
2114
|
+
|
|
2115
|
+
|
|
2010
2116
|
def fix_tasks(primary: Path, options: argparse.Namespace) -> Dict[str, object]:
|
|
2011
2117
|
"""Step 7: one fix task per findings batch, carrying failed criteria and observations verbatim."""
|
|
2012
2118
|
receipt: Dict[str, object] = {"status": None, "wave": options.wave, "cycle": options.cycle,
|
|
@@ -2015,14 +2121,32 @@ def fix_tasks(primary: Path, options: argparse.Namespace) -> Dict[str, object]:
|
|
|
2015
2121
|
project = primary / options.project_dir
|
|
2016
2122
|
tasks_dir = project / "tasks"
|
|
2017
2123
|
repairs = []
|
|
2124
|
+
pending = []
|
|
2018
2125
|
for path in sorted(tasks_dir.glob("*-fix-wave-*-cycle-*.md")):
|
|
2019
2126
|
text = path.read_text(encoding="utf-8")
|
|
2020
2127
|
fields, _ = isolation.task_frontmatter(text)
|
|
2021
2128
|
repairs.append({"task": fields["id"], "path": path.relative_to(primary).as_posix(),
|
|
2022
|
-
"wave": int(fields["wave"]), "locators": re.findall(r"(?m)^### (.+)$", _common.section_body(text, "Review findings") or "")
|
|
2129
|
+
"wave": int(fields["wave"]), "locators": re.findall(r"(?m)^### (.+)$", _common.section_body(text, "Review findings") or ""),
|
|
2130
|
+
**({"repo": fields["repo"]} if fields.get("repo") else {})})
|
|
2131
|
+
if fields["status"] != "done":
|
|
2132
|
+
pending.append(repairs[-1])
|
|
2023
2133
|
suffix = f"-fix-wave-{options.wave}-cycle-{options.cycle}.md"
|
|
2024
|
-
|
|
2025
|
-
|
|
2134
|
+
# A build recovery restarts a changed wave at cycle 1, so the repairs and the PLAN heading
|
|
2135
|
+
# of the review it set aside carry the same names. They belong to that review, not this one.
|
|
2136
|
+
replaced = replaced_review_base(primary, options.project_dir, options.wave, options.cycle)
|
|
2137
|
+
|
|
2138
|
+
def of_replaced_review(relative: str) -> bool:
|
|
2139
|
+
if not replaced or isolation.run_git(primary, "show", f"{replaced}:{relative}").returncode != 0:
|
|
2140
|
+
return False
|
|
2141
|
+
return not isolation.run_git(primary, "log", "--diff-filter=AD", "--format=%H",
|
|
2142
|
+
f"{replaced}..HEAD", "--", relative).stdout.strip()
|
|
2143
|
+
|
|
2144
|
+
stale = [item for item in repairs if item["path"].endswith(suffix) and of_replaced_review(item["path"])]
|
|
2145
|
+
existing = [item for item in repairs if item["path"].endswith(suffix) and item not in stale]
|
|
2146
|
+
# A landed repair no longer carries its finding: a repeat proves that fix failed and needs the next one.
|
|
2147
|
+
# Two repos can carry the same locator, so a repair carries a finding only in its own repo.
|
|
2148
|
+
carried = {(item.get("repo"), locator) for item in pending if not item["path"].endswith(suffix)
|
|
2149
|
+
for locator in item["locators"]}
|
|
2026
2150
|
findings = review_findings.compute(primary, options.project_dir, options.wave, options.cycle)
|
|
2027
2151
|
receipt["findings"] = findings
|
|
2028
2152
|
escalation = [key for key in ("structural_blockers", "skeptic_groups", "cap_reached", "all_refuted")
|
|
@@ -2043,8 +2167,10 @@ def fix_tasks(primary: Path, options: argparse.Namespace) -> Dict[str, object]:
|
|
|
2043
2167
|
repair_waves = {item["wave"] for item in existing}
|
|
2044
2168
|
if len(repair_waves) > 1:
|
|
2045
2169
|
raise DriverStop("repair tasks for this cycle disagree on their wave")
|
|
2046
|
-
|
|
2170
|
+
stale_waves = {item["wave"] for item in stale}
|
|
2171
|
+
repair_heading = next((heading for heading in re.finditer(
|
|
2047
2172
|
rf"(?m)^## Wave (\d+) — fix wave {options.wave} cycle {options.cycle} review findings$", plan_text)
|
|
2173
|
+
if int(heading.group(1)) not in stale_waves), None)
|
|
2048
2174
|
if repair_waves:
|
|
2049
2175
|
fix_wave = next(iter(repair_waves))
|
|
2050
2176
|
elif repair_heading:
|
|
@@ -2055,9 +2181,10 @@ def fix_tasks(primary: Path, options: argparse.Namespace) -> Dict[str, object]:
|
|
|
2055
2181
|
raise DriverStop("repair tasks disagree with the PLAN repair wave")
|
|
2056
2182
|
receipt["existing"] = existing
|
|
2057
2183
|
for batch in findings["fix_batches"]:
|
|
2058
|
-
|
|
2184
|
+
repo = batch.get("repo") # a member name, or None for the coordinator
|
|
2185
|
+
if any(item.get("repo") == repo and set(batch["locators"]) <= set(item["locators"]) for item in existing):
|
|
2059
2186
|
continue
|
|
2060
|
-
if all(locator in carried for locator in batch["locators"]):
|
|
2187
|
+
if all((repo, locator) in carried for locator in batch["locators"]):
|
|
2061
2188
|
receipt["carried"].append(batch)
|
|
2062
2189
|
continue
|
|
2063
2190
|
batch_groups = [groups[locator] for locator in batch["locators"]]
|
|
@@ -2065,9 +2192,15 @@ def fix_tasks(primary: Path, options: argparse.Namespace) -> Dict[str, object]:
|
|
|
2065
2192
|
for source in sources:
|
|
2066
2193
|
if source not in texts:
|
|
2067
2194
|
raise DriverStop(f"fix batch names unknown task {source}")
|
|
2195
|
+
# One task changes one repo: the fix depends on, and runs the Verify of, the source tasks of its repo.
|
|
2196
|
+
sources = [source for source in sources if isolation.task_frontmatter(texts[source])[0].get("repo") == repo]
|
|
2197
|
+
# A repeat builds on the landed repairs of its finding: repair-evidence links them by this dependency.
|
|
2198
|
+
deps = sources + [item["task"] for item in repairs
|
|
2199
|
+
if item not in pending and item.get("repo") == repo
|
|
2200
|
+
and f"-fix-wave-{options.wave}-cycle-" in item["path"] and not item["path"].endswith(suffix)
|
|
2201
|
+
and set(item["locators"]) & set(batch["locators"])]
|
|
2068
2202
|
task_id = next_task_id(tasks_dir)
|
|
2069
|
-
verify =
|
|
2070
|
-
f"(\n{command}\n)" for command in dict.fromkeys(_common.task_verify_command(texts[source]) for source in sources))
|
|
2203
|
+
verify = _common.fix_verify(_common.task_verify_command(texts[source]) for source in sources)
|
|
2071
2204
|
heavy = any(re.search(r"(?m)^Heavy:\s*yes", texts[source]) for source in sources)
|
|
2072
2205
|
criteria = "\n".join(f"{index}. {criterion}" for index, criterion in
|
|
2073
2206
|
enumerate(dict.fromkeys(group["criterion"] for group in batch_groups), 1))
|
|
@@ -2075,18 +2208,19 @@ def fix_tasks(primary: Path, options: argparse.Namespace) -> Dict[str, object]:
|
|
|
2075
2208
|
f"### {group['locator']}\nCriterion: {group['criterion']}\n"
|
|
2076
2209
|
+ "\n".join(f"- {item['text']}" for item in group["observations"]) for group in batch_groups)
|
|
2077
2210
|
files = "\n".join(f" - {path}" for path in batch["files"])
|
|
2211
|
+
member = f"repo: {repo}\n" if repo else "" # the task template puts `repo:` before `files:`
|
|
2078
2212
|
title = f"Fix wave {options.wave} cycle {options.cycle} review findings in {', '.join(sources)}"
|
|
2079
2213
|
text = f"""---
|
|
2080
2214
|
id: {task_id}
|
|
2081
2215
|
title: {title}
|
|
2082
2216
|
wave: {fix_wave}
|
|
2083
|
-
deps: [{', '.join(
|
|
2217
|
+
deps: [{', '.join(deps)}]
|
|
2084
2218
|
status: pending
|
|
2085
2219
|
agent: null
|
|
2086
2220
|
base: null
|
|
2087
2221
|
worktree: null
|
|
2088
2222
|
task_branch: null
|
|
2089
|
-
files:
|
|
2223
|
+
{member}files:
|
|
2090
2224
|
{files}
|
|
2091
2225
|
---
|
|
2092
2226
|
|
|
@@ -2135,8 +2269,8 @@ Heavy: {'yes' if heavy else 'no'}
|
|
|
2135
2269
|
path = tasks_dir / f"{task_id}-fix-wave-{options.wave}-cycle-{options.cycle}.md"
|
|
2136
2270
|
_common.atomic_write(path, text)
|
|
2137
2271
|
receipt["created"].append({"task": task_id, "path": path.relative_to(primary).as_posix(),
|
|
2138
|
-
"wave": fix_wave, "deps":
|
|
2139
|
-
"locators": batch["locators"]})
|
|
2272
|
+
"wave": fix_wave, "deps": deps, "files": batch["files"],
|
|
2273
|
+
"locators": batch["locators"], **({"repo": repo} if repo else {})})
|
|
2140
2274
|
rows = []
|
|
2141
2275
|
for item in [*existing, *receipt["created"]]:
|
|
2142
2276
|
fields, _ = isolation.task_frontmatter((primary / item["path"]).read_text(encoding="utf-8"))
|
|
@@ -2168,7 +2302,7 @@ Heavy: {'yes' if heavy else 'no'}
|
|
|
2168
2302
|
if plan_changed:
|
|
2169
2303
|
_common.atomic_write(plan_path, updated)
|
|
2170
2304
|
receipt["plan_wave"] = fix_wave
|
|
2171
|
-
lint = subprocess.run([sys.executable, "-B", str(
|
|
2305
|
+
lint = subprocess.run([sys.executable, "-B", str(helper_script("check_task_briefs.py")),
|
|
2172
2306
|
"--repo", str(primary), "--base", isolation.current_sha(primary),
|
|
2173
2307
|
"--tasks-dir", f"{options.project_dir}/tasks",
|
|
2174
2308
|
"--project-dir", options.project_dir], capture_output=True, encoding="utf-8", errors="replace")
|
|
@@ -2421,6 +2555,10 @@ def main(argv=None) -> int:
|
|
|
2421
2555
|
if native and native["base"] != records[0].get("base"):
|
|
2422
2556
|
records = []
|
|
2423
2557
|
else:
|
|
2558
|
+
if (native and records[0]["outcome"] == "blocked" and records[0].get("native")
|
|
2559
|
+
and not records[0].get("command")):
|
|
2560
|
+
# A failed finish leaves a host-dispatched task active at its base: finish may run again.
|
|
2561
|
+
update_state(records[0], outcome=None, reason=None)
|
|
2424
2562
|
native = None
|
|
2425
2563
|
if arguments.task_id in current.proven:
|
|
2426
2564
|
commit = current.proven[arguments.task_id]
|
package/scripts/git_guard.py
CHANGED
|
@@ -19,9 +19,10 @@ _HERE = Path(__file__).resolve().parent
|
|
|
19
19
|
sys.path.insert(0, str(_HERE / "runtime" if (_HERE / "runtime" / "isolation.py").is_file() else _HERE))
|
|
20
20
|
sys.dont_write_bytecode = True # a hook must not leave __pycache__ in the worktree
|
|
21
21
|
try:
|
|
22
|
-
from isolation import BOOKKEEPING_PREFIXES, NULL_SHA, _landing_state, task_frontmatter
|
|
22
|
+
from isolation import BOOKKEEPING_PREFIXES, NULL_SHA, _landing_state, task_frontmatter, undeclared_paths
|
|
23
23
|
from pipeline_git import is_ship_subject, ship_body_matches, ship_commit_body, task_commit_body
|
|
24
24
|
from pipeline_state import _completion_status
|
|
25
|
+
from _common import valid_default_branch
|
|
25
26
|
import members
|
|
26
27
|
except ImportError as error: # pragma: no cover - broken install
|
|
27
28
|
print(
|
|
@@ -68,6 +69,11 @@ PUBLICATION_HINT = (
|
|
|
68
69
|
"(STATE shipped/done) through archive_milestone.py integrate; findings during "
|
|
69
70
|
"ship reopen through the patch plan (see SHIP.md)"
|
|
70
71
|
)
|
|
72
|
+
AUTHORIZED_PUSH_HINT = (
|
|
73
|
+
"an owner-approved push of one exact commit to a branch other than the bound "
|
|
74
|
+
"or default branch needs refs/gsd-path/authorizations/<project>/push/<ref> "
|
|
75
|
+
"at that commit (see HOOKS.md)"
|
|
76
|
+
)
|
|
71
77
|
LEGACY_STATE_FIELDS = {
|
|
72
78
|
"pipeline",
|
|
73
79
|
"project",
|
|
@@ -240,7 +246,10 @@ def frontmatter_of(content, label):
|
|
|
240
246
|
|
|
241
247
|
|
|
242
248
|
def strict_ship_state(state, archive):
|
|
243
|
-
|
|
249
|
+
# default_branch is optional beside each accepted field set.
|
|
250
|
+
if "default_branch" in state and not valid_default_branch(state["default_branch"]):
|
|
251
|
+
return False
|
|
252
|
+
fields = frozenset(state) - {"default_branch"}
|
|
244
253
|
if fields not in {
|
|
245
254
|
frozenset(LEGACY_STATE_FIELDS),
|
|
246
255
|
frozenset(INTEGRATION_STATE_FIELDS),
|
|
@@ -509,7 +518,7 @@ def product_commit_violations(entries, subject, body):
|
|
|
509
518
|
if error:
|
|
510
519
|
problems.append(error)
|
|
511
520
|
if contract_paths is not None:
|
|
512
|
-
stray =
|
|
521
|
+
stray = undeclared_paths(repo_root(), base, set(staged), contract_paths)
|
|
513
522
|
if stray:
|
|
514
523
|
problems.append("undeclared paths: " + ", ".join(stray))
|
|
515
524
|
if body != task_commit_body(task_file, staged, base).strip():
|
|
@@ -559,8 +568,10 @@ def member_pre_push_violations(lines, role):
|
|
|
559
568
|
"""Member mode: the member's own STATE is ignored; coordinator refs need authorization."""
|
|
560
569
|
checkout, project = repo_root(), role["project"]
|
|
561
570
|
reserved = members.member_ref_prefixes(project)
|
|
562
|
-
|
|
563
|
-
|
|
571
|
+
# The coordinator's MEMBERS.md records this member's default branch.
|
|
572
|
+
default = members.member_default_branch(role["coordinator"], role["name"])
|
|
573
|
+
members.require_origin_branch(checkout, default)
|
|
574
|
+
exclude = ["--not", f"refs/remotes/origin/{default}"]
|
|
564
575
|
member_work = None
|
|
565
576
|
found = []
|
|
566
577
|
for _, local_sha, remote_ref, remote_sha in _push_updates(lines):
|
|
@@ -615,14 +626,25 @@ def pre_push_violations(lines):
|
|
|
615
626
|
continue
|
|
616
627
|
state = state_at(local_sha)
|
|
617
628
|
owed = unshipped_bound_branch(state) if state is not None else None
|
|
618
|
-
if owed is not None:
|
|
629
|
+
if owed is not None and not authorized_publication(state, remote_ref, local_sha):
|
|
619
630
|
found.append(
|
|
620
631
|
f"{remote_ref} <- {local_sha[:12]} carries unshipped {owed} work "
|
|
621
|
-
f"({state.get('phase')}/{state.get('status')}); {PUBLICATION_HINT}"
|
|
632
|
+
f"({state.get('phase')}/{state.get('status')}); {PUBLICATION_HINT}; "
|
|
633
|
+
f"{AUTHORIZED_PUSH_HINT}"
|
|
622
634
|
)
|
|
623
635
|
return found
|
|
624
636
|
|
|
625
637
|
|
|
638
|
+
def authorized_publication(state, remote_ref, sha):
|
|
639
|
+
"""The owner authorized this exact commit on this one non-default branch."""
|
|
640
|
+
project = state.get("project")
|
|
641
|
+
if not isinstance(project, str) or not members.NAME_RE.fullmatch(project):
|
|
642
|
+
return False
|
|
643
|
+
if not remote_ref.startswith("refs/heads/") or remote_ref == f"refs/heads/{default_branch()}":
|
|
644
|
+
return False
|
|
645
|
+
return members.authorized(repo_root(), project, "push", remote_ref, sha)
|
|
646
|
+
|
|
647
|
+
|
|
626
648
|
def is_ship_commit(subject):
|
|
627
649
|
return bool(subject and subject.strip().casefold().startswith("ship:"))
|
|
628
650
|
|
|
@@ -641,7 +663,20 @@ def abandon_contract_violations(subject, body, abandon):
|
|
|
641
663
|
return []
|
|
642
664
|
|
|
643
665
|
|
|
666
|
+
def recorded_default_branch():
|
|
667
|
+
"""STATE default_branch in the worktree: main without the field; None without a readable STATE."""
|
|
668
|
+
try:
|
|
669
|
+
content = (repo_root() / ".project" / "STATE.md").read_text(encoding="utf-8")
|
|
670
|
+
name = frontmatter_of(content, "STATE.md").get("default_branch", "main")
|
|
671
|
+
except (OSError, ValueError, subprocess.CalledProcessError):
|
|
672
|
+
return None
|
|
673
|
+
return name if valid_default_branch(name) else None
|
|
674
|
+
|
|
675
|
+
|
|
644
676
|
def default_branch():
|
|
677
|
+
recorded = recorded_default_branch()
|
|
678
|
+
if recorded:
|
|
679
|
+
return recorded
|
|
645
680
|
origin_head = subprocess.run(
|
|
646
681
|
["git", "symbolic-ref", "--quiet", "--short", "refs/remotes/origin/HEAD"],
|
|
647
682
|
capture_output=True,
|