@opengsd/gsd-path 1.5.0 → 1.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +138 -0
- package/DOCS.md +4 -3
- package/FULL.md +64 -4
- package/HOOKS.md +63 -2
- package/MIGRATE.md +12 -0
- package/QUICK.md +4 -2
- package/README.md +67 -15
- package/RUNTIME.md +23 -5
- package/UPDATE.md +35 -0
- package/WORKFLOW.md +58 -3
- package/package.json +1 -1
- package/scripts/_common.py +107 -1
- package/scripts/build_recovery.py +62 -30
- package/scripts/build_state.py +195 -5
- package/scripts/check_handoffs.py +92 -30
- package/scripts/check_task_briefs.py +19 -14
- package/scripts/detect_project.py +17 -2
- package/scripts/discussion_records.py +9 -1
- package/scripts/discussion_validate.py +7 -6
- package/scripts/dispatch_driver.py +157 -31
- package/scripts/git_guard.py +24 -5
- package/scripts/guard_hook.py +1216 -47
- package/scripts/install.mjs +76 -3
- package/scripts/install.py +182 -37
- package/scripts/integration.py +98 -64
- package/scripts/isolation.py +148 -11
- package/scripts/members.py +68 -25
- package/scripts/migrate_core.py +2 -0
- package/scripts/model_policy.py +5 -1
- package/scripts/pipeline_diagnose.py +17 -2
- package/scripts/pipeline_git.py +68 -30
- package/scripts/pipeline_state.py +28 -6
- package/scripts/pipeline_undo.py +17 -8
- package/scripts/promote_lookahead.py +3 -3
- package/scripts/review_findings.py +180 -44
- package/scripts/roadmap.py +9 -1
- package/scripts/runtime_store.py +1 -0
- package/scripts/state_checkpoint.py +30 -9
- package/scripts/state_promote.py +15 -4
- package/scripts/task_context.py +2 -0
- package/scripts/workflow_run.py +9 -2
- package/skills/gsd-path/DEFINE.md +6 -2
- package/skills/gsd-path/PLAN.md +3 -0
- package/skills/gsd-path/ROADMAP.md +16 -6
- package/skills/gsd-path/SHIP.md +26 -4
- package/skills/gsd-path/SKILL.md +52 -24
- package/skills/gsd-path/references/build-native.md +96 -9
- package/skills/gsd-path/references/build-recovery.md +13 -0
- package/skills/gsd-path/references/coder.md +7 -3
- package/skills/gsd-path/references/operating-rules.md +11 -3
- package/skills/gsd-path/references/planner.md +13 -1
- package/skills/gsd-path/references/reviewer-final.md +5 -5
- package/skills/gsd-path/references/reviewer-wave.md +46 -13
- package/skills/gsd-path/references/runtime-selection.md +9 -0
- package/skills/gsd-path/scripts/_common.py +107 -1
- package/skills/gsd-path/scripts/build_recovery.py +62 -30
- package/skills/gsd-path/scripts/build_state.py +195 -5
- package/skills/gsd-path/scripts/check_handoffs.py +92 -30
- package/skills/gsd-path/scripts/check_task_briefs.py +19 -14
- package/skills/gsd-path/scripts/detect_project.py +17 -2
- package/skills/gsd-path/scripts/discussion_records.py +9 -1
- package/skills/gsd-path/scripts/discussion_validate.py +7 -6
- package/skills/gsd-path/scripts/dispatch_driver.py +157 -31
- package/skills/gsd-path/scripts/git_guard.py +24 -5
- package/skills/gsd-path/scripts/integration.py +98 -64
- package/skills/gsd-path/scripts/isolation.py +148 -11
- package/skills/gsd-path/scripts/members.py +68 -25
- package/skills/gsd-path/scripts/model_policy.py +5 -1
- package/skills/gsd-path/scripts/pipeline_git.py +68 -30
- package/skills/gsd-path/scripts/pipeline_state.py +28 -6
- package/skills/gsd-path/scripts/promote_lookahead.py +3 -3
- package/skills/gsd-path/scripts/review_findings.py +180 -44
- package/skills/gsd-path/scripts/roadmap.py +9 -1
- package/skills/gsd-path/scripts/state_checkpoint.py +30 -9
- package/skills/gsd-path/scripts/state_promote.py +15 -4
- package/skills/gsd-path/scripts/task_context.py +2 -0
- package/skills/gsd-path/scripts/workflow_run.py +9 -2
- package/skills/gsd-path/templates/final-review.md +5 -5
- package/skills/gsd-path/templates/state.md +1 -0
- package/skills/gsd-path/templates/task.md +1 -0
- package/skills/gsd-path/templates/wave-review.md +18 -9
- package/skills/gsd-path-build/references/build-native.md +96 -9
- package/skills/gsd-path-build/references/build-recovery.md +13 -0
- package/skills/gsd-path-build/references/coder.md +7 -3
- package/skills/gsd-path-build/references/operating-rules.md +11 -3
- package/skills/gsd-path-build/references/reviewer-final.md +5 -5
- package/skills/gsd-path-build/references/reviewer-wave.md +46 -13
- package/skills/gsd-path-build/references/runtime-selection.md +9 -0
- package/skills/gsd-path-build/scripts/_common.py +107 -1
- package/skills/gsd-path-build/scripts/build_recovery.py +62 -30
- package/skills/gsd-path-build/scripts/build_state.py +195 -5
- package/skills/gsd-path-build/scripts/check_handoffs.py +92 -30
- package/skills/gsd-path-build/scripts/check_task_briefs.py +19 -14
- package/skills/gsd-path-build/scripts/detect_project.py +17 -2
- package/skills/gsd-path-build/scripts/discussion_records.py +9 -1
- package/skills/gsd-path-build/scripts/discussion_validate.py +7 -6
- package/skills/gsd-path-build/scripts/dispatch_driver.py +157 -31
- package/skills/gsd-path-build/scripts/integration.py +98 -64
- package/skills/gsd-path-build/scripts/isolation.py +148 -11
- package/skills/gsd-path-build/scripts/members.py +68 -25
- package/skills/gsd-path-build/scripts/model_policy.py +5 -1
- package/skills/gsd-path-build/scripts/pipeline_git.py +68 -30
- package/skills/gsd-path-build/scripts/pipeline_state.py +28 -6
- package/skills/gsd-path-build/scripts/promote_lookahead.py +3 -3
- package/skills/gsd-path-build/scripts/review_findings.py +180 -44
- package/skills/gsd-path-build/scripts/roadmap.py +9 -1
- package/skills/gsd-path-build/scripts/state_checkpoint.py +30 -9
- package/skills/gsd-path-build/scripts/state_promote.py +15 -4
- package/skills/gsd-path-build/scripts/task_context.py +2 -0
- package/skills/gsd-path-build/scripts/workflow_run.py +9 -2
- package/skills/gsd-path-build/templates/task.md +1 -0
- package/skills/gsd-path-build/templates/wave-review.md +18 -9
- package/skills/gsd-path-decide/references/operating-rules.md +11 -3
- package/skills/gsd-path-decide/references/runtime-selection.md +9 -0
- package/skills/gsd-path-decide/scripts/_common.py +107 -1
- package/skills/gsd-path-decide/scripts/build_recovery.py +62 -30
- package/skills/gsd-path-decide/scripts/check_handoffs.py +92 -30
- package/skills/gsd-path-decide/scripts/check_task_briefs.py +19 -14
- package/skills/gsd-path-decide/scripts/detect_project.py +17 -2
- package/skills/gsd-path-decide/scripts/discussion_records.py +9 -1
- package/skills/gsd-path-decide/scripts/discussion_validate.py +7 -6
- package/skills/gsd-path-decide/scripts/integration.py +98 -64
- package/skills/gsd-path-decide/scripts/isolation.py +148 -11
- package/skills/gsd-path-decide/scripts/members.py +68 -25
- package/skills/gsd-path-decide/scripts/model_policy.py +5 -1
- package/skills/gsd-path-decide/scripts/pipeline_git.py +68 -30
- package/skills/gsd-path-decide/scripts/pipeline_state.py +28 -6
- package/skills/gsd-path-decide/scripts/promote_lookahead.py +3 -3
- package/skills/gsd-path-decide/scripts/roadmap.py +9 -1
- package/skills/gsd-path-decide/scripts/state_checkpoint.py +30 -9
- package/skills/gsd-path-decide/scripts/state_promote.py +15 -4
- package/skills/gsd-path-define/SKILL.md +6 -2
- package/skills/gsd-path-define/references/build-recovery.md +13 -0
- package/skills/gsd-path-define/references/operating-rules.md +11 -3
- package/skills/gsd-path-define/references/runtime-selection.md +9 -0
- package/skills/gsd-path-define/scripts/_common.py +107 -1
- package/skills/gsd-path-define/scripts/build_recovery.py +62 -30
- package/skills/gsd-path-define/scripts/check_handoffs.py +92 -30
- package/skills/gsd-path-define/scripts/check_task_briefs.py +19 -14
- package/skills/gsd-path-define/scripts/detect_project.py +17 -2
- package/skills/gsd-path-define/scripts/discussion_records.py +9 -1
- package/skills/gsd-path-define/scripts/discussion_validate.py +7 -6
- package/skills/gsd-path-define/scripts/integration.py +98 -64
- package/skills/gsd-path-define/scripts/isolation.py +148 -11
- package/skills/gsd-path-define/scripts/members.py +68 -25
- package/skills/gsd-path-define/scripts/model_policy.py +5 -1
- package/skills/gsd-path-define/scripts/pipeline_git.py +68 -30
- package/skills/gsd-path-define/scripts/pipeline_state.py +28 -6
- package/skills/gsd-path-define/scripts/promote_lookahead.py +3 -3
- package/skills/gsd-path-define/scripts/roadmap.py +9 -1
- package/skills/gsd-path-define/scripts/state_checkpoint.py +30 -9
- package/skills/gsd-path-define/scripts/state_promote.py +15 -4
- package/skills/gsd-path-define/templates/state.md +1 -0
- package/skills/gsd-path-discuss/references/operating-rules.md +11 -3
- package/skills/gsd-path-discuss/references/runtime-selection.md +9 -0
- package/skills/gsd-path-discuss/scripts/_common.py +107 -1
- package/skills/gsd-path-discuss/scripts/discussion_records.py +9 -1
- package/skills/gsd-path-discuss/scripts/model_policy.py +5 -1
- package/skills/gsd-path-docs-audit/references/operating-rules.md +11 -3
- package/skills/gsd-path-docs-audit/references/runtime-selection.md +9 -0
- package/skills/gsd-path-docs-audit/scripts/_common.py +107 -1
- package/skills/gsd-path-docs-audit/scripts/build_recovery.py +62 -30
- package/skills/gsd-path-docs-audit/scripts/check_handoffs.py +92 -30
- package/skills/gsd-path-docs-audit/scripts/check_task_briefs.py +19 -14
- package/skills/gsd-path-docs-audit/scripts/detect_project.py +17 -2
- package/skills/gsd-path-docs-audit/scripts/discussion_records.py +9 -1
- package/skills/gsd-path-docs-audit/scripts/discussion_validate.py +7 -6
- package/skills/gsd-path-docs-audit/scripts/integration.py +98 -64
- package/skills/gsd-path-docs-audit/scripts/isolation.py +148 -11
- package/skills/gsd-path-docs-audit/scripts/members.py +68 -25
- package/skills/gsd-path-docs-audit/scripts/model_policy.py +5 -1
- package/skills/gsd-path-docs-audit/scripts/pipeline_git.py +68 -30
- package/skills/gsd-path-docs-audit/scripts/pipeline_state.py +28 -6
- package/skills/gsd-path-docs-audit/scripts/promote_lookahead.py +3 -3
- package/skills/gsd-path-docs-audit/scripts/roadmap.py +9 -1
- package/skills/gsd-path-docs-audit/scripts/state_checkpoint.py +30 -9
- package/skills/gsd-path-docs-audit/scripts/state_promote.py +15 -4
- package/skills/gsd-path-forensics/references/operating-rules.md +11 -3
- package/skills/gsd-path-forensics/references/runtime-selection.md +9 -0
- package/skills/gsd-path-forensics/scripts/_common.py +107 -1
- package/skills/gsd-path-forensics/scripts/discussion_records.py +9 -1
- package/skills/gsd-path-forensics/scripts/model_policy.py +5 -1
- package/skills/gsd-path-forensics/scripts/pipeline_diagnose.py +17 -2
- package/skills/gsd-path-forensics/scripts/pipeline_undo.py +17 -8
- package/skills/gsd-path-inspect/references/operating-rules.md +11 -3
- package/skills/gsd-path-inspect/references/runtime-selection.md +9 -0
- package/skills/gsd-path-inspect/scripts/_common.py +107 -1
- package/skills/gsd-path-inspect/scripts/build_recovery.py +62 -30
- package/skills/gsd-path-inspect/scripts/check_handoffs.py +92 -30
- package/skills/gsd-path-inspect/scripts/check_task_briefs.py +19 -14
- package/skills/gsd-path-inspect/scripts/detect_project.py +17 -2
- package/skills/gsd-path-inspect/scripts/discussion_records.py +9 -1
- package/skills/gsd-path-inspect/scripts/discussion_validate.py +7 -6
- package/skills/gsd-path-inspect/scripts/integration.py +98 -64
- package/skills/gsd-path-inspect/scripts/isolation.py +148 -11
- package/skills/gsd-path-inspect/scripts/members.py +68 -25
- package/skills/gsd-path-inspect/scripts/model_policy.py +5 -1
- package/skills/gsd-path-inspect/scripts/pipeline_git.py +68 -30
- package/skills/gsd-path-inspect/scripts/pipeline_state.py +28 -6
- package/skills/gsd-path-inspect/scripts/promote_lookahead.py +3 -3
- package/skills/gsd-path-inspect/scripts/roadmap.py +9 -1
- package/skills/gsd-path-inspect/scripts/state_checkpoint.py +30 -9
- package/skills/gsd-path-inspect/scripts/state_promote.py +15 -4
- package/skills/gsd-path-inspect/scripts/workflow_run.py +9 -2
- package/skills/gsd-path-inspect/templates/state.md +1 -0
- package/skills/gsd-path-loop/references/operating-rules.md +11 -3
- package/skills/gsd-path-loop/references/runtime-selection.md +9 -0
- package/skills/gsd-path-loop/scripts/_common.py +107 -1
- package/skills/gsd-path-loop/scripts/discussion_records.py +9 -1
- package/skills/gsd-path-loop/scripts/model_policy.py +5 -1
- package/skills/gsd-path-migrate/SKILL.md +8 -0
- package/skills/gsd-path-migrate/references/operating-rules.md +11 -3
- package/skills/gsd-path-migrate/references/runtime-selection.md +9 -0
- package/skills/gsd-path-migrate/scripts/_common.py +107 -1
- package/skills/gsd-path-migrate/scripts/discussion_records.py +9 -1
- package/skills/gsd-path-migrate/scripts/migrate_core.py +2 -0
- package/skills/gsd-path-migrate/scripts/model_policy.py +5 -1
- package/skills/gsd-path-plan/SKILL.md +3 -0
- package/skills/gsd-path-plan/references/build-recovery.md +13 -0
- package/skills/gsd-path-plan/references/operating-rules.md +11 -3
- package/skills/gsd-path-plan/references/planner.md +13 -1
- package/skills/gsd-path-plan/references/runtime-selection.md +9 -0
- package/skills/gsd-path-plan/scripts/_common.py +107 -1
- package/skills/gsd-path-plan/scripts/build_recovery.py +62 -30
- package/skills/gsd-path-plan/scripts/check_handoffs.py +92 -30
- package/skills/gsd-path-plan/scripts/check_task_briefs.py +19 -14
- package/skills/gsd-path-plan/scripts/detect_project.py +17 -2
- package/skills/gsd-path-plan/scripts/discussion_records.py +9 -1
- package/skills/gsd-path-plan/scripts/discussion_validate.py +7 -6
- package/skills/gsd-path-plan/scripts/integration.py +98 -64
- package/skills/gsd-path-plan/scripts/isolation.py +148 -11
- package/skills/gsd-path-plan/scripts/members.py +68 -25
- package/skills/gsd-path-plan/scripts/model_policy.py +5 -1
- package/skills/gsd-path-plan/scripts/pipeline_git.py +68 -30
- package/skills/gsd-path-plan/scripts/pipeline_state.py +28 -6
- package/skills/gsd-path-plan/scripts/promote_lookahead.py +3 -3
- package/skills/gsd-path-plan/scripts/roadmap.py +9 -1
- package/skills/gsd-path-plan/scripts/state_checkpoint.py +30 -9
- package/skills/gsd-path-plan/scripts/state_promote.py +15 -4
- package/skills/gsd-path-plan/scripts/workflow_run.py +9 -2
- package/skills/gsd-path-plan/templates/task.md +1 -0
- package/skills/gsd-path-research/references/operating-rules.md +11 -3
- package/skills/gsd-path-research/references/runtime-selection.md +9 -0
- package/skills/gsd-path-research/scripts/_common.py +107 -1
- package/skills/gsd-path-research/scripts/build_recovery.py +62 -30
- package/skills/gsd-path-research/scripts/check_handoffs.py +92 -30
- package/skills/gsd-path-research/scripts/check_task_briefs.py +19 -14
- package/skills/gsd-path-research/scripts/detect_project.py +17 -2
- package/skills/gsd-path-research/scripts/discussion_records.py +9 -1
- package/skills/gsd-path-research/scripts/discussion_validate.py +7 -6
- package/skills/gsd-path-research/scripts/integration.py +98 -64
- package/skills/gsd-path-research/scripts/isolation.py +148 -11
- package/skills/gsd-path-research/scripts/members.py +68 -25
- package/skills/gsd-path-research/scripts/model_policy.py +5 -1
- package/skills/gsd-path-research/scripts/pipeline_git.py +68 -30
- package/skills/gsd-path-research/scripts/pipeline_state.py +28 -6
- package/skills/gsd-path-research/scripts/promote_lookahead.py +3 -3
- package/skills/gsd-path-research/scripts/roadmap.py +9 -1
- package/skills/gsd-path-research/scripts/state_checkpoint.py +30 -9
- package/skills/gsd-path-research/scripts/state_promote.py +15 -4
- package/skills/gsd-path-roadmap/SKILL.md +16 -6
- package/skills/gsd-path-roadmap/references/operating-rules.md +11 -3
- package/skills/gsd-path-roadmap/references/runtime-selection.md +9 -0
- package/skills/gsd-path-roadmap/scripts/_common.py +107 -1
- package/skills/gsd-path-roadmap/scripts/build_recovery.py +62 -30
- package/skills/gsd-path-roadmap/scripts/check_handoffs.py +92 -30
- package/skills/gsd-path-roadmap/scripts/check_task_briefs.py +19 -14
- package/skills/gsd-path-roadmap/scripts/detect_project.py +17 -2
- package/skills/gsd-path-roadmap/scripts/discussion_records.py +9 -1
- package/skills/gsd-path-roadmap/scripts/discussion_validate.py +7 -6
- package/skills/gsd-path-roadmap/scripts/integration.py +98 -64
- package/skills/gsd-path-roadmap/scripts/isolation.py +148 -11
- package/skills/gsd-path-roadmap/scripts/members.py +68 -25
- package/skills/gsd-path-roadmap/scripts/model_policy.py +5 -1
- package/skills/gsd-path-roadmap/scripts/pipeline_git.py +68 -30
- package/skills/gsd-path-roadmap/scripts/pipeline_state.py +28 -6
- package/skills/gsd-path-roadmap/scripts/promote_lookahead.py +3 -3
- package/skills/gsd-path-roadmap/scripts/roadmap.py +9 -1
- package/skills/gsd-path-roadmap/scripts/state_checkpoint.py +30 -9
- package/skills/gsd-path-roadmap/scripts/state_promote.py +15 -4
- package/skills/gsd-path-ship/SKILL.md +26 -4
- package/skills/gsd-path-ship/references/operating-rules.md +11 -3
- package/skills/gsd-path-ship/references/reviewer-final.md +5 -5
- package/skills/gsd-path-ship/references/reviewer-wave.md +46 -13
- package/skills/gsd-path-ship/references/runtime-selection.md +9 -0
- package/skills/gsd-path-ship/scripts/_common.py +107 -1
- package/skills/gsd-path-ship/scripts/build_recovery.py +62 -30
- package/skills/gsd-path-ship/scripts/build_state.py +195 -5
- package/skills/gsd-path-ship/scripts/check_handoffs.py +92 -30
- package/skills/gsd-path-ship/scripts/check_task_briefs.py +19 -14
- package/skills/gsd-path-ship/scripts/detect_project.py +17 -2
- package/skills/gsd-path-ship/scripts/discussion_records.py +9 -1
- package/skills/gsd-path-ship/scripts/discussion_validate.py +7 -6
- package/skills/gsd-path-ship/scripts/integration.py +98 -64
- package/skills/gsd-path-ship/scripts/isolation.py +148 -11
- package/skills/gsd-path-ship/scripts/members.py +68 -25
- package/skills/gsd-path-ship/scripts/model_policy.py +5 -1
- package/skills/gsd-path-ship/scripts/pipeline_git.py +68 -30
- package/skills/gsd-path-ship/scripts/pipeline_state.py +28 -6
- package/skills/gsd-path-ship/scripts/promote_lookahead.py +3 -3
- package/skills/gsd-path-ship/scripts/roadmap.py +9 -1
- package/skills/gsd-path-ship/scripts/state_checkpoint.py +30 -9
- package/skills/gsd-path-ship/scripts/state_promote.py +15 -4
- package/skills/gsd-path-ship/scripts/workflow_run.py +9 -2
- package/skills/gsd-path-ship/templates/final-review.md +5 -5
- package/skills/gsd-path-ship/templates/wave-review.md +18 -9
- package/skills/gsd-path-undo/references/operating-rules.md +11 -3
- package/skills/gsd-path-undo/references/runtime-selection.md +9 -0
- package/skills/gsd-path-undo/scripts/_common.py +107 -1
- package/skills/gsd-path-undo/scripts/discussion_records.py +9 -1
- package/skills/gsd-path-undo/scripts/model_policy.py +5 -1
- package/skills/gsd-path-undo/scripts/pipeline_undo.py +17 -8
- package/skills/path/DEFINE.md +6 -2
- package/skills/path/PLAN.md +3 -0
- package/skills/path/ROADMAP.md +16 -6
- package/skills/path/SHIP.md +26 -4
- package/skills/path/SKILL.md +52 -24
- package/skills/path/references/build-native.md +96 -9
- package/skills/path/references/build-recovery.md +13 -0
- package/skills/path/references/coder.md +7 -3
- package/skills/path/references/operating-rules.md +11 -3
- package/skills/path/references/planner.md +13 -1
- package/skills/path/references/reviewer-final.md +5 -5
- package/skills/path/references/reviewer-wave.md +46 -13
- package/skills/path/references/runtime-selection.md +9 -0
- package/skills/path/scripts/_common.py +107 -1
- package/skills/path/scripts/build_recovery.py +62 -30
- package/skills/path/scripts/build_state.py +195 -5
- package/skills/path/scripts/check_handoffs.py +92 -30
- package/skills/path/scripts/check_task_briefs.py +19 -14
- package/skills/path/scripts/detect_project.py +17 -2
- package/skills/path/scripts/discussion_records.py +9 -1
- package/skills/path/scripts/discussion_validate.py +7 -6
- package/skills/path/scripts/dispatch_driver.py +157 -31
- package/skills/path/scripts/git_guard.py +24 -5
- package/skills/path/scripts/integration.py +98 -64
- package/skills/path/scripts/isolation.py +148 -11
- package/skills/path/scripts/members.py +68 -25
- package/skills/path/scripts/model_policy.py +5 -1
- package/skills/path/scripts/pipeline_git.py +68 -30
- package/skills/path/scripts/pipeline_state.py +28 -6
- package/skills/path/scripts/promote_lookahead.py +3 -3
- package/skills/path/scripts/review_findings.py +180 -44
- package/skills/path/scripts/roadmap.py +9 -1
- package/skills/path/scripts/state_checkpoint.py +30 -9
- package/skills/path/scripts/state_promote.py +15 -4
- package/skills/path/scripts/task_context.py +2 -0
- package/skills/path/scripts/workflow_run.py +9 -2
- package/skills/path/templates/final-review.md +5 -5
- package/skills/path/templates/state.md +1 -0
- package/skills/path/templates/task.md +1 -0
- package/skills/path/templates/wave-review.md +18 -9
|
@@ -17,19 +17,21 @@ tasks in this wave as the rubric. For each task:
|
|
|
17
17
|
criterion. Never use the primary worktree or branch tip as task
|
|
18
18
|
evidence. Do not create or remove Git worktrees yourself.
|
|
19
19
|
4. Mark `pass` or `fail`. Before the evidence separator (` — `), copy the
|
|
20
|
-
complete task acceptance or interface criterion
|
|
21
|
-
|
|
22
|
-
|
|
20
|
+
complete task acceptance or interface criterion (see Review file format
|
|
21
|
+
below). Do not substitute an AC id or summary: finding grouping binds
|
|
22
|
+
this text to the task. Give the observed result, `file:line`, and
|
|
23
23
|
concrete fix direction for each failure.
|
|
24
24
|
|
|
25
|
+
A Log entry `orchestrator regression Verify of <task id> retired: superseded
|
|
26
|
+
by <task id>` is not evidence: judge the superseding task's acceptance
|
|
27
|
+
criteria. The same applies to the entry `orchestrator regression Verify of
|
|
28
|
+
<fix task id>: part of <task id> retired: superseded by <task id>`.
|
|
29
|
+
|
|
25
30
|
Then check each INTENT.md success criterion owned by a task in this wave
|
|
26
31
|
against that same isolated product. In each `### SCn —` heading, copy the
|
|
27
|
-
criterion text
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
only whitespace is normalized at the gate. Do not paraphrase or drop
|
|
31
|
-
sub-bullets. Mark `pass` or `fail` with evidence; a failed owned SC blocks
|
|
32
|
-
the wave. Omit the Intent coverage section when no task in the wave owns
|
|
32
|
+
complete criterion text from INTENT.md `## Success criteria` (see Review
|
|
33
|
+
file format below). Mark `pass` or `fail` with evidence; a failed owned SC
|
|
34
|
+
blocks the wave. Omit the Intent coverage section when no task in the wave owns
|
|
33
35
|
an SC. When INTENT.md `## Edge coverage` or `## Prohibitions` has rows for
|
|
34
36
|
an owned SC, judge it against those rulings too: cite each `held-out` edge
|
|
35
37
|
id with its held-out test's recorded output and each `judgment` prohibition
|
|
@@ -48,6 +50,30 @@ For cycle greater than 1, read the previous review, re-check every failure,
|
|
|
48
50
|
and regression-check affected passes. State when the same criterion fails for
|
|
49
51
|
a different reason. Use only the wave-review template.
|
|
50
52
|
|
|
53
|
+
### Review file format
|
|
54
|
+
|
|
55
|
+
`scripts/check_handoffs.py wave` rejects a review file that breaks one of
|
|
56
|
+
these rules, and `scripts/review_findings.py` cannot group a failure whose
|
|
57
|
+
criterion text does not match. Apply the rules before you write the file.
|
|
58
|
+
|
|
59
|
+
- **Section order.** Put the `## T### —` sections in task id order (T001
|
|
60
|
+
before T002). The order of the PLAN.md wave table does not apply. A `pass`
|
|
61
|
+
review has one section for each task of the wave.
|
|
62
|
+
- **Section heading.** Write `## T### — <title>: pass` or `: fail`, where
|
|
63
|
+
`<title>` is the exact `title:` value of the task file.
|
|
64
|
+
- **Evidence lines.** An evidence line starts with `- ✅ ` for pass or
|
|
65
|
+
`- ❌ ` for fail. Each task section and each `### SCn —` block needs one
|
|
66
|
+
or more lines with the marker of its own verdict. A failed task needs a
|
|
67
|
+
`- ❌ ` criterion line, also when other text describes the failure.
|
|
68
|
+
- **Criterion text.** In a task evidence line and in a `### SCn —` heading,
|
|
69
|
+
copy every word of the criterion. Join wrapped lines and sub-bullets with
|
|
70
|
+
one space. You can keep or remove the `- ` markers of the sub-bullets.
|
|
71
|
+
A changed, added, removed or moved word does not match.
|
|
72
|
+
- **Angle brackets.** Text such as `<b>` or `<path>` outside backticks reads
|
|
73
|
+
as an unfilled template placeholder and is rejected. Write literal markup
|
|
74
|
+
and code in backticks inside a sentence, for example: the output has
|
|
75
|
+
`<b>hello</b>`.
|
|
76
|
+
|
|
51
77
|
### Re-review after a proven repair
|
|
52
78
|
|
|
53
79
|
When the brief supplies a `review_findings.py repair-evidence` receipt, use
|
|
@@ -55,14 +81,21 @@ its isolated repair product for the linked original criteria, including owned
|
|
|
55
81
|
INTENT criteria. The original faulty landing and earlier verdicts remain
|
|
56
82
|
historical evidence; they do not force the later verdict to remain failed.
|
|
57
83
|
Reconstruct the receipt's repair `base` plus only its `commit^..commit` patch
|
|
58
|
-
for `files` inside the supplied sidecar.
|
|
59
|
-
|
|
84
|
+
for `files` inside the supplied sidecar. For a member repair (the receipt
|
|
85
|
+
names `member`), use `member_base` and `member_commit` in that member's
|
|
86
|
+
repository. `earlier_repairs` are linked repairs of the same findings that
|
|
87
|
+
are already in the repair base. When the receipt has `later_repairs`, linked
|
|
88
|
+
repairs changed the same files after this repair: apply their patches in
|
|
89
|
+
order too. Read the repair task's recorded
|
|
90
|
+
Verify, or with `later_repairs` that of the last one, which is the receipt's
|
|
91
|
+
`verification`; inspect the patch against the original unchanged contract, and reuse
|
|
60
92
|
settled checks. Check unaffected criteria from their prior evidence plus the
|
|
61
93
|
repair diff for regressions. Cite the receipt, repair landing and task Log in
|
|
62
94
|
the new review. A receipt proves provenance, not acceptance: judge whether the
|
|
63
95
|
repair actually satisfies the criterion. A missing or rejected receipt blocks
|
|
64
|
-
using that repair; the parent resolves it
|
|
65
|
-
|
|
96
|
+
using that repair; the parent resolves it, or briefs this review in full at
|
|
97
|
+
the current HEAD without a receipt, rather than creating another product fix
|
|
98
|
+
for the unchanged historical failure.
|
|
66
99
|
|
|
67
100
|
## Final scope in a quick full wave
|
|
68
101
|
|
|
@@ -20,6 +20,15 @@ references to bundled copies of those helpers. Other skill helpers, templates,
|
|
|
20
20
|
and role briefs remain in the installed skill bundle. Pass `-B` to Python helper
|
|
21
21
|
invocations so execution does not create bytecode in the runtime store.
|
|
22
22
|
|
|
23
|
+
The selected runtime is authoritative for each helper in its `manifest.json`;
|
|
24
|
+
this includes `review_findings.py` and `task_context.py`. A selected runtime
|
|
25
|
+
that was published before a helper became a runtime file does not list that
|
|
26
|
+
helper: run the bundled copy until the project runtime is upgraded.
|
|
27
|
+
`dispatch_driver.py` and `workflow_run.py` always run from the installed skill
|
|
28
|
+
bundle, because they read role briefs and templates from it; a skill update
|
|
29
|
+
replaces them, a runtime upgrade does not. The driver reloads the helper modules
|
|
30
|
+
that it imports from the selected runtime.
|
|
31
|
+
|
|
23
32
|
With no declaration, retain the legacy helper lookup. An existing
|
|
24
33
|
`.gsd-path/runtime/` requires the explicit `--runtime-migrate --project PATH`
|
|
25
34
|
operation before adopting external storage; review the resulting Git diff.
|
|
@@ -25,7 +25,8 @@ import signal
|
|
|
25
25
|
import subprocess
|
|
26
26
|
import time
|
|
27
27
|
from pathlib import Path
|
|
28
|
-
from typing import BinaryIO, Iterator, List, Optional, Sequence
|
|
28
|
+
from typing import BinaryIO, Iterable, Iterator, List, Optional, Sequence, Tuple
|
|
29
|
+
|
|
29
30
|
|
|
30
31
|
# Chosen by what imports, not os.name, so tests can emulate Windows on POSIX.
|
|
31
32
|
try:
|
|
@@ -161,6 +162,33 @@ def project_ignore_error(repo: Path) -> Optional[str]:
|
|
|
161
162
|
+ ". Anchor the product rule (for example `/build/`) in a task that owns it.")
|
|
162
163
|
|
|
163
164
|
|
|
165
|
+
_DEFAULT_BRANCH_RE = re.compile(
|
|
166
|
+
r"[^\s#\"'\\~^:?*\[\x00-\x1f\x7f-][^\s#\"'\\~^:?*\[\x00-\x1f\x7f]*"
|
|
167
|
+
)
|
|
168
|
+
_RESERVED_BRANCH_PREFIXES = ("gsd-path/", "gsd-path-integrate/", "gsd-path-task/")
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def valid_default_branch(name: str) -> bool:
|
|
172
|
+
"""A git branch name that STATE frontmatter can hold and Path does not reserve."""
|
|
173
|
+
return (
|
|
174
|
+
bool(_DEFAULT_BRANCH_RE.fullmatch(name))
|
|
175
|
+
and name != "null"
|
|
176
|
+
and not name.startswith(_RESERVED_BRANCH_PREFIXES)
|
|
177
|
+
and not name.endswith(("/", ".", ".lock"))
|
|
178
|
+
and not any(part in name for part in ("..", "//", "/.", "@{"))
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def origin_default_branch(repo: Path) -> str:
|
|
183
|
+
"""The origin default branch that initialization records; an unset origin/HEAD means main."""
|
|
184
|
+
result = run_git(repo, "symbolic-ref", "--quiet", "refs/remotes/origin/HEAD")
|
|
185
|
+
prefix = "refs/remotes/origin/"
|
|
186
|
+
remote_ref = result.stdout.strip()
|
|
187
|
+
if result.returncode != 0 or not remote_ref.startswith(prefix):
|
|
188
|
+
return "main"
|
|
189
|
+
return remote_ref.removeprefix(prefix)
|
|
190
|
+
|
|
191
|
+
|
|
164
192
|
def section_body(text: str, heading: str) -> Optional[str]:
|
|
165
193
|
"""The text under `## <heading>` up to the next `## `, or None when absent."""
|
|
166
194
|
match = re.search(
|
|
@@ -169,6 +197,79 @@ def section_body(text: str, heading: str) -> Optional[str]:
|
|
|
169
197
|
return match.group("body") if match else None
|
|
170
198
|
|
|
171
199
|
|
|
200
|
+
def overlapping_paths(left: Sequence[str], right: Sequence[str]) -> List[str]:
|
|
201
|
+
"""Return the sorted paths that two task `files` lists both cover.
|
|
202
|
+
|
|
203
|
+
An entry covers itself and the paths below it. For a declared directory
|
|
204
|
+
and a path below it, the result names the lower path. The callers have no
|
|
205
|
+
task base, so this does not ask if an entry is a file: the task-brief
|
|
206
|
+
lint rejects a path below a file at the base.
|
|
207
|
+
"""
|
|
208
|
+
return sorted(
|
|
209
|
+
{
|
|
210
|
+
max(one, other, key=len)
|
|
211
|
+
for one in left
|
|
212
|
+
for other in right
|
|
213
|
+
if one == other or one.startswith(other + "/") or other.startswith(one + "/")
|
|
214
|
+
}
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
# The `- ` of an indented sub-bullet line. A line that would then read as a
|
|
219
|
+
# numbered item (`- 2. text`) keeps its marker, so both readings of a source
|
|
220
|
+
# have the same criteria.
|
|
221
|
+
SUB_BULLET_MARKER = re.compile(r"(?m)^([ \t]+)-[ \t]+(?![ \t]*\d+\.\s)")
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def without_sub_bullet_markers(source: str) -> str:
|
|
225
|
+
"""`source` with the `- ` marker of each indented sub-bullet line removed."""
|
|
226
|
+
return SUB_BULLET_MARKER.sub(r"\1", source)
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
class Criterion(str):
|
|
230
|
+
"""A criterion as its parser joins it, with the `- ` markers of its sub-bullet lines.
|
|
231
|
+
|
|
232
|
+
`bare` is the same criterion parsed from `without_sub_bullet_markers(source)`.
|
|
233
|
+
The joined text cannot tell a marker from a minus sign; the source lines can.
|
|
234
|
+
"""
|
|
235
|
+
|
|
236
|
+
bare: str
|
|
237
|
+
|
|
238
|
+
def __new__(cls, text: str, bare: str) -> "Criterion":
|
|
239
|
+
criterion = super().__new__(cls, text)
|
|
240
|
+
criterion.bare = bare
|
|
241
|
+
return criterion
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def quotes_criterion(quote: str, criterion: str) -> bool:
|
|
245
|
+
"""Whether a review quote is `criterion`, for every reader.
|
|
246
|
+
|
|
247
|
+
Only whitespace is collapsed. The quote must equal the criterion with all
|
|
248
|
+
the markers of its sub-bullet lines or with none of them; every other
|
|
249
|
+
character, a hyphen inside a line too, is compared.
|
|
250
|
+
"""
|
|
251
|
+
forms = (criterion, getattr(criterion, "bare", criterion))
|
|
252
|
+
return " ".join(quote.split()) in {" ".join(form.split()) for form in forms}
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def quoted_criterion(item: str, criteria: Sequence[str]) -> Optional[Tuple[int, int]]:
|
|
256
|
+
"""Index of the criterion quoted at the start of `item`, and where the quote ends.
|
|
257
|
+
|
|
258
|
+
A criterion can contain ` — ` itself, so each separator and the end of
|
|
259
|
+
`item` is a candidate, the longest quote first. A criterion as written
|
|
260
|
+
wins over any criterion read without its sub-bullet markers.
|
|
261
|
+
"""
|
|
262
|
+
ends = [match.start() for match in re.finditer("(?= —(?: |$))|$", item)][::-1]
|
|
263
|
+
for bare in (False, True):
|
|
264
|
+
for end in ends:
|
|
265
|
+
quote = " ".join(item[:end].split())
|
|
266
|
+
for index, criterion in enumerate(criteria):
|
|
267
|
+
form = getattr(criterion, "bare", criterion) if bare else criterion
|
|
268
|
+
if quote == " ".join(form.split()):
|
|
269
|
+
return index, end
|
|
270
|
+
return None
|
|
271
|
+
|
|
272
|
+
|
|
172
273
|
def task_verify_command(task_text: str) -> str:
|
|
173
274
|
"""The Verify shell text, excluding the closing fence's separator newline."""
|
|
174
275
|
body = section_body(task_text, "Verify")
|
|
@@ -176,6 +277,11 @@ def task_verify_command(task_text: str) -> str:
|
|
|
176
277
|
return block.group("block").removesuffix("\n") if block else ""
|
|
177
278
|
|
|
178
279
|
|
|
280
|
+
def fix_verify(commands: Iterable[str]) -> str:
|
|
281
|
+
"""The Verify of a fix task: each distinct source Verify in its own subshell, under `set -e`."""
|
|
282
|
+
return "set -e\n" + "\n".join(f"(\n{command}\n)" for command in dict.fromkeys(commands))
|
|
283
|
+
|
|
284
|
+
|
|
179
285
|
def latest_verify_entry(entries: list, command: str, commit: str,
|
|
180
286
|
repo: Optional[str] = None) -> Optional[dict]:
|
|
181
287
|
"""Rows are keyed by (command, repo, commit); a row without `repo` is a coordinator row."""
|
|
@@ -22,6 +22,10 @@ MARKER = "build recovery: "
|
|
|
22
22
|
RECOVERY_KEYS = frozenset({"base", "branch", "kind", "source"})
|
|
23
23
|
|
|
24
24
|
|
|
25
|
+
# Only the value of a cap line is exempt; a comment or a line separator after it stays in the compared text.
|
|
26
|
+
CAP_VALUE = re.compile(r"(?m)^(-[ \t]*(?:max_review_cycles|wave_budget):)[ \t\w.,$/:]*")
|
|
27
|
+
|
|
28
|
+
|
|
25
29
|
def runtime():
|
|
26
30
|
if __package__:
|
|
27
31
|
from scripts import pipeline_state
|
|
@@ -58,8 +62,17 @@ def _parse_recovery_record(value: object) -> dict:
|
|
|
58
62
|
return {**value, "source": source}
|
|
59
63
|
|
|
60
64
|
|
|
61
|
-
def _recovery_base_proves_milestone(before, current, source: str) -> bool:
|
|
62
|
-
if before.branch != current.branch
|
|
65
|
+
def _recovery_base_proves_milestone(before, current, source: str, active: bool = True) -> bool:
|
|
66
|
+
if before.branch != current.branch:
|
|
67
|
+
return False
|
|
68
|
+
if before.milestone != current.milestone and (
|
|
69
|
+
# Issue #374: a legacy base records milestone null when define never
|
|
70
|
+
# named it. The owner names it at ship, after the build resumed, so
|
|
71
|
+
# only a closed build recovery accepts an unset base milestone; the
|
|
72
|
+
# bound branch already encodes M00N. A milestone named by the base
|
|
73
|
+
# must still equal the current one.
|
|
74
|
+
active or source != "build" or before.milestone is not None
|
|
75
|
+
):
|
|
63
76
|
return False
|
|
64
77
|
if source == "plan":
|
|
65
78
|
return (before.phase, before.status) in {("plan", "active"), ("plan", "blocked")}
|
|
@@ -99,7 +112,9 @@ def context(repo: Path, text: str | None = None) -> dict | None:
|
|
|
99
112
|
if base_text is None:
|
|
100
113
|
raise state.PipelineStateError("build recovery base has no STATE.md")
|
|
101
114
|
before = state._state_from_text(base_text)
|
|
102
|
-
if not _recovery_base_proves_milestone(
|
|
115
|
+
if not _recovery_base_proves_milestone(
|
|
116
|
+
before, current, recovery.get("source", "build"), recovery["active"]
|
|
117
|
+
):
|
|
103
118
|
raise state.PipelineStateError("build recovery base does not prove this blocked milestone")
|
|
104
119
|
return recovery
|
|
105
120
|
|
|
@@ -289,61 +304,78 @@ def validate_plan(repo: Path) -> None:
|
|
|
289
304
|
intent = checkpoint_runtime()._git_text_at(repo, recovery["base"], ".project/intent/INTENT.md")
|
|
290
305
|
if state._read_real_file(repo / ".project/intent/INTENT.md", "INTENT.md") != intent:
|
|
291
306
|
raise state.PipelineStateError("plan repair cannot change approved intent")
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
match = re.match(r"wave-([1-9]\d*)[.]", path.name)
|
|
295
|
-
if match and int(match[1]) not in unchanged:
|
|
296
|
-
raise state.PipelineStateError("changed wave must be reviewed after build resumes")
|
|
307
|
+
if stale_review_reasons(repo, recovery, _review_waves(repo / ".project/review")):
|
|
308
|
+
raise state.PipelineStateError("changed wave must be reviewed after build resumes")
|
|
297
309
|
|
|
298
310
|
|
|
299
|
-
def
|
|
300
|
-
|
|
311
|
+
def _review_waves(directory: Path) -> set[int]:
|
|
312
|
+
return {int(match[1]) for path in directory.iterdir()
|
|
313
|
+
if (match := re.match(r"wave-([1-9]\d*)[.]", path.name))}
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
def stale_review_reasons(repo: Path, recovery: dict, waves: set[int]) -> dict[int, str]:
|
|
317
|
+
"""Say why a wave's preserved review is stale; a wave not returned has matching intent, plan, and task bytes."""
|
|
301
318
|
state = runtime()
|
|
302
319
|
project = repo / ".project"
|
|
303
320
|
old_intent = checkpoint_runtime()._git_text_at(repo, recovery["base"], ".project/intent/INTENT.md")
|
|
304
321
|
if state._read_real_file(project / "intent/INTENT.md", "INTENT.md") != old_intent:
|
|
305
|
-
return
|
|
322
|
+
return dict.fromkeys(waves, "INTENT.md changed")
|
|
306
323
|
old_plan = checkpoint_runtime()._git_text_at(repo, recovery["base"], ".project/plan/PLAN.md") or ""
|
|
307
324
|
new_plan = state._read_real_file(project / "plan/PLAN.md", "PLAN.md")
|
|
325
|
+
panel_lines = r"(?m)^-[ \t]*review_panel:.*$"
|
|
326
|
+
if re.findall(panel_lines, old_plan) != re.findall(panel_lines, new_plan):
|
|
327
|
+
return dict.fromkeys(waves, "PLAN.md `review_panel` setting changed")
|
|
308
328
|
|
|
309
329
|
def sections(text):
|
|
310
330
|
return {match[1]: match[2] for match in re.finditer(
|
|
311
331
|
r"(?ms)^## ([^\n]+)\n(.*?)(?=^## |\Z)", text,
|
|
312
332
|
)}
|
|
313
333
|
|
|
334
|
+
def governing(plan):
|
|
335
|
+
# Non-wave settings govern every wave, including coverage and surfaces. Caps only bound
|
|
336
|
+
# later work and Dependency notes are prose, so neither changes what a passed review judged.
|
|
337
|
+
return {title: CAP_VALUE.sub(r"\1", body) if title == "Config" else body
|
|
338
|
+
for title, body in plan.items()
|
|
339
|
+
if not title.startswith("Wave ") and title != "Dependency notes"}
|
|
340
|
+
|
|
314
341
|
old_sections, new_sections = sections(old_plan), sections(new_plan)
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
342
|
+
old_governing, new_governing = governing(old_sections), governing(new_sections)
|
|
343
|
+
for title in sorted(old_governing.keys() | new_governing.keys()):
|
|
344
|
+
if old_governing.get(title) != new_governing.get(title):
|
|
345
|
+
return dict.fromkeys(waves, f"PLAN.md section `{title}` changed")
|
|
319
346
|
old_tasks = checkpoint_runtime()._task_contracts_at(repo, recovery["base"], ".project/tasks")
|
|
320
347
|
current = settled_tasks(repo)
|
|
321
|
-
|
|
322
|
-
for
|
|
323
|
-
|
|
324
|
-
if not
|
|
348
|
+
reasons = {}
|
|
349
|
+
for wave in waves:
|
|
350
|
+
section = {title: body for title, body in new_sections.items() if re.match(rf"Wave {wave}\b", title)}
|
|
351
|
+
if not section or any(old_sections.get(title) != body for title, body in section.items()):
|
|
352
|
+
reasons[wave] = f"PLAN.md section `Wave {wave}` changed"
|
|
325
353
|
continue
|
|
326
|
-
wave = match[1]
|
|
327
354
|
before = {Path(path).name: text for path, text in old_tasks.values()
|
|
328
|
-
if task_fields(text, path).get("wave") == wave}
|
|
329
|
-
after = {name: text for name, (text, fields) in current.items() if fields.get("wave") == wave}
|
|
330
|
-
if before
|
|
331
|
-
|
|
332
|
-
return
|
|
355
|
+
if task_fields(text, path).get("wave") == str(wave)}
|
|
356
|
+
after = {name: text for name, (text, fields) in current.items() if fields.get("wave") == str(wave)}
|
|
357
|
+
if not before or before != after:
|
|
358
|
+
reasons[wave] = f"wave {wave} task contracts changed"
|
|
359
|
+
return reasons
|
|
333
360
|
|
|
334
361
|
|
|
335
|
-
def restore_unchanged_reviews(repo: Path) -> None:
|
|
362
|
+
def restore_unchanged_reviews(repo: Path) -> dict | None:
|
|
363
|
+
"""Restore reusable wave reviews and report every preserved wave, so a dropped review is visible at approval."""
|
|
336
364
|
recovery = context(repo)
|
|
337
365
|
if not recovery or not recovery["active"]:
|
|
338
|
-
return
|
|
339
|
-
|
|
340
|
-
|
|
366
|
+
return None
|
|
367
|
+
backup = review_backup(repo, recovery)
|
|
368
|
+
waves = _review_waves(backup)
|
|
369
|
+
reasons = stale_review_reasons(repo, recovery, waves)
|
|
370
|
+
for path in backup.iterdir():
|
|
341
371
|
match = re.match(r"wave-([1-9]\d*)[.]", path.name)
|
|
342
|
-
if match and int(match[1]) in
|
|
372
|
+
if match and int(match[1]) not in reasons:
|
|
343
373
|
destination = repo / ".project/review" / path.name
|
|
344
374
|
content = path.read_text(encoding="utf-8")
|
|
345
375
|
if not destination.exists() or runtime()._read_real_file(destination, destination.name) != content:
|
|
346
376
|
runtime()._atomic_write(destination, content)
|
|
377
|
+
return {"restored": sorted(waves - reasons.keys()),
|
|
378
|
+
"review_again": [{"wave": wave, "reason": reasons[wave]} for wave in sorted(reasons)]}
|
|
347
379
|
|
|
348
380
|
|
|
349
381
|
def inventory_checkpoint(repo: Path, head: str) -> bool:
|
|
@@ -94,6 +94,8 @@ class Task:
|
|
|
94
94
|
verify_heavy: bool
|
|
95
95
|
repo: str = ""
|
|
96
96
|
review_findings: bool = False
|
|
97
|
+
verify: str = ""
|
|
98
|
+
supersedes: Tuple[str, ...] = ()
|
|
97
99
|
|
|
98
100
|
|
|
99
101
|
@dataclass(frozen=True)
|
|
@@ -248,6 +250,8 @@ def _parse_task(path: Path, relative_path: str, task_file: Optional[str] = None)
|
|
|
248
250
|
verify_heavy=_verify_heavy(text, label),
|
|
249
251
|
repo=str(fields.get("repo") or ""),
|
|
250
252
|
review_findings=REVIEW_FINDINGS_HEADING in _sections(text),
|
|
253
|
+
verify=_common.task_verify_command(text),
|
|
254
|
+
supersedes=_normalize_list(fields.get("supersedes", []), f"{label} supersedes", task_ids=True),
|
|
251
255
|
)
|
|
252
256
|
|
|
253
257
|
|
|
@@ -420,7 +424,8 @@ def _recovery_reports(project: Project) -> Dict[str, Mapping[str, object]]:
|
|
|
420
424
|
return reports
|
|
421
425
|
|
|
422
426
|
|
|
423
|
-
def _validate_ready_metadata(project: Project) ->
|
|
427
|
+
def _validate_ready_metadata(project: Project) -> Dict[str, Mapping[str, object]]:
|
|
428
|
+
"""Validate every task against its recovery report; return the reports by task id."""
|
|
424
429
|
by_id = {task.task_id: task for task in project.tasks}
|
|
425
430
|
recovery = _recovery_reports(project)
|
|
426
431
|
seen_worktrees: Dict[str, str] = {}
|
|
@@ -484,12 +489,155 @@ def _validate_ready_metadata(project: Project) -> None:
|
|
|
484
489
|
if report.get("verdict") not in LANDED_VERDICTS:
|
|
485
490
|
reason = report.get("reason", "landing commit is not proven")
|
|
486
491
|
_invalid_state(task, str(reason))
|
|
492
|
+
return recovery
|
|
487
493
|
|
|
488
494
|
|
|
489
495
|
def _overlap(left: Task, right: Task) -> List[str]:
|
|
490
496
|
if left.repo != right.repo:
|
|
491
497
|
return []
|
|
492
|
-
return
|
|
498
|
+
return _common.overlapping_paths(left.files, right.files)
|
|
499
|
+
|
|
500
|
+
|
|
501
|
+
def _superseded(tasks: Sequence[Task], task: Task) -> Dict[str, str]:
|
|
502
|
+
"""Earlier task id -> the task that replaces its behavior: this task, or a `done` task."""
|
|
503
|
+
|
|
504
|
+
return {
|
|
505
|
+
earlier: other.task_id
|
|
506
|
+
for other in tasks
|
|
507
|
+
if other.status == "done" or other.task_id == task.task_id
|
|
508
|
+
for earlier in other.supersedes
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
|
|
512
|
+
def _held(by_id: Mapping[str, Task], task: Task) -> List[Task]:
|
|
513
|
+
"""The tasks whose protective role this task inherits: each task it supersedes, through a chain."""
|
|
514
|
+
|
|
515
|
+
held: Dict[str, Task] = {}
|
|
516
|
+
pending = list(task.supersedes)
|
|
517
|
+
while pending:
|
|
518
|
+
other = by_id.get(pending.pop())
|
|
519
|
+
if other is not None and other.task_id not in held: # a cycle ends here
|
|
520
|
+
held[other.task_id] = other
|
|
521
|
+
pending.extend(other.supersedes)
|
|
522
|
+
return [by_id[name] for name in sorted(held)]
|
|
523
|
+
|
|
524
|
+
|
|
525
|
+
def _live_verify(
|
|
526
|
+
by_id: Mapping[str, Task], superseded: Mapping[str, str], source: Task
|
|
527
|
+
) -> Tuple[str, Optional[str], List[Tuple[str, str]]]:
|
|
528
|
+
"""The Verify of a source that still runs, or the task that retired all of it, and the drops.
|
|
529
|
+
|
|
530
|
+
The Verify of a fix task is exactly the composition that fix-tasks generates from the
|
|
531
|
+
Verify of each task in its deps, so it carries a copy of each: only the copies that are
|
|
532
|
+
not retired stay in the command. Any other Verify is one command. Each drop is
|
|
533
|
+
(superseded task, superseding task); a copy whose command a live dependency still runs
|
|
534
|
+
is not a drop.
|
|
535
|
+
"""
|
|
536
|
+
|
|
537
|
+
if source.task_id in superseded:
|
|
538
|
+
return "", superseded[source.task_id], [(source.task_id, superseded[source.task_id])]
|
|
539
|
+
deps = [by_id[dependency] for dependency in source.deps if dependency in by_id]
|
|
540
|
+
if not deps or source.verify != _common.fix_verify(dep.verify for dep in deps):
|
|
541
|
+
return source.verify, None, []
|
|
542
|
+
parts = [_live_verify(by_id, superseded, dep) for dep in deps]
|
|
543
|
+
live = [command for command, retired_by, _ in parts if not retired_by]
|
|
544
|
+
dropped = list(dict.fromkeys(drop for _, _, drops in parts for drop in drops
|
|
545
|
+
if by_id[drop[0]].verify not in live))
|
|
546
|
+
if not live:
|
|
547
|
+
return "", parts[0][1], dropped
|
|
548
|
+
return _common.fix_verify(live), None, dropped
|
|
549
|
+
|
|
550
|
+
|
|
551
|
+
def _regression_sources(
|
|
552
|
+
tasks: Sequence[Task], task: Task
|
|
553
|
+
) -> List[Tuple[Task, List[str], Optional[str], str, List[Tuple[str, str]]]]:
|
|
554
|
+
"""Each `done` task whose Verify protects a file of this task:
|
|
555
|
+
(source, replaced, retired_by, command, dropped).
|
|
556
|
+
|
|
557
|
+
A source is a task of an earlier wave that shares a declared file with this task. A task
|
|
558
|
+
that supersedes others inherits their role: it is also a source, in any wave, for a task
|
|
559
|
+
that shares a file with a task it holds, and `replaced` names those tasks.
|
|
560
|
+
`retired_by` names the task that supersedes the source: this task, or a `done` task. A
|
|
561
|
+
retired Verify does not run, also not as a copy in the Verify of a fix task: `command`
|
|
562
|
+
is the Verify that runs and `dropped` names each retired copy (`_live_verify`). The source still counts for the one-at-a-time
|
|
563
|
+
rule, so the superseding task never runs beside a task that shares a file of the
|
|
564
|
+
superseded task.
|
|
565
|
+
"""
|
|
566
|
+
|
|
567
|
+
by_id = {other.task_id: other for other in tasks}
|
|
568
|
+
superseded = _superseded(tasks, task)
|
|
569
|
+
sources = []
|
|
570
|
+
for other in tasks:
|
|
571
|
+
if other.status != "done" or other.task_id == task.task_id:
|
|
572
|
+
continue
|
|
573
|
+
replaced = [held.task_id for held in _held(by_id, other) if _overlap(task, held)]
|
|
574
|
+
if replaced or (other.wave < task.wave and _overlap(task, other)):
|
|
575
|
+
command, retired_by, dropped = _live_verify(by_id, superseded, other)
|
|
576
|
+
sources.append((other, replaced, retired_by, command, dropped))
|
|
577
|
+
return sources
|
|
578
|
+
|
|
579
|
+
|
|
580
|
+
def _landing_tasks(repo: Path, task_id: str, project_dir: str) -> Tuple[List[Task], Task]:
|
|
581
|
+
tasks = [
|
|
582
|
+
_parse_task(path, path.relative_to(repo).as_posix())
|
|
583
|
+
for path in sorted((repo / project_dir / "tasks").glob("*.md"))
|
|
584
|
+
]
|
|
585
|
+
task = next((item for item in tasks if item.task_id == task_id), None)
|
|
586
|
+
if task is None:
|
|
587
|
+
raise BuildStateError("unknown-task", f"task file is missing: {task_id}")
|
|
588
|
+
return tasks, task
|
|
589
|
+
|
|
590
|
+
|
|
591
|
+
def retired_verifies(
|
|
592
|
+
repo: Path, task_id: str, project_dir: str = DEFAULT_PROJECT_DIR
|
|
593
|
+
) -> List[Dict[str, str]]:
|
|
594
|
+
"""Return the earlier-wave tasks whose Verify a landing of this task does not run again.
|
|
595
|
+
|
|
596
|
+
A task's plan-approved `supersedes` names them. The Verify is retired for that task, and
|
|
597
|
+
for every task that lands after that task is `done`: the Verify of the superseding task
|
|
598
|
+
then runs in its place (`regression_verifies`, key `supersedes`). A fix task whose
|
|
599
|
+
generated Verify holds only retired copies is here too. A fix task whose generated Verify
|
|
600
|
+
loses some of its copies has one entry for each, with the superseded task under `part`.
|
|
601
|
+
"""
|
|
602
|
+
|
|
603
|
+
tasks, task = _landing_tasks(repo, task_id, project_dir)
|
|
604
|
+
retired: List[Dict[str, str]] = []
|
|
605
|
+
for other, _, retired_by, _, dropped in _regression_sources(tasks, task):
|
|
606
|
+
if retired_by:
|
|
607
|
+
if other.verify:
|
|
608
|
+
retired.append({"task": other.task_id, "superseded_by": retired_by})
|
|
609
|
+
else:
|
|
610
|
+
retired.extend({"task": other.task_id, "part": part, "superseded_by": by}
|
|
611
|
+
for part, by in dropped)
|
|
612
|
+
return retired
|
|
613
|
+
|
|
614
|
+
|
|
615
|
+
def regression_verifies(
|
|
616
|
+
repo: Path, task_id: str, own: str, project_dir: str = DEFAULT_PROJECT_DIR
|
|
617
|
+
) -> List[Dict[str, object]]:
|
|
618
|
+
"""Return the Verify commands of landed tasks that a landing of this task must also pass.
|
|
619
|
+
|
|
620
|
+
One entry per distinct command, with every source task that has it, and under
|
|
621
|
+
`supersedes` the tasks whose retired Verify it replaces for this landing. A command
|
|
622
|
+
that the task's own Verify already runs is left out: an identical command, or
|
|
623
|
+
a dependency's command when the own Verify is exactly the fix-task Verify of
|
|
624
|
+
the task's dependencies. Text that only contains a command proves nothing.
|
|
625
|
+
A retired Verify is not here, also not as a copy in the Verify of a fix task: see
|
|
626
|
+
`retired_verifies`.
|
|
627
|
+
"""
|
|
628
|
+
|
|
629
|
+
tasks, task = _landing_tasks(repo, task_id, project_dir)
|
|
630
|
+
by_id = {item.task_id: item for item in tasks}
|
|
631
|
+
composed = [by_id[dependency].verify for dependency in task.deps if dependency in by_id]
|
|
632
|
+
covered = {own, *(composed if own == _common.fix_verify(composed) else ())}
|
|
633
|
+
sources: Dict[str, Dict[str, object]] = {}
|
|
634
|
+
for other, replaced, _, command, _ in _regression_sources(tasks, task):
|
|
635
|
+
if command and command not in covered:
|
|
636
|
+
entry = sources.setdefault(command, {"tasks": [], "command": command})
|
|
637
|
+
entry["tasks"].append(other.task_id)
|
|
638
|
+
if replaced: # this Verify runs in the place of the Verify of each task it replaced
|
|
639
|
+
entry["supersedes"] = sorted({*entry.get("supersedes", ()), *replaced})
|
|
640
|
+
return list(sources.values())
|
|
493
641
|
|
|
494
642
|
|
|
495
643
|
def _blocked_wave_reviews(project: Project) -> Dict[int, int]:
|
|
@@ -522,12 +670,31 @@ def _blocked_wave_reviews(project: Project) -> Dict[int, int]:
|
|
|
522
670
|
}
|
|
523
671
|
|
|
524
672
|
|
|
673
|
+
def _unreviewed_waves(project: Project, waves: Sequence[int]) -> List[int]:
|
|
674
|
+
"""Return the given waves that still need a canonical review file of their own.
|
|
675
|
+
|
|
676
|
+
A repair wave is left out: the next review cycle of its source wave judges it.
|
|
677
|
+
"""
|
|
678
|
+
|
|
679
|
+
review_dir = project.tasks_dir.parent / "review"
|
|
680
|
+
reviewed = {
|
|
681
|
+
wave for wave in waves
|
|
682
|
+
if all(task.review_findings for task in project.tasks if task.wave == wave)
|
|
683
|
+
}
|
|
684
|
+
if review_dir.is_dir() and not review_dir.is_symlink():
|
|
685
|
+
for path in review_dir.iterdir():
|
|
686
|
+
named = check_handoffs.WAVE_REVIEW_NAME.fullmatch(path.name)
|
|
687
|
+
if named is not None and path.is_file() and not path.is_symlink():
|
|
688
|
+
reviewed.add(int(named.group("wave")))
|
|
689
|
+
return [wave for wave in waves if wave not in reviewed]
|
|
690
|
+
|
|
691
|
+
|
|
525
692
|
def ready(repo: str, project_dir: str = DEFAULT_PROJECT_DIR, *,
|
|
526
693
|
allow_done: bool = False) -> Dict[str, object]:
|
|
527
694
|
"""Return task readiness, optionally accepting build/done for completion checks."""
|
|
528
695
|
|
|
529
696
|
project = _load_project(repo, project_dir, ("active", "done") if allow_done else ("active",))
|
|
530
|
-
_validate_ready_metadata(project)
|
|
697
|
+
reports = _validate_ready_metadata(project)
|
|
531
698
|
by_id = {task.task_id: task for task in project.tasks}
|
|
532
699
|
unfinished_waves = sorted({task.wave for task in project.tasks if task.status != "done"})
|
|
533
700
|
current_wave = unfinished_waves[0] if unfinished_waves else None
|
|
@@ -626,11 +793,31 @@ def ready(repo: str, project_dir: str = DEFAULT_PROJECT_DIR, *,
|
|
|
626
793
|
{"wave": current_wave, "tasks": stalled},
|
|
627
794
|
)
|
|
628
795
|
|
|
796
|
+
# Tasks that share a regression source land one at a time, each from the landing of the one
|
|
797
|
+
# before, so the source's Verify runs on their combined edits. A parallel round leaves the
|
|
798
|
+
# primary task file pending: a retained isolate shows that the task is already dispatched.
|
|
799
|
+
# A retired source still counts: its superseding task holds its role.
|
|
800
|
+
sources = {task.task_id: {other.task_id for other, _, _, _, _ in _regression_sources(project.tasks, task)}
|
|
801
|
+
for task in concurrent}
|
|
802
|
+
started = [task for task in selectable if reports[task.task_id].get("verdict") != "none"]
|
|
803
|
+
claimed = {source for task in active + started for source in sources[task.task_id]}
|
|
804
|
+
dispatchable = []
|
|
805
|
+
for task in selectable:
|
|
806
|
+
if task in started or not sources[task.task_id] & claimed:
|
|
807
|
+
dispatchable.append(task)
|
|
808
|
+
claimed |= sources[task.task_id]
|
|
809
|
+
|
|
629
810
|
typed: Dict[str, object] = {}
|
|
630
811
|
if repair_for_wave is not None:
|
|
631
812
|
typed["repair_for_wave"] = repair_for_wave
|
|
632
813
|
if blocked_review is not None:
|
|
633
814
|
typed["blocked_review"] = blocked_review
|
|
815
|
+
# Informational: every wave below the lowest unfinished wave is all done.
|
|
816
|
+
unreviewed = _unreviewed_waves(project, sorted(
|
|
817
|
+
{task.wave for task in project.tasks if unfinished_waves and task.wave < unfinished_waves[0]}
|
|
818
|
+
))
|
|
819
|
+
if unreviewed:
|
|
820
|
+
typed["unreviewed_waves"] = unreviewed
|
|
634
821
|
return {
|
|
635
822
|
"command": "ready",
|
|
636
823
|
"branch": project.branch,
|
|
@@ -646,10 +833,13 @@ def ready(repo: str, project_dir: str = DEFAULT_PROJECT_DIR, *,
|
|
|
646
833
|
"deps": list(task.deps),
|
|
647
834
|
"files": list(task.files),
|
|
648
835
|
"task_file": task.task_file,
|
|
649
|
-
|
|
836
|
+
# The landing also runs each regression Verify, so a heavy one makes the task heavy.
|
|
837
|
+
"verify_heavy": task.verify_heavy
|
|
838
|
+
or any(other.verify_heavy and not retired_by
|
|
839
|
+
for other, _, retired_by, _, _ in _regression_sources(project.tasks, task)),
|
|
650
840
|
**({"repo": task.repo} if task.repo else {}),
|
|
651
841
|
}
|
|
652
|
-
for task in
|
|
842
|
+
for task in dispatchable
|
|
653
843
|
],
|
|
654
844
|
}
|
|
655
845
|
|