@opengsd/gsd-path 1.4.0 → 1.5.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 +325 -344
- package/DOCS.md +12 -2
- package/FULL.md +3 -2
- package/HOOKS.md +95 -8
- package/QUICK.md +1 -1
- package/README.md +78 -110
- package/RUNTIME.md +1 -1
- package/UPDATE.md +4 -3
- package/WORKFLOW.md +22 -1
- package/package.json +5 -2
- package/platforms/shared-agents/dispatch.md +13 -0
- package/scripts/_common.py +149 -1
- package/scripts/archive_milestone.py +14 -13
- package/scripts/build_recovery.py +96 -31
- package/scripts/build_state.py +79 -3
- package/scripts/check_docs_audit.py +77 -4
- package/scripts/check_handoffs.py +546 -30
- package/scripts/check_task_briefs.py +279 -23
- package/scripts/detect_project.py +243 -18
- package/scripts/discussion_validate.py +86 -29
- package/scripts/dispatch_driver.py +392 -22
- package/scripts/git_guard.py +3 -3
- package/scripts/guard_hook.py +1416 -165
- package/scripts/install.mjs +23 -8
- package/scripts/install.py +49 -9
- package/scripts/integration.py +146 -1
- package/scripts/isolation.py +392 -60
- package/scripts/loop_run.py +16 -4
- package/scripts/members.py +11 -9
- package/scripts/path_config.py +3 -3
- package/scripts/pipeline_git.py +63 -12
- package/scripts/pipeline_state.py +111 -33
- package/scripts/pipeline_undo.py +101 -8
- package/scripts/runtime_store.py +20 -2
- package/scripts/skill-resources.json +9 -2
- package/scripts/state_checkpoint.py +9 -27
- package/scripts/status_runtime.py +22 -4
- package/scripts/task_context.py +12 -3
- package/scripts/wizard.mjs +1 -0
- package/scripts/workflow_run.py +5 -2
- package/scripts/worktree_paths.py +54 -2
- package/skills/gsd-path/DEFINE.md +28 -3
- package/skills/gsd-path/PLAN.md +11 -3
- package/skills/gsd-path/RESEARCH.md +11 -0
- package/skills/gsd-path/SHIP.md +6 -1
- package/skills/gsd-path/references/build-native.md +16 -1
- package/skills/gsd-path/references/dispatch.md +13 -0
- package/skills/gsd-path/references/docs-auditor.md +5 -0
- package/skills/gsd-path/references/planner.md +14 -4
- package/skills/gsd-path/references/reviewer-final.md +20 -2
- package/skills/gsd-path/references/reviewer-wave.md +15 -4
- package/skills/gsd-path/references/reviewer.md +3 -1
- package/skills/gsd-path/references/spec-probes.md +150 -0
- package/skills/gsd-path/scripts/_common.py +149 -1
- package/skills/gsd-path/scripts/archive_milestone.py +14 -13
- package/skills/gsd-path/scripts/build_recovery.py +96 -31
- package/skills/gsd-path/scripts/build_state.py +79 -3
- package/skills/gsd-path/scripts/check_docs_audit.py +77 -4
- package/skills/gsd-path/scripts/check_handoffs.py +546 -30
- package/skills/gsd-path/scripts/check_task_briefs.py +279 -23
- package/skills/gsd-path/scripts/detect_project.py +243 -18
- package/skills/gsd-path/scripts/discussion_validate.py +86 -29
- package/skills/gsd-path/scripts/dispatch_driver.py +392 -22
- package/skills/gsd-path/scripts/git_guard.py +3 -3
- package/skills/gsd-path/scripts/integration.py +146 -1
- package/skills/gsd-path/scripts/isolation.py +392 -60
- package/skills/gsd-path/scripts/loop_run.py +16 -4
- package/skills/gsd-path/scripts/members.py +11 -9
- package/skills/gsd-path/scripts/path_config.py +3 -3
- package/skills/gsd-path/scripts/pipeline_git.py +63 -12
- package/skills/gsd-path/scripts/pipeline_state.py +111 -33
- package/skills/gsd-path/scripts/state_checkpoint.py +9 -27
- package/skills/gsd-path/scripts/status_runtime.py +22 -4
- package/skills/gsd-path/scripts/task_context.py +12 -3
- package/skills/gsd-path/scripts/workflow_run.py +5 -2
- package/skills/gsd-path/scripts/worktree_paths.py +54 -2
- package/skills/gsd-path/templates/docs-audit.md +5 -1
- package/skills/gsd-path/templates/final-review.md +11 -2
- package/skills/gsd-path/templates/intent.md +30 -0
- package/skills/gsd-path/templates/patch-findings.md +3 -3
- package/skills/gsd-path/templates/plan.md +17 -2
- package/skills/gsd-path/templates/research-handoff.md +14 -3
- package/skills/gsd-path/templates/state.md +1 -1
- package/skills/gsd-path/templates/wave-review.md +16 -6
- package/skills/gsd-path-build/references/build-native.md +16 -1
- package/skills/gsd-path-build/references/dispatch.md +13 -0
- package/skills/gsd-path-build/references/reviewer-final.md +20 -2
- package/skills/gsd-path-build/references/reviewer-wave.md +15 -4
- package/skills/gsd-path-build/references/reviewer.md +3 -1
- package/skills/gsd-path-build/references/spec-probes.md +150 -0
- package/skills/gsd-path-build/scripts/_common.py +149 -1
- package/skills/gsd-path-build/scripts/archive_milestone.py +14 -13
- package/skills/gsd-path-build/scripts/build_recovery.py +96 -31
- package/skills/gsd-path-build/scripts/build_state.py +79 -3
- package/skills/gsd-path-build/scripts/check_handoffs.py +546 -30
- package/skills/gsd-path-build/scripts/check_task_briefs.py +279 -23
- package/skills/gsd-path-build/scripts/detect_project.py +243 -18
- package/skills/gsd-path-build/scripts/discussion_validate.py +86 -29
- package/skills/gsd-path-build/scripts/dispatch_driver.py +392 -22
- package/skills/gsd-path-build/scripts/integration.py +146 -1
- package/skills/gsd-path-build/scripts/isolation.py +392 -60
- package/skills/gsd-path-build/scripts/loop_run.py +16 -4
- package/skills/gsd-path-build/scripts/members.py +11 -9
- package/skills/gsd-path-build/scripts/path_config.py +3 -3
- package/skills/gsd-path-build/scripts/pipeline_git.py +63 -12
- package/skills/gsd-path-build/scripts/pipeline_state.py +111 -33
- package/skills/gsd-path-build/scripts/state_checkpoint.py +9 -27
- package/skills/gsd-path-build/scripts/task_context.py +12 -3
- package/skills/gsd-path-build/scripts/workflow_run.py +5 -2
- package/skills/gsd-path-build/scripts/worktree_paths.py +54 -2
- package/skills/gsd-path-build/templates/wave-review.md +16 -6
- package/skills/gsd-path-decide/references/dispatch.md +13 -0
- package/skills/gsd-path-decide/scripts/_common.py +149 -1
- package/skills/gsd-path-decide/scripts/archive_milestone.py +14 -13
- package/skills/gsd-path-decide/scripts/build_recovery.py +96 -31
- package/skills/gsd-path-decide/scripts/check_handoffs.py +546 -30
- package/skills/gsd-path-decide/scripts/check_task_briefs.py +279 -23
- package/skills/gsd-path-decide/scripts/detect_project.py +243 -18
- package/skills/gsd-path-decide/scripts/discussion_validate.py +86 -29
- package/skills/gsd-path-decide/scripts/integration.py +146 -1
- package/skills/gsd-path-decide/scripts/isolation.py +392 -60
- package/skills/gsd-path-decide/scripts/loop_run.py +16 -4
- package/skills/gsd-path-decide/scripts/members.py +11 -9
- package/skills/gsd-path-decide/scripts/path_config.py +3 -3
- package/skills/gsd-path-decide/scripts/pipeline_git.py +63 -12
- package/skills/gsd-path-decide/scripts/pipeline_state.py +111 -33
- package/skills/gsd-path-decide/scripts/state_checkpoint.py +9 -27
- package/skills/gsd-path-decide/scripts/worktree_paths.py +54 -2
- package/skills/gsd-path-define/SKILL.md +28 -3
- package/skills/gsd-path-define/references/spec-probes.md +150 -0
- package/skills/gsd-path-define/scripts/_common.py +149 -1
- package/skills/gsd-path-define/scripts/archive_milestone.py +14 -13
- package/skills/gsd-path-define/scripts/build_recovery.py +96 -31
- package/skills/gsd-path-define/scripts/check_handoffs.py +546 -30
- package/skills/gsd-path-define/scripts/check_task_briefs.py +279 -23
- package/skills/gsd-path-define/scripts/detect_project.py +243 -18
- package/skills/gsd-path-define/scripts/discussion_validate.py +86 -29
- package/skills/gsd-path-define/scripts/integration.py +146 -1
- package/skills/gsd-path-define/scripts/isolation.py +392 -60
- package/skills/gsd-path-define/scripts/loop_run.py +16 -4
- package/skills/gsd-path-define/scripts/members.py +11 -9
- package/skills/gsd-path-define/scripts/path_config.py +3 -3
- package/skills/gsd-path-define/scripts/pipeline_git.py +63 -12
- package/skills/gsd-path-define/scripts/pipeline_state.py +111 -33
- package/skills/gsd-path-define/scripts/state_checkpoint.py +9 -27
- package/skills/gsd-path-define/scripts/worktree_paths.py +54 -2
- package/skills/gsd-path-define/templates/intent.md +30 -0
- package/skills/gsd-path-define/templates/state.md +1 -1
- package/skills/gsd-path-discuss/scripts/_common.py +149 -1
- package/skills/gsd-path-discuss/scripts/loop_run.py +16 -4
- package/skills/gsd-path-discuss/scripts/path_config.py +3 -3
- package/skills/gsd-path-discuss/scripts/worktree_paths.py +54 -2
- package/skills/gsd-path-docs-audit/references/dispatch.md +13 -0
- package/skills/gsd-path-docs-audit/references/docs-auditor.md +5 -0
- package/skills/gsd-path-docs-audit/scripts/_common.py +149 -1
- package/skills/gsd-path-docs-audit/scripts/archive_milestone.py +14 -13
- package/skills/gsd-path-docs-audit/scripts/build_recovery.py +96 -31
- package/skills/gsd-path-docs-audit/scripts/check_docs_audit.py +77 -4
- package/skills/gsd-path-docs-audit/scripts/check_handoffs.py +546 -30
- package/skills/gsd-path-docs-audit/scripts/check_task_briefs.py +279 -23
- package/skills/gsd-path-docs-audit/scripts/detect_project.py +243 -18
- package/skills/gsd-path-docs-audit/scripts/discussion_validate.py +86 -29
- package/skills/gsd-path-docs-audit/scripts/integration.py +146 -1
- package/skills/gsd-path-docs-audit/scripts/isolation.py +392 -60
- package/skills/gsd-path-docs-audit/scripts/loop_run.py +16 -4
- package/skills/gsd-path-docs-audit/scripts/members.py +11 -9
- package/skills/gsd-path-docs-audit/scripts/path_config.py +3 -3
- package/skills/gsd-path-docs-audit/scripts/pipeline_git.py +63 -12
- package/skills/gsd-path-docs-audit/scripts/pipeline_state.py +111 -33
- package/skills/gsd-path-docs-audit/scripts/state_checkpoint.py +9 -27
- package/skills/gsd-path-docs-audit/scripts/worktree_paths.py +54 -2
- package/skills/gsd-path-docs-audit/templates/docs-audit.md +5 -1
- package/skills/gsd-path-forensics/scripts/_common.py +149 -1
- package/skills/gsd-path-forensics/scripts/loop_run.py +16 -4
- package/skills/gsd-path-forensics/scripts/path_config.py +3 -3
- package/skills/gsd-path-forensics/scripts/pipeline_undo.py +101 -8
- package/skills/gsd-path-forensics/scripts/worktree_paths.py +54 -2
- package/skills/gsd-path-inspect/references/dispatch.md +13 -0
- package/skills/gsd-path-inspect/references/docs-auditor.md +5 -0
- package/skills/gsd-path-inspect/scripts/_common.py +149 -1
- package/skills/gsd-path-inspect/scripts/archive_milestone.py +14 -13
- package/skills/gsd-path-inspect/scripts/build_recovery.py +96 -31
- package/skills/gsd-path-inspect/scripts/check_docs_audit.py +77 -4
- package/skills/gsd-path-inspect/scripts/check_handoffs.py +546 -30
- package/skills/gsd-path-inspect/scripts/check_task_briefs.py +279 -23
- package/skills/gsd-path-inspect/scripts/detect_project.py +243 -18
- package/skills/gsd-path-inspect/scripts/discussion_validate.py +86 -29
- package/skills/gsd-path-inspect/scripts/integration.py +146 -1
- package/skills/gsd-path-inspect/scripts/isolation.py +392 -60
- package/skills/gsd-path-inspect/scripts/loop_run.py +16 -4
- package/skills/gsd-path-inspect/scripts/members.py +11 -9
- package/skills/gsd-path-inspect/scripts/path_config.py +3 -3
- package/skills/gsd-path-inspect/scripts/pipeline_git.py +63 -12
- package/skills/gsd-path-inspect/scripts/pipeline_state.py +111 -33
- package/skills/gsd-path-inspect/scripts/state_checkpoint.py +9 -27
- package/skills/gsd-path-inspect/scripts/workflow_run.py +5 -2
- package/skills/gsd-path-inspect/scripts/worktree_paths.py +54 -2
- package/skills/gsd-path-inspect/templates/docs-audit.md +5 -1
- package/skills/gsd-path-inspect/templates/state.md +1 -1
- package/skills/gsd-path-loop/scripts/_common.py +149 -1
- package/skills/gsd-path-loop/scripts/loop_run.py +16 -4
- package/skills/gsd-path-loop/scripts/path_config.py +3 -3
- package/skills/gsd-path-loop/scripts/worktree_paths.py +54 -2
- package/skills/gsd-path-migrate/scripts/_common.py +149 -1
- package/skills/gsd-path-migrate/scripts/loop_run.py +16 -4
- package/skills/gsd-path-migrate/scripts/path_config.py +3 -3
- package/skills/gsd-path-plan/SKILL.md +11 -3
- package/skills/gsd-path-plan/references/dispatch.md +13 -0
- package/skills/gsd-path-plan/references/planner.md +14 -4
- package/skills/gsd-path-plan/references/spec-probes.md +150 -0
- package/skills/gsd-path-plan/scripts/_common.py +149 -1
- package/skills/gsd-path-plan/scripts/archive_milestone.py +14 -13
- package/skills/gsd-path-plan/scripts/build_recovery.py +96 -31
- package/skills/gsd-path-plan/scripts/check_handoffs.py +546 -30
- package/skills/gsd-path-plan/scripts/check_task_briefs.py +279 -23
- package/skills/gsd-path-plan/scripts/detect_project.py +243 -18
- package/skills/gsd-path-plan/scripts/discussion_validate.py +86 -29
- package/skills/gsd-path-plan/scripts/integration.py +146 -1
- package/skills/gsd-path-plan/scripts/isolation.py +392 -60
- package/skills/gsd-path-plan/scripts/loop_run.py +16 -4
- package/skills/gsd-path-plan/scripts/members.py +11 -9
- package/skills/gsd-path-plan/scripts/path_config.py +3 -3
- package/skills/gsd-path-plan/scripts/pipeline_git.py +63 -12
- package/skills/gsd-path-plan/scripts/pipeline_state.py +111 -33
- package/skills/gsd-path-plan/scripts/state_checkpoint.py +9 -27
- package/skills/gsd-path-plan/scripts/workflow_run.py +5 -2
- package/skills/gsd-path-plan/scripts/worktree_paths.py +54 -2
- package/skills/gsd-path-plan/templates/patch-findings.md +3 -3
- package/skills/gsd-path-plan/templates/plan.md +17 -2
- package/skills/gsd-path-research/SKILL.md +11 -0
- package/skills/gsd-path-research/references/dispatch.md +13 -0
- package/skills/gsd-path-research/scripts/_common.py +149 -1
- package/skills/gsd-path-research/scripts/archive_milestone.py +14 -13
- package/skills/gsd-path-research/scripts/build_recovery.py +96 -31
- package/skills/gsd-path-research/scripts/check_handoffs.py +546 -30
- package/skills/gsd-path-research/scripts/check_task_briefs.py +279 -23
- package/skills/gsd-path-research/scripts/detect_project.py +243 -18
- package/skills/gsd-path-research/scripts/discussion_validate.py +86 -29
- package/skills/gsd-path-research/scripts/integration.py +146 -1
- package/skills/gsd-path-research/scripts/isolation.py +392 -60
- package/skills/gsd-path-research/scripts/loop_run.py +16 -4
- package/skills/gsd-path-research/scripts/members.py +11 -9
- package/skills/gsd-path-research/scripts/path_config.py +3 -3
- package/skills/gsd-path-research/scripts/pipeline_git.py +63 -12
- package/skills/gsd-path-research/scripts/pipeline_state.py +111 -33
- package/skills/gsd-path-research/scripts/state_checkpoint.py +9 -27
- package/skills/gsd-path-research/scripts/worktree_paths.py +54 -2
- package/skills/gsd-path-research/templates/research-handoff.md +14 -3
- package/skills/gsd-path-roadmap/references/dispatch.md +13 -0
- package/skills/gsd-path-roadmap/scripts/_common.py +149 -1
- package/skills/gsd-path-roadmap/scripts/archive_milestone.py +14 -13
- package/skills/gsd-path-roadmap/scripts/build_recovery.py +96 -31
- package/skills/gsd-path-roadmap/scripts/check_handoffs.py +546 -30
- package/skills/gsd-path-roadmap/scripts/check_task_briefs.py +279 -23
- package/skills/gsd-path-roadmap/scripts/detect_project.py +243 -18
- package/skills/gsd-path-roadmap/scripts/discussion_validate.py +86 -29
- package/skills/gsd-path-roadmap/scripts/integration.py +146 -1
- package/skills/gsd-path-roadmap/scripts/isolation.py +392 -60
- package/skills/gsd-path-roadmap/scripts/loop_run.py +16 -4
- package/skills/gsd-path-roadmap/scripts/members.py +11 -9
- package/skills/gsd-path-roadmap/scripts/path_config.py +3 -3
- package/skills/gsd-path-roadmap/scripts/pipeline_git.py +63 -12
- package/skills/gsd-path-roadmap/scripts/pipeline_state.py +111 -33
- package/skills/gsd-path-roadmap/scripts/state_checkpoint.py +9 -27
- package/skills/gsd-path-roadmap/scripts/worktree_paths.py +54 -2
- package/skills/gsd-path-ship/SKILL.md +6 -1
- package/skills/gsd-path-ship/references/dispatch.md +13 -0
- package/skills/gsd-path-ship/references/reviewer-final.md +20 -2
- package/skills/gsd-path-ship/references/reviewer-wave.md +15 -4
- package/skills/gsd-path-ship/references/reviewer.md +3 -1
- package/skills/gsd-path-ship/references/spec-probes.md +150 -0
- package/skills/gsd-path-ship/scripts/_common.py +149 -1
- package/skills/gsd-path-ship/scripts/archive_milestone.py +14 -13
- package/skills/gsd-path-ship/scripts/build_recovery.py +96 -31
- package/skills/gsd-path-ship/scripts/build_state.py +79 -3
- package/skills/gsd-path-ship/scripts/check_handoffs.py +546 -30
- package/skills/gsd-path-ship/scripts/check_task_briefs.py +279 -23
- package/skills/gsd-path-ship/scripts/detect_project.py +243 -18
- package/skills/gsd-path-ship/scripts/discussion_validate.py +86 -29
- package/skills/gsd-path-ship/scripts/integration.py +146 -1
- package/skills/gsd-path-ship/scripts/isolation.py +392 -60
- package/skills/gsd-path-ship/scripts/loop_run.py +16 -4
- package/skills/gsd-path-ship/scripts/members.py +11 -9
- package/skills/gsd-path-ship/scripts/path_config.py +3 -3
- package/skills/gsd-path-ship/scripts/pipeline_git.py +63 -12
- package/skills/gsd-path-ship/scripts/pipeline_state.py +111 -33
- package/skills/gsd-path-ship/scripts/state_checkpoint.py +9 -27
- package/skills/gsd-path-ship/scripts/workflow_run.py +5 -2
- package/skills/gsd-path-ship/scripts/worktree_paths.py +54 -2
- package/skills/gsd-path-ship/templates/final-review.md +11 -2
- package/skills/gsd-path-ship/templates/patch-findings.md +3 -3
- package/skills/gsd-path-ship/templates/wave-review.md +16 -6
- package/skills/gsd-path-undo/scripts/_common.py +149 -1
- package/skills/gsd-path-undo/scripts/loop_run.py +16 -4
- package/skills/gsd-path-undo/scripts/path_config.py +3 -3
- package/skills/gsd-path-undo/scripts/pipeline_undo.py +101 -8
- package/skills/gsd-path-undo/scripts/worktree_paths.py +54 -2
- package/skills/path/DEFINE.md +28 -3
- package/skills/path/PLAN.md +11 -3
- package/skills/path/RESEARCH.md +11 -0
- package/skills/path/SHIP.md +6 -1
- package/skills/path/references/build-native.md +16 -1
- package/skills/path/references/dispatch.md +13 -0
- package/skills/path/references/docs-auditor.md +5 -0
- package/skills/path/references/planner.md +14 -4
- package/skills/path/references/reviewer-final.md +20 -2
- package/skills/path/references/reviewer-wave.md +15 -4
- package/skills/path/references/reviewer.md +3 -1
- package/skills/path/references/spec-probes.md +150 -0
- package/skills/path/scripts/_common.py +149 -1
- package/skills/path/scripts/archive_milestone.py +14 -13
- package/skills/path/scripts/build_recovery.py +96 -31
- package/skills/path/scripts/build_state.py +79 -3
- package/skills/path/scripts/check_docs_audit.py +77 -4
- package/skills/path/scripts/check_handoffs.py +546 -30
- package/skills/path/scripts/check_task_briefs.py +279 -23
- package/skills/path/scripts/detect_project.py +243 -18
- package/skills/path/scripts/discussion_validate.py +86 -29
- package/skills/path/scripts/dispatch_driver.py +392 -22
- package/skills/path/scripts/git_guard.py +3 -3
- package/skills/path/scripts/integration.py +146 -1
- package/skills/path/scripts/isolation.py +392 -60
- package/skills/path/scripts/loop_run.py +16 -4
- package/skills/path/scripts/members.py +11 -9
- package/skills/path/scripts/path_config.py +3 -3
- package/skills/path/scripts/pipeline_git.py +63 -12
- package/skills/path/scripts/pipeline_state.py +111 -33
- package/skills/path/scripts/state_checkpoint.py +9 -27
- package/skills/path/scripts/status_runtime.py +22 -4
- package/skills/path/scripts/task_context.py +12 -3
- package/skills/path/scripts/workflow_run.py +5 -2
- package/skills/path/scripts/worktree_paths.py +54 -2
- package/skills/path/templates/docs-audit.md +5 -1
- package/skills/path/templates/final-review.md +11 -2
- package/skills/path/templates/intent.md +30 -0
- package/skills/path/templates/patch-findings.md +3 -3
- package/skills/path/templates/plan.md +17 -2
- package/skills/path/templates/research-handoff.md +14 -3
- package/skills/path/templates/state.md +1 -1
- package/skills/path/templates/wave-review.md +16 -6
- package/skills/gsd-path/scripts/__pycache__/_common.cpython-312.pyc +0 -0
- package/skills/gsd-path/scripts/__pycache__/check_handoffs.cpython-312.pyc +0 -0
- package/skills/gsd-path-build/scripts/__pycache__/_common.cpython-312.pyc +0 -0
- package/skills/gsd-path-build/scripts/__pycache__/check_handoffs.cpython-312.pyc +0 -0
- package/skills/gsd-path-inspect/scripts/__pycache__/_common.cpython-312.pyc +0 -0
- package/skills/gsd-path-plan/scripts/__pycache__/_common.cpython-312.pyc +0 -0
- package/skills/gsd-path-ship/scripts/__pycache__/_common.cpython-312.pyc +0 -0
package/scripts/_common.py
CHANGED
|
@@ -65,6 +65,49 @@ VERIFY_RESULTS = ("pass", "fail")
|
|
|
65
65
|
VERIFY_LEDGER_SCHEMA = "gsd-path/verify-ledger/v2"
|
|
66
66
|
VERIFY_BLOCK_PATTERN = re.compile(r"```bash[ \t]*\n(?P<block>.*?)```", re.DOTALL)
|
|
67
67
|
|
|
68
|
+
REVIEW_BULLET_HEADING_PATTERN = re.compile(r"^#{2,3} ")
|
|
69
|
+
REVIEW_BULLET_FIELD_LABEL_PATTERN = re.compile(r"^- \*\*[^*]+\*\*:")
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def review_bullet_field_label_pattern(field: str) -> re.Pattern[str]:
|
|
73
|
+
return re.compile(rf"^- \*\*{re.escape(field)}\*\*:[ \t]*(.*)$")
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _review_bullet_field_value(lines: Sequence[str], index: int, inline: str) -> str:
|
|
77
|
+
if inline:
|
|
78
|
+
return inline
|
|
79
|
+
collected: list[str] = []
|
|
80
|
+
for line in lines[index + 1 :]:
|
|
81
|
+
if REVIEW_BULLET_FIELD_LABEL_PATTERN.match(line) or REVIEW_BULLET_HEADING_PATTERN.match(
|
|
82
|
+
line
|
|
83
|
+
):
|
|
84
|
+
break
|
|
85
|
+
if line and not line[0].isspace():
|
|
86
|
+
break
|
|
87
|
+
stripped = line.strip()
|
|
88
|
+
if stripped:
|
|
89
|
+
collected.append(stripped)
|
|
90
|
+
return "\n".join(collected)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def find_review_bullet_field_values(lines: Sequence[str], field: str) -> list[str]:
|
|
94
|
+
pattern = review_bullet_field_label_pattern(field)
|
|
95
|
+
values: list[str] = []
|
|
96
|
+
for index, line in enumerate(lines):
|
|
97
|
+
match = pattern.fullmatch(line)
|
|
98
|
+
if match is None:
|
|
99
|
+
continue
|
|
100
|
+
values.append(_review_bullet_field_value(lines, index, match.group(1)))
|
|
101
|
+
return values
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def parse_review_bullet_field(lines: Sequence[str], field: str) -> str:
|
|
105
|
+
"""Parse one `- **Field**:` review bullet, including indented continuations."""
|
|
106
|
+
values = find_review_bullet_field_values(lines, field)
|
|
107
|
+
if len(values) != 1:
|
|
108
|
+
raise ValueError(field)
|
|
109
|
+
return values[0]
|
|
110
|
+
|
|
68
111
|
|
|
69
112
|
# Sample paths the pipeline must commit. A product rule such as an unanchored
|
|
70
113
|
# `build/` would otherwise drop them silently from every commit.
|
|
@@ -150,6 +193,11 @@ def verify_ledger_entries(path: Path) -> list:
|
|
|
150
193
|
return parse_verify_ledger(path.read_text(encoding="utf-8"))
|
|
151
194
|
|
|
152
195
|
|
|
196
|
+
def read_user_text(path: Path) -> str:
|
|
197
|
+
"""Read a user-edited text file with CRLF normalized to LF."""
|
|
198
|
+
return path.read_text(encoding="utf-8").replace("\r\n", "\n").replace("\r", "\n")
|
|
199
|
+
|
|
200
|
+
|
|
153
201
|
def parse_verify_ledger(text: str) -> list:
|
|
154
202
|
entries = []
|
|
155
203
|
for number, line in enumerate(text.splitlines(), start=1):
|
|
@@ -204,7 +252,43 @@ def run_command(
|
|
|
204
252
|
def run_git(
|
|
205
253
|
repo: Path, *arguments: str, input: Optional[str] = None
|
|
206
254
|
) -> subprocess.CompletedProcess[str]:
|
|
207
|
-
return _run_exact((
|
|
255
|
+
return _run_exact(git_command(repo, *arguments), input=input)
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def git_command(repo: Path, *arguments: str) -> tuple[str, ...]:
|
|
259
|
+
"""Git argv for repo, including WSL drvfs workarounds when needed."""
|
|
260
|
+
return ("git", "-C", str(repo), *git_drvfs_config_flags(repo), *arguments)
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
def git_drvfs_config_flags(repo: Path) -> tuple[str, ...]:
|
|
264
|
+
"""Disable index preload on 9p/drvfs mounts where git commit races on index.lock (#207)."""
|
|
265
|
+
if os.name != "posix":
|
|
266
|
+
return ()
|
|
267
|
+
try:
|
|
268
|
+
root = repo.resolve()
|
|
269
|
+
mounts = Path("/proc/mounts").read_text(encoding="utf-8", errors="replace").splitlines()
|
|
270
|
+
except OSError:
|
|
271
|
+
return ()
|
|
272
|
+
best = ""
|
|
273
|
+
fstype = ""
|
|
274
|
+
for line in mounts:
|
|
275
|
+
parts = line.split()
|
|
276
|
+
if len(parts) < 3:
|
|
277
|
+
continue
|
|
278
|
+
mount_point, mount_type = parts[1], parts[2]
|
|
279
|
+
if mount_type not in {"9p", "drvfs"}:
|
|
280
|
+
continue
|
|
281
|
+
if root == Path(mount_point) or root.is_relative_to(mount_point):
|
|
282
|
+
if len(mount_point) >= len(best):
|
|
283
|
+
best, fstype = mount_point, mount_type
|
|
284
|
+
if not fstype:
|
|
285
|
+
return ()
|
|
286
|
+
return ("-c", "core.preloadindex=false", "-c", "index.threads=1")
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
def git_index_lock_retryable(stderr: str) -> bool:
|
|
290
|
+
text = stderr.casefold()
|
|
291
|
+
return "index.lock" in text and "file exists" in text
|
|
208
292
|
|
|
209
293
|
|
|
210
294
|
# OS files that are never pipeline artifacts when git ignores them.
|
|
@@ -227,6 +311,70 @@ def git_visible_entries(directory: Path) -> set:
|
|
|
227
311
|
return {path.split("/", 1)[0] for path in result.stdout.split("\0") if path}
|
|
228
312
|
|
|
229
313
|
|
|
314
|
+
RUNTIME_PIN_PATH = ".gsd-path/runtime.json"
|
|
315
|
+
RUNTIME_PIN_ALLOWANCE = (
|
|
316
|
+
"only runtime-pin commits (.gsd-path/runtime.json) may follow the reviewed HEAD"
|
|
317
|
+
)
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
def reviewed_head_covers(repo: Path, reviewed: str, head: str) -> bool:
|
|
321
|
+
"""True when a review of `reviewed` still covers `head`.
|
|
322
|
+
|
|
323
|
+
That is the same commit, or `reviewed` is an ancestor of `head` and every
|
|
324
|
+
commit on the first-parent path in ``reviewed..head`` is a non-merge commit
|
|
325
|
+
whose only change is ``.gsd-path/runtime.json``. An empty commit does not
|
|
326
|
+
qualify. Any other path, any merge commit, an unresolvable SHA, or a
|
|
327
|
+
non-ancestor returns False.
|
|
328
|
+
"""
|
|
329
|
+
if not reviewed or not head:
|
|
330
|
+
return False
|
|
331
|
+
resolved_repo = Path(repo)
|
|
332
|
+
resolved: List[str] = []
|
|
333
|
+
for value in (reviewed, head):
|
|
334
|
+
result = run_git(
|
|
335
|
+
resolved_repo, "rev-parse", "--verify", "--quiet", f"{value}^{{commit}}"
|
|
336
|
+
)
|
|
337
|
+
if result.returncode != 0 or not result.stdout.strip():
|
|
338
|
+
return False
|
|
339
|
+
resolved.append(result.stdout.strip())
|
|
340
|
+
reviewed_sha, head_sha = resolved
|
|
341
|
+
if reviewed_sha == head_sha:
|
|
342
|
+
return True
|
|
343
|
+
if (
|
|
344
|
+
run_git(resolved_repo, "merge-base", "--is-ancestor", reviewed_sha, head_sha).returncode
|
|
345
|
+
!= 0
|
|
346
|
+
):
|
|
347
|
+
return False
|
|
348
|
+
listing = run_git(
|
|
349
|
+
resolved_repo,
|
|
350
|
+
"rev-list",
|
|
351
|
+
"--first-parent",
|
|
352
|
+
"--parents",
|
|
353
|
+
f"{reviewed_sha}..{head_sha}",
|
|
354
|
+
)
|
|
355
|
+
if listing.returncode != 0:
|
|
356
|
+
return False
|
|
357
|
+
for line in listing.stdout.splitlines():
|
|
358
|
+
fields = line.split()
|
|
359
|
+
if len(fields) != 2:
|
|
360
|
+
return False
|
|
361
|
+
commit_sha, parent_sha = fields[0], fields[1]
|
|
362
|
+
changed = run_git(
|
|
363
|
+
resolved_repo,
|
|
364
|
+
"diff",
|
|
365
|
+
"--name-only",
|
|
366
|
+
"--no-renames",
|
|
367
|
+
"--no-ext-diff",
|
|
368
|
+
parent_sha,
|
|
369
|
+
commit_sha,
|
|
370
|
+
)
|
|
371
|
+
if changed.returncode != 0:
|
|
372
|
+
return False
|
|
373
|
+
if {path for path in changed.stdout.splitlines() if path} != {RUNTIME_PIN_PATH}:
|
|
374
|
+
return False
|
|
375
|
+
return True
|
|
376
|
+
|
|
377
|
+
|
|
230
378
|
def atomic_replace(path: Path, temporary_path: Path, content: str) -> None:
|
|
231
379
|
temporary_path.parent.mkdir(parents=True, exist_ok=True)
|
|
232
380
|
descriptor = None
|
|
@@ -426,10 +426,10 @@ def frontmatter_value(content: str, key: str) -> Optional[str]:
|
|
|
426
426
|
for line in lines[1:]:
|
|
427
427
|
if line == "---":
|
|
428
428
|
break
|
|
429
|
-
match = re.match(rf"^{re.escape(key)}
|
|
429
|
+
match = re.match(rf"^{re.escape(key)}:(.*)$", line)
|
|
430
430
|
if match:
|
|
431
|
-
value = match.group(1)
|
|
432
|
-
return value
|
|
431
|
+
value = _common.strip_yaml_comment(match.group(1))
|
|
432
|
+
return value.strip().strip('"').strip("'")
|
|
433
433
|
return None
|
|
434
434
|
|
|
435
435
|
|
|
@@ -586,14 +586,11 @@ def resolved_archive_target(
|
|
|
586
586
|
expected_number = milestone_number(branch)
|
|
587
587
|
configured = parsed_state.archive
|
|
588
588
|
entries = archive_sequence_entries(project)
|
|
589
|
-
missing_prior = [
|
|
590
|
-
number for number in range(1, expected_number) if number not in entries
|
|
591
|
-
]
|
|
592
589
|
later = sorted(number for number in entries if number > expected_number)
|
|
593
|
-
if
|
|
590
|
+
if later:
|
|
594
591
|
raise ArchiveError(
|
|
595
592
|
f"archive sequence does not match bound branch {branch}: "
|
|
596
|
-
f"
|
|
593
|
+
f"later={later}"
|
|
597
594
|
)
|
|
598
595
|
|
|
599
596
|
if configured is not None:
|
|
@@ -1065,9 +1062,10 @@ def prepare_locked(project: Path, active_root: Path, slug: str) -> dict:
|
|
|
1065
1062
|
final = active_root / "review" / "FINAL.md"
|
|
1066
1063
|
reviewed = re.search(r"(?m)^Reviewed HEAD:\s*(\S+)", final.read_text(encoding="utf-8")) if final.is_file() else None
|
|
1067
1064
|
head = require_git_success(run_git(project, "rev-parse", "HEAD"), "resolve HEAD").strip()
|
|
1068
|
-
if reviewed and reviewed.group(1)
|
|
1065
|
+
if reviewed and not _common.reviewed_head_covers(project, reviewed.group(1), head):
|
|
1069
1066
|
raise ArchiveError(
|
|
1070
1067
|
f"FINAL.md Reviewed HEAD {reviewed.group(1)} does not match current HEAD {head}; "
|
|
1068
|
+
f"{_common.RUNTIME_PIN_ALLOWANCE}; "
|
|
1071
1069
|
"re-run the final review on the current commit before archiving"
|
|
1072
1070
|
)
|
|
1073
1071
|
target = resolved_archive_target(project, slug, parsed_state)
|
|
@@ -1680,6 +1678,7 @@ def abandon_locked(
|
|
|
1680
1678
|
|
|
1681
1679
|
|
|
1682
1680
|
run_git = _common.run_git
|
|
1681
|
+
reviewed_head_covers = _common.reviewed_head_covers
|
|
1683
1682
|
run_command = _common.run_command
|
|
1684
1683
|
|
|
1685
1684
|
|
|
@@ -1899,9 +1898,10 @@ def preflight(repo: Path) -> dict:
|
|
|
1899
1898
|
require_stageable_carry_forward(project, archive)
|
|
1900
1899
|
require_clean_older_archives(project, configured)
|
|
1901
1900
|
head = require_git_success(run_git(project, "rev-parse", "HEAD"), "resolve preflight HEAD")
|
|
1902
|
-
if reviewed_head
|
|
1901
|
+
if not reviewed_head_covers(project, reviewed_head, head):
|
|
1903
1902
|
raise ArchiveError(
|
|
1904
|
-
f"FINAL.md Reviewed HEAD {reviewed_head} does not match current HEAD {head}"
|
|
1903
|
+
f"FINAL.md Reviewed HEAD {reviewed_head} does not match current HEAD {head}; "
|
|
1904
|
+
f"{_common.RUNTIME_PIN_ALLOWANCE}"
|
|
1905
1905
|
)
|
|
1906
1906
|
return {
|
|
1907
1907
|
"archive": configured,
|
|
@@ -2093,9 +2093,10 @@ def validate(repo: Path, *, historical: bool = False) -> dict:
|
|
|
2093
2093
|
|
|
2094
2094
|
members = member_ship_rows(project, archive, ship_commit)
|
|
2095
2095
|
ship_parent = require_git_success(run_git(project, "rev-parse", f"{ship_commit}^"), "resolve ship parent")
|
|
2096
|
-
if reviewed_head
|
|
2096
|
+
if not reviewed_head_covers(project, reviewed_head, ship_parent):
|
|
2097
2097
|
raise ArchiveError(
|
|
2098
|
-
f"FINAL.md Reviewed HEAD {reviewed_head} does not match ship parent {ship_parent}"
|
|
2098
|
+
f"FINAL.md Reviewed HEAD {reviewed_head} does not match ship parent {ship_parent}; "
|
|
2099
|
+
f"{_common.RUNTIME_PIN_ALLOWANCE}"
|
|
2099
2100
|
)
|
|
2100
2101
|
require_canonical_commit_body(
|
|
2101
2102
|
project,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
# gsd-path project runtime
|
|
2
|
-
"""Guard
|
|
2
|
+
"""Guard return to Define or Plan using a committed recovery base."""
|
|
3
3
|
|
|
4
4
|
from __future__ import annotations
|
|
5
5
|
|
|
@@ -17,7 +17,9 @@ EVENTS = {
|
|
|
17
17
|
"define": "build intent corrections requested",
|
|
18
18
|
"plan": "build plan repair requested",
|
|
19
19
|
}
|
|
20
|
+
PLAN_INTENT_EVENT = "plan intent corrections requested"
|
|
20
21
|
MARKER = "build recovery: "
|
|
22
|
+
RECOVERY_KEYS = frozenset({"base", "branch", "kind", "source"})
|
|
21
23
|
|
|
22
24
|
|
|
23
25
|
def runtime():
|
|
@@ -36,9 +38,42 @@ def checkpoint_runtime():
|
|
|
36
38
|
return state_checkpoint
|
|
37
39
|
|
|
38
40
|
|
|
41
|
+
def _parse_recovery_record(value: object) -> dict:
|
|
42
|
+
if (
|
|
43
|
+
not isinstance(value, dict)
|
|
44
|
+
or not {"base", "branch", "kind"}.issubset(value)
|
|
45
|
+
or not set(value).issubset(RECOVERY_KEYS)
|
|
46
|
+
or not isinstance(value["base"], str)
|
|
47
|
+
or not re.fullmatch(r"[0-9a-f]{40}", value["base"])
|
|
48
|
+
):
|
|
49
|
+
raise runtime().PipelineStateError("invalid build recovery record")
|
|
50
|
+
source = value.get("source", "build")
|
|
51
|
+
if source not in {"build", "plan"}:
|
|
52
|
+
raise runtime().PipelineStateError("invalid build recovery record")
|
|
53
|
+
if source == "plan":
|
|
54
|
+
if value["kind"] != "define":
|
|
55
|
+
raise runtime().PipelineStateError("invalid build recovery record")
|
|
56
|
+
elif value["kind"] not in EVENTS:
|
|
57
|
+
raise runtime().PipelineStateError("invalid build recovery record")
|
|
58
|
+
return {**value, "source": source}
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _recovery_base_proves_milestone(before, current, source: str) -> bool:
|
|
62
|
+
if before.branch != current.branch or before.milestone != current.milestone:
|
|
63
|
+
return False
|
|
64
|
+
if source == "plan":
|
|
65
|
+
return (before.phase, before.status) in {("plan", "active"), ("plan", "blocked")}
|
|
66
|
+
return (before.phase, before.status) == ("build", "blocked")
|
|
67
|
+
|
|
68
|
+
|
|
39
69
|
def context(repo: Path, text: str | None = None) -> dict | None:
|
|
40
70
|
state = runtime()
|
|
41
|
-
|
|
71
|
+
try:
|
|
72
|
+
current, loaded, _ = state.load_state(repo)
|
|
73
|
+
except state.PipelineStateError as error:
|
|
74
|
+
if "STATE.md must be a real file" not in str(error):
|
|
75
|
+
raise
|
|
76
|
+
return None
|
|
42
77
|
text = loaded if text is None else text
|
|
43
78
|
recovery = None
|
|
44
79
|
for line in text.splitlines():
|
|
@@ -47,26 +82,24 @@ def context(repo: Path, text: str | None = None) -> dict | None:
|
|
|
47
82
|
event = line.split(" — ", 2)[-1]
|
|
48
83
|
if event.startswith(MARKER):
|
|
49
84
|
try:
|
|
50
|
-
value = json.loads(event[len(MARKER):])
|
|
85
|
+
value = _parse_recovery_record(json.loads(event[len(MARKER):]))
|
|
51
86
|
except ValueError as error:
|
|
52
87
|
raise state.PipelineStateError("invalid build recovery record") from error
|
|
53
|
-
if (not isinstance(value, dict) or set(value) != {"base", "branch", "kind"}
|
|
54
|
-
or value["kind"] not in EVENTS
|
|
55
|
-
or not isinstance(value["base"], str)
|
|
56
|
-
or not re.fullmatch(r"[0-9a-f]{40}", value["base"])):
|
|
57
|
-
raise state.PipelineStateError("invalid build recovery record")
|
|
58
88
|
recovery = {**value, "active": True} if value["branch"] == current.branch else None
|
|
59
|
-
elif
|
|
60
|
-
recovery
|
|
89
|
+
elif recovery and recovery.get("active"):
|
|
90
|
+
source = recovery.get("source", "build")
|
|
91
|
+
if source == "build" and event == "build started":
|
|
92
|
+
recovery["active"] = False
|
|
93
|
+
elif source == "plan" and event == "plan approved":
|
|
94
|
+
recovery["active"] = False
|
|
61
95
|
if recovery:
|
|
62
96
|
if not state._is_ancestor(repo, recovery["base"], "HEAD"):
|
|
63
97
|
raise state.PipelineStateError("build recovery base is not an ancestor of HEAD")
|
|
64
|
-
|
|
65
|
-
if
|
|
98
|
+
base_text = checkpoint_runtime()._git_text_at(repo, recovery["base"], ".project/STATE.md")
|
|
99
|
+
if base_text is None:
|
|
66
100
|
raise state.PipelineStateError("build recovery base has no STATE.md")
|
|
67
|
-
before = state._state_from_text(
|
|
68
|
-
if (
|
|
69
|
-
or before.branch != current.branch or before.milestone != current.milestone):
|
|
101
|
+
before = state._state_from_text(base_text)
|
|
102
|
+
if not _recovery_base_proves_milestone(before, current, recovery.get("source", "build")):
|
|
70
103
|
raise state.PipelineStateError("build recovery base does not prove this blocked milestone")
|
|
71
104
|
return recovery
|
|
72
105
|
|
|
@@ -108,18 +141,20 @@ def settled_tasks(repo: Path) -> dict[str, tuple[str, dict]]:
|
|
|
108
141
|
return tasks
|
|
109
142
|
|
|
110
143
|
|
|
111
|
-
def
|
|
144
|
+
def _begin_common(repo: Path, before, event: str) -> str:
|
|
112
145
|
state = runtime()
|
|
113
|
-
if (before.phase, before.status) != ("build", "blocked") or after.phase not in EVENTS:
|
|
114
|
-
raise state.PipelineStateError("build recovery requires build/blocked")
|
|
115
|
-
if event != EVENTS[after.phase]:
|
|
116
|
-
raise state.PipelineStateError(f"state transition requires event: {EVENTS[after.phase]}")
|
|
117
146
|
if before.archive is not None or before.branch != state._current_branch(repo):
|
|
118
147
|
raise state.PipelineStateError("build recovery requires the active bound branch")
|
|
119
148
|
if state._worktree_changes(repo):
|
|
120
149
|
raise state.PipelineStateError("checkpoint the blocked build before recovery")
|
|
121
150
|
if any(state.transaction_journals(repo).values()):
|
|
122
151
|
raise state.PipelineStateError("finish pending transactions before build recovery")
|
|
152
|
+
state._read_real_file(repo / ".project/intent/INTENT.md", "INTENT.md")
|
|
153
|
+
return state._run_git(repo, "rev-parse", "HEAD").stdout.strip()
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def _settle_build_dispatch(repo: Path) -> None:
|
|
157
|
+
state = runtime()
|
|
123
158
|
records = records_root(repo)
|
|
124
159
|
for directory in {path.parent.parent for path in records.rglob("attempt-*/state.json")}:
|
|
125
160
|
latest = max(directory.glob("attempt-*/state.json"), key=lambda path: int(path.parent.name.split("-")[1]))
|
|
@@ -128,19 +163,49 @@ def begin(repo: Path, before, after, event: str) -> dict:
|
|
|
128
163
|
or (record.get("worktree") and record.get("branch") and not record.get("cleanup_complete"))
|
|
129
164
|
or record.get("outcome") not in {"landed", "collected", "blocked", "resolved", "redispatched"}):
|
|
130
165
|
raise state.PipelineStateError(f"settle dispatch records before recovery: {latest}")
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def _verify_done_tasks(repo: Path, tasks: dict[str, tuple[str, dict]], base: str) -> None:
|
|
169
|
+
state = runtime()
|
|
135
170
|
done = [repo / ".project/tasks" / name for name, (_, fields) in tasks.items()
|
|
136
171
|
if fields["status"] == "done"]
|
|
137
|
-
if done:
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
172
|
+
if not done:
|
|
173
|
+
return
|
|
174
|
+
if __package__:
|
|
175
|
+
from scripts.isolation import verify_landed_task_files
|
|
176
|
+
else:
|
|
177
|
+
from isolation import verify_landed_task_files
|
|
178
|
+
verify_landed_task_files(repo, done, ".project/tasks", base)
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def begin(repo: Path, before, after, event: str) -> dict:
|
|
182
|
+
state = runtime()
|
|
183
|
+
if (before.phase, before.status) == ("build", "blocked") and after.phase in EVENTS:
|
|
184
|
+
if event != EVENTS[after.phase]:
|
|
185
|
+
raise state.PipelineStateError(f"state transition requires event: {EVENTS[after.phase]}")
|
|
186
|
+
_settle_build_dispatch(repo)
|
|
187
|
+
tasks = settled_tasks(repo)
|
|
188
|
+
state._read_real_file(repo / ".project/plan/PLAN.md", "PLAN.md")
|
|
189
|
+
base = _begin_common(repo, before, event)
|
|
190
|
+
_verify_done_tasks(repo, tasks, base)
|
|
191
|
+
return {"kind": after.phase, "base": base, "branch": before.branch, "source": "build"}
|
|
192
|
+
if (
|
|
193
|
+
before.phase == "plan"
|
|
194
|
+
and before.status in {"active", "blocked"}
|
|
195
|
+
and (after.phase, after.status) == ("define", "active")
|
|
196
|
+
):
|
|
197
|
+
if event != PLAN_INTENT_EVENT:
|
|
198
|
+
raise state.PipelineStateError(f"state transition requires event: {PLAN_INTENT_EVENT}")
|
|
199
|
+
state._read_real_file(repo / ".project/plan/PLAN.md", "PLAN.md")
|
|
200
|
+
tasks_dir = repo / ".project/tasks"
|
|
201
|
+
tasks = settled_tasks(repo) if tasks_dir.is_dir() and any(
|
|
202
|
+
path.is_file() and not state._common.is_ignored_junk(path)
|
|
203
|
+
for path in tasks_dir.iterdir()
|
|
204
|
+
) else {}
|
|
205
|
+
base = _begin_common(repo, before, event)
|
|
206
|
+
_verify_done_tasks(repo, tasks, base)
|
|
207
|
+
return {"kind": "define", "base": base, "branch": before.branch, "source": "plan"}
|
|
208
|
+
raise state.PipelineStateError("build recovery requires build/blocked")
|
|
144
209
|
|
|
145
210
|
|
|
146
211
|
def records_root(repo: Path) -> Path:
|
package/scripts/build_state.py
CHANGED
|
@@ -24,7 +24,7 @@ if __package__:
|
|
|
24
24
|
)
|
|
25
25
|
from .pipeline_git import task_commit_subject
|
|
26
26
|
from .pipeline_state import PipelineStateError, load_state
|
|
27
|
-
from . import _common
|
|
27
|
+
from . import _common, check_handoffs
|
|
28
28
|
else:
|
|
29
29
|
try:
|
|
30
30
|
from check_task_briefs import _frontmatter, _sections
|
|
@@ -37,6 +37,7 @@ else:
|
|
|
37
37
|
from pipeline_git import task_commit_subject
|
|
38
38
|
from pipeline_state import PipelineStateError, load_state
|
|
39
39
|
import _common
|
|
40
|
+
import check_handoffs
|
|
40
41
|
except ImportError: # pragma: no cover - package imports used by tests
|
|
41
42
|
from scripts.check_task_briefs import _frontmatter, _sections
|
|
42
43
|
from scripts.isolation import (
|
|
@@ -47,7 +48,7 @@ else:
|
|
|
47
48
|
)
|
|
48
49
|
from scripts.pipeline_git import task_commit_subject
|
|
49
50
|
from scripts.pipeline_state import PipelineStateError, load_state
|
|
50
|
-
from scripts import _common
|
|
51
|
+
from scripts import _common, check_handoffs
|
|
51
52
|
|
|
52
53
|
|
|
53
54
|
DEFAULT_PROJECT_DIR = ".project"
|
|
@@ -57,6 +58,10 @@ FULL_SHA_RE = re.compile(r"^[0-9a-f]{40}$")
|
|
|
57
58
|
# recover verdicts that count as landed, mapped to their evidence classification.
|
|
58
59
|
LANDED_VERDICTS = {"recovered": "proven-landed", "attested": "attested"}
|
|
59
60
|
WAVE_HEADING_RE = re.compile(r"(?m)^## Wave (?P<wave>\d+)\b.*$")
|
|
61
|
+
REVIEW_VERDICT_RE = re.compile(
|
|
62
|
+
r"(?m)^Wave verdict:\s*(?P<verdict>pass|blocked)\s*(?:<!--.*?-->)?\s*$"
|
|
63
|
+
)
|
|
64
|
+
REVIEW_FINDINGS_HEADING = "Review findings"
|
|
60
65
|
VERIFY_HEAVY_RE = re.compile(r"(?m)^Heavy:\s*(?P<value>yes|no)\s*(?:<!--.*-->)?\s*$")
|
|
61
66
|
VALID_TASK_STATUSES = {"pending", "in-progress", "done", "failed", "blocked"}
|
|
62
67
|
VERIFY_RESULTS = _common.VERIFY_RESULTS
|
|
@@ -88,6 +93,7 @@ class Task:
|
|
|
88
93
|
task_file: str
|
|
89
94
|
verify_heavy: bool
|
|
90
95
|
repo: str = ""
|
|
96
|
+
review_findings: bool = False
|
|
91
97
|
|
|
92
98
|
|
|
93
99
|
@dataclass(frozen=True)
|
|
@@ -241,6 +247,7 @@ def _parse_task(path: Path, relative_path: str, task_file: Optional[str] = None)
|
|
|
241
247
|
task_file=task_file or relative_path,
|
|
242
248
|
verify_heavy=_verify_heavy(text, label),
|
|
243
249
|
repo=str(fields.get("repo") or ""),
|
|
250
|
+
review_findings=REVIEW_FINDINGS_HEADING in _sections(text),
|
|
244
251
|
)
|
|
245
252
|
|
|
246
253
|
|
|
@@ -485,6 +492,36 @@ def _overlap(left: Task, right: Task) -> List[str]:
|
|
|
485
492
|
return sorted(set(left.files) & set(right.files))
|
|
486
493
|
|
|
487
494
|
|
|
495
|
+
def _blocked_wave_reviews(project: Project) -> Dict[int, int]:
|
|
496
|
+
"""Return waves whose highest canonical review cycle has a blocked lens."""
|
|
497
|
+
|
|
498
|
+
review_dir = project.tasks_dir.parent / "review"
|
|
499
|
+
if review_dir.is_symlink() or not review_dir.is_dir():
|
|
500
|
+
return {}
|
|
501
|
+
|
|
502
|
+
latest: Dict[int, int] = {}
|
|
503
|
+
verdicts: Dict[Tuple[int, int], List[str]] = {}
|
|
504
|
+
for path in sorted(review_dir.iterdir()):
|
|
505
|
+
named = check_handoffs.WAVE_REVIEW_NAME.fullmatch(path.name)
|
|
506
|
+
if named is None or path.is_symlink() or not path.is_file():
|
|
507
|
+
continue
|
|
508
|
+
wave, cycle = int(named.group("wave")), int(named.group("cycle"))
|
|
509
|
+
latest[wave] = max(latest.get(wave, 0), cycle)
|
|
510
|
+
try:
|
|
511
|
+
text = path.read_text(encoding="utf-8")
|
|
512
|
+
except (OSError, UnicodeDecodeError):
|
|
513
|
+
continue
|
|
514
|
+
verdict = REVIEW_VERDICT_RE.search(text)
|
|
515
|
+
if verdict is not None:
|
|
516
|
+
verdicts.setdefault((wave, cycle), []).append(verdict.group("verdict"))
|
|
517
|
+
|
|
518
|
+
return {
|
|
519
|
+
wave: cycle
|
|
520
|
+
for wave, cycle in latest.items()
|
|
521
|
+
if "blocked" in verdicts.get((wave, cycle), [])
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
|
|
488
525
|
def ready(repo: str, project_dir: str = DEFAULT_PROJECT_DIR, *,
|
|
489
526
|
allow_done: bool = False) -> Dict[str, object]:
|
|
490
527
|
"""Return task readiness, optionally accepting build/done for completion checks."""
|
|
@@ -494,6 +531,39 @@ def ready(repo: str, project_dir: str = DEFAULT_PROJECT_DIR, *,
|
|
|
494
531
|
by_id = {task.task_id: task for task in project.tasks}
|
|
495
532
|
unfinished_waves = sorted({task.wave for task in project.tasks if task.status != "done"})
|
|
496
533
|
current_wave = unfinished_waves[0] if unfinished_waves else None
|
|
534
|
+
blocked_reviews = _blocked_wave_reviews(project)
|
|
535
|
+
blocked_wave = min(
|
|
536
|
+
(
|
|
537
|
+
wave
|
|
538
|
+
for wave in blocked_reviews
|
|
539
|
+
if (current_wave is None or wave < current_wave)
|
|
540
|
+
and any(task.wave == wave for task in project.tasks)
|
|
541
|
+
and all(task.status == "done" for task in project.tasks if task.wave == wave)
|
|
542
|
+
),
|
|
543
|
+
default=None,
|
|
544
|
+
)
|
|
545
|
+
repair_for_wave: Optional[int] = None
|
|
546
|
+
blocked_review: Optional[Dict[str, object]] = None
|
|
547
|
+
if blocked_wave is not None:
|
|
548
|
+
source_ids = {task.task_id for task in project.tasks if task.wave == blocked_wave}
|
|
549
|
+
repairs = [
|
|
550
|
+
task
|
|
551
|
+
for task in project.tasks
|
|
552
|
+
if task.status != "done"
|
|
553
|
+
and task.wave > blocked_wave
|
|
554
|
+
and task.review_findings
|
|
555
|
+
and source_ids.intersection(task.deps)
|
|
556
|
+
]
|
|
557
|
+
if repairs:
|
|
558
|
+
repair_for_wave = blocked_wave
|
|
559
|
+
current_wave = min(task.wave for task in repairs)
|
|
560
|
+
else:
|
|
561
|
+
current_wave = blocked_wave
|
|
562
|
+
blocked_review = {
|
|
563
|
+
"wave": blocked_wave,
|
|
564
|
+
"cycle": blocked_reviews[blocked_wave],
|
|
565
|
+
"waiting_waves": unfinished_waves,
|
|
566
|
+
}
|
|
497
567
|
active = [task for task in project.tasks if task.status == "in-progress"]
|
|
498
568
|
if current_wave is not None and any(task.wave != current_wave for task in active):
|
|
499
569
|
task = next(task for task in active if task.wave != current_wave)
|
|
@@ -536,7 +606,7 @@ def ready(repo: str, project_dir: str = DEFAULT_PROJECT_DIR, *,
|
|
|
536
606
|
)
|
|
537
607
|
|
|
538
608
|
wave_active = any(task.wave == current_wave for task in active)
|
|
539
|
-
if current_wave is not None and not selectable and not wave_active:
|
|
609
|
+
if current_wave is not None and blocked_review is None and not selectable and not wave_active:
|
|
540
610
|
stalled = [
|
|
541
611
|
{
|
|
542
612
|
"id": task.task_id,
|
|
@@ -556,11 +626,17 @@ def ready(repo: str, project_dir: str = DEFAULT_PROJECT_DIR, *,
|
|
|
556
626
|
{"wave": current_wave, "tasks": stalled},
|
|
557
627
|
)
|
|
558
628
|
|
|
629
|
+
typed: Dict[str, object] = {}
|
|
630
|
+
if repair_for_wave is not None:
|
|
631
|
+
typed["repair_for_wave"] = repair_for_wave
|
|
632
|
+
if blocked_review is not None:
|
|
633
|
+
typed["blocked_review"] = blocked_review
|
|
559
634
|
return {
|
|
560
635
|
"command": "ready",
|
|
561
636
|
"branch": project.branch,
|
|
562
637
|
"head": project.head,
|
|
563
638
|
"current_wave": current_wave,
|
|
639
|
+
**typed,
|
|
564
640
|
"ready": [
|
|
565
641
|
{
|
|
566
642
|
"id": task.task_id,
|