@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
|
@@ -55,10 +55,22 @@ try:
|
|
|
55
55
|
fcntl.flock(fd, fcntl.LOCK_EX)
|
|
56
56
|
|
|
57
57
|
except ImportError: # Windows does not provide POSIX file locks.
|
|
58
|
+
import errno
|
|
58
59
|
import msvcrt
|
|
60
|
+
import time
|
|
59
61
|
|
|
60
62
|
def lock_exclusive(fd: int) -> None:
|
|
61
|
-
|
|
63
|
+
delay = 0.01
|
|
64
|
+
while True:
|
|
65
|
+
try:
|
|
66
|
+
os.lseek(fd, 0, os.SEEK_SET)
|
|
67
|
+
msvcrt.locking(fd, msvcrt.LK_NBLCK, 1)
|
|
68
|
+
return
|
|
69
|
+
except OSError as error:
|
|
70
|
+
if error.errno not in (errno.EACCES, errno.EAGAIN, errno.EDEADLK):
|
|
71
|
+
raise
|
|
72
|
+
time.sleep(delay)
|
|
73
|
+
delay = min(delay * 2, 0.25)
|
|
62
74
|
|
|
63
75
|
|
|
64
76
|
IGNORE_DIRS = {
|
|
@@ -217,16 +229,20 @@ HTML_BLOCK_START = re.compile(
|
|
|
217
229
|
re.IGNORECASE,
|
|
218
230
|
)
|
|
219
231
|
REPARSE_NAME_SURROGATE = 0x20000000
|
|
220
|
-
|
|
232
|
+
_POSIX_ANCHORED_EVIDENCE_SUPPORTED = (
|
|
221
233
|
hasattr(os, "O_DIRECTORY")
|
|
222
234
|
and hasattr(os, "O_NOFOLLOW")
|
|
223
235
|
and os.open in os.supports_dir_fd
|
|
224
236
|
and os.stat in os.supports_dir_fd
|
|
225
237
|
and os.stat in os.supports_follow_symlinks
|
|
226
238
|
)
|
|
239
|
+
WINDOWS_ANCHORED_EVIDENCE_SUPPORTED = os.name == "nt"
|
|
240
|
+
ANCHORED_EVIDENCE_SUPPORTED = (
|
|
241
|
+
_POSIX_ANCHORED_EVIDENCE_SUPPORTED or WINDOWS_ANCHORED_EVIDENCE_SUPPORTED
|
|
242
|
+
)
|
|
227
243
|
LISTDIR_DIR_FD_SUPPORTED = os.listdir in getattr(os, "supports_fd", ())
|
|
228
244
|
ANCHORED_STATE_CREATE_SUPPORTED = (
|
|
229
|
-
|
|
245
|
+
_POSIX_ANCHORED_EVIDENCE_SUPPORTED
|
|
230
246
|
and LISTDIR_DIR_FD_SUPPORTED
|
|
231
247
|
and os.mkdir in getattr(os, "supports_dir_fd", ())
|
|
232
248
|
and os.unlink in getattr(os, "supports_dir_fd", ())
|
|
@@ -269,11 +285,12 @@ def is_link_like_status(status: os.stat_result) -> bool:
|
|
|
269
285
|
def is_link_like(path: Path, status: os.stat_result) -> bool:
|
|
270
286
|
if is_link_like_status(status):
|
|
271
287
|
return True
|
|
272
|
-
|
|
273
|
-
if os.name != "nt" or isjunction is None:
|
|
288
|
+
if os.name != "nt":
|
|
274
289
|
return False
|
|
275
290
|
try:
|
|
276
|
-
return
|
|
291
|
+
return is_link_like_status(os.lstat(path))
|
|
292
|
+
except FileNotFoundError:
|
|
293
|
+
return False
|
|
277
294
|
except OSError as error:
|
|
278
295
|
raise DetectError(f"cannot inspect link-like evidence: {path}: {error}") from error
|
|
279
296
|
|
|
@@ -332,7 +349,7 @@ def is_managed_pipeline_directory(relative: str, root: Path) -> bool:
|
|
|
332
349
|
return is_verified_skill_bundle(relative, root)
|
|
333
350
|
|
|
334
351
|
|
|
335
|
-
def is_verified_skill_bundle_at(relative: str, directory_fd: int) -> bool:
|
|
352
|
+
def is_verified_skill_bundle_at(relative: str, directory_fd: int, root: Path) -> bool:
|
|
336
353
|
parts = PurePosixPath(relative).parts
|
|
337
354
|
if not parts:
|
|
338
355
|
return False
|
|
@@ -342,19 +359,45 @@ def is_verified_skill_bundle_at(relative: str, directory_fd: int) -> bool:
|
|
|
342
359
|
return False
|
|
343
360
|
if is_verified_installer_bundle(parts):
|
|
344
361
|
return True
|
|
362
|
+
if _POSIX_ANCHORED_EVIDENCE_SUPPORTED:
|
|
363
|
+
try:
|
|
364
|
+
status = os.stat(
|
|
365
|
+
"SKILL.md",
|
|
366
|
+
dir_fd=directory_fd,
|
|
367
|
+
follow_symlinks=False,
|
|
368
|
+
)
|
|
369
|
+
except FileNotFoundError:
|
|
370
|
+
return False
|
|
371
|
+
except OSError as error:
|
|
372
|
+
raise DetectError(
|
|
373
|
+
f"cannot inspect managed skill bundle: {relative}: {error}"
|
|
374
|
+
) from error
|
|
375
|
+
return not is_link_like_status(status) and stat.S_ISREG(status.st_mode)
|
|
376
|
+
skill_fd = None
|
|
345
377
|
try:
|
|
346
|
-
|
|
378
|
+
win = get_windows_handles()
|
|
379
|
+
skill_fd = win.open_relative(
|
|
380
|
+
directory_fd,
|
|
347
381
|
"SKILL.md",
|
|
348
|
-
|
|
349
|
-
|
|
382
|
+
_FILE_READ_ATTRIBUTES,
|
|
383
|
+
_PIN_SHARE,
|
|
384
|
+
_FILE_OPEN,
|
|
385
|
+
0,
|
|
350
386
|
)
|
|
387
|
+
status = os.fstat(skill_fd)
|
|
351
388
|
except FileNotFoundError:
|
|
352
389
|
return False
|
|
353
390
|
except OSError as error:
|
|
391
|
+
if getattr(error, "winerror", None) == 2:
|
|
392
|
+
return False
|
|
354
393
|
raise DetectError(
|
|
355
394
|
f"cannot inspect managed skill bundle: {relative}: {error}"
|
|
356
395
|
) from error
|
|
357
|
-
|
|
396
|
+
finally:
|
|
397
|
+
if skill_fd is not None:
|
|
398
|
+
os.close(skill_fd)
|
|
399
|
+
skill = root.joinpath(*parts) / "SKILL.md"
|
|
400
|
+
return not is_link_like(skill, status) and stat.S_ISREG(status.st_mode)
|
|
358
401
|
|
|
359
402
|
|
|
360
403
|
def is_fixed_managed_pipeline_artifact(relative: str) -> bool:
|
|
@@ -423,13 +466,20 @@ def read_regular_evidence(
|
|
|
423
466
|
) from error
|
|
424
467
|
if not relative.parts:
|
|
425
468
|
raise DetectError(f"{evidence_name} evidence path is empty: {path}")
|
|
426
|
-
if
|
|
469
|
+
if _POSIX_ANCHORED_EVIDENCE_SUPPORTED:
|
|
427
470
|
return read_anchored_evidence(
|
|
428
471
|
relative,
|
|
429
472
|
root,
|
|
430
473
|
missing_ok=missing_ok,
|
|
431
474
|
evidence_name=evidence_name,
|
|
432
475
|
)
|
|
476
|
+
if WINDOWS_ANCHORED_EVIDENCE_SUPPORTED:
|
|
477
|
+
return read_windows_anchored_evidence(
|
|
478
|
+
relative,
|
|
479
|
+
root,
|
|
480
|
+
missing_ok=missing_ok,
|
|
481
|
+
evidence_name=evidence_name,
|
|
482
|
+
)
|
|
433
483
|
return read_lstat_open_evidence(
|
|
434
484
|
relative,
|
|
435
485
|
root,
|
|
@@ -447,6 +497,17 @@ def directory_flags() -> int:
|
|
|
447
497
|
)
|
|
448
498
|
|
|
449
499
|
|
|
500
|
+
def listdir_anchored(dir_fd: int) -> list[str]:
|
|
501
|
+
"""List names through a fresh directory descriptor under dir_fd."""
|
|
502
|
+
fresh = os.open(".", directory_flags(), dir_fd=dir_fd)
|
|
503
|
+
try:
|
|
504
|
+
if not os.path.samestat(os.fstat(fresh), os.fstat(dir_fd)):
|
|
505
|
+
raise DetectError("anchored directory listing lost its identity")
|
|
506
|
+
return os.listdir(fresh)
|
|
507
|
+
finally:
|
|
508
|
+
os.close(fresh)
|
|
509
|
+
|
|
510
|
+
|
|
450
511
|
def file_flags() -> int:
|
|
451
512
|
return (
|
|
452
513
|
os.O_RDONLY
|
|
@@ -766,9 +827,12 @@ def raise_walk_error(error: OSError) -> None:
|
|
|
766
827
|
|
|
767
828
|
|
|
768
829
|
def iter_worktree_files(root: Path) -> Iterable[str]:
|
|
769
|
-
if
|
|
830
|
+
if _POSIX_ANCHORED_EVIDENCE_SUPPORTED and LISTDIR_DIR_FD_SUPPORTED:
|
|
770
831
|
yield from iter_worktree_files_anchored(root)
|
|
771
832
|
return
|
|
833
|
+
if WINDOWS_ANCHORED_EVIDENCE_SUPPORTED:
|
|
834
|
+
yield from iter_worktree_files_windows_anchored(root)
|
|
835
|
+
return
|
|
772
836
|
for dirpath, dirnames, filenames in os.walk(
|
|
773
837
|
root,
|
|
774
838
|
followlinks=False,
|
|
@@ -826,7 +890,7 @@ def iter_from_dir_fd(
|
|
|
826
890
|
dir_fd: int, relative_dir: str, root: Path
|
|
827
891
|
) -> Iterable[str]:
|
|
828
892
|
try:
|
|
829
|
-
names =
|
|
893
|
+
names = listdir_anchored(dir_fd)
|
|
830
894
|
except OSError as error:
|
|
831
895
|
raise DetectError(f"cannot traverse filesystem evidence: {error}") from error
|
|
832
896
|
for name in names:
|
|
@@ -862,7 +926,7 @@ def iter_from_dir_fd(
|
|
|
862
926
|
raise DetectError(
|
|
863
927
|
f"filesystem evidence changed while traversing: {relative}"
|
|
864
928
|
)
|
|
865
|
-
if is_verified_skill_bundle_at(relative, child):
|
|
929
|
+
if is_verified_skill_bundle_at(relative, child, root):
|
|
866
930
|
continue
|
|
867
931
|
yield from iter_from_dir_fd(child, relative, root)
|
|
868
932
|
finally:
|
|
@@ -1302,7 +1366,7 @@ def close_file_descriptors(
|
|
|
1302
1366
|
|
|
1303
1367
|
def project_entries(project_fd: int, project: Path) -> set:
|
|
1304
1368
|
"""Names in .project without ignored OS junk (Finder may add .DS_Store at any time)."""
|
|
1305
|
-
entries = set(
|
|
1369
|
+
entries = set(listdir_anchored(project_fd))
|
|
1306
1370
|
for name in entries & _common.OS_JUNK_NAMES:
|
|
1307
1371
|
try:
|
|
1308
1372
|
status = os.stat(name, dir_fd=project_fd, follow_symlinks=False)
|
|
@@ -1413,12 +1477,12 @@ def write_state_anchored(
|
|
|
1413
1477
|
follow_symlinks=False,
|
|
1414
1478
|
)
|
|
1415
1479
|
except FileNotFoundError as error:
|
|
1416
|
-
if "STATE.md" in
|
|
1480
|
+
if "STATE.md" in listdir_anchored(project_fd):
|
|
1417
1481
|
raise DetectError("STATE.md already exists") from error
|
|
1418
1482
|
raise DetectError("temporary STATE.md disappeared") from error
|
|
1419
1483
|
if not os.path.samestat(created_status, named_temporary):
|
|
1420
1484
|
raise DetectError("temporary STATE.md changed before writing")
|
|
1421
|
-
if "STATE.md" in
|
|
1485
|
+
if "STATE.md" in listdir_anchored(project_fd):
|
|
1422
1486
|
raise DetectError("STATE.md already exists")
|
|
1423
1487
|
os.ftruncate(state_fd, 0)
|
|
1424
1488
|
write_all(state_fd, payload)
|
|
@@ -1653,6 +1717,167 @@ class _WindowsHandles:
|
|
|
1653
1717
|
raise ctypes.WinError(ctypes.get_last_error())
|
|
1654
1718
|
|
|
1655
1719
|
|
|
1720
|
+
_WINDOWS_HANDLES: Optional[_WindowsHandles] = None
|
|
1721
|
+
|
|
1722
|
+
|
|
1723
|
+
def get_windows_handles() -> _WindowsHandles:
|
|
1724
|
+
global _WINDOWS_HANDLES
|
|
1725
|
+
if _WINDOWS_HANDLES is None:
|
|
1726
|
+
_WINDOWS_HANDLES = _WindowsHandles()
|
|
1727
|
+
return _WINDOWS_HANDLES
|
|
1728
|
+
|
|
1729
|
+
|
|
1730
|
+
def _windows_missing_ok(error: OSError) -> bool:
|
|
1731
|
+
return getattr(error, "winerror", None) in {2, 3}
|
|
1732
|
+
|
|
1733
|
+
|
|
1734
|
+
def read_windows_anchored_evidence(
|
|
1735
|
+
relative: Path,
|
|
1736
|
+
root: Path,
|
|
1737
|
+
*,
|
|
1738
|
+
missing_ok: bool,
|
|
1739
|
+
evidence_name: str,
|
|
1740
|
+
) -> Optional[str]:
|
|
1741
|
+
win = get_windows_handles()
|
|
1742
|
+
descriptors: list[int] = []
|
|
1743
|
+
try:
|
|
1744
|
+
descriptors.append(win.open_root(root))
|
|
1745
|
+
opened_root = os.fstat(descriptors[-1])
|
|
1746
|
+
if not stat.S_ISDIR(opened_root.st_mode):
|
|
1747
|
+
raise DetectError(f"repo evidence is not a directory: {root}")
|
|
1748
|
+
for index, part in enumerate(relative.parts):
|
|
1749
|
+
current = root.joinpath(*relative.parts[: index + 1])
|
|
1750
|
+
final = index == len(relative.parts) - 1
|
|
1751
|
+
access = _GENERIC_READ if final else _DIRECTORY_ACCESS
|
|
1752
|
+
options = _FILE_NON_DIRECTORY_FILE if final else _FILE_DIRECTORY_FILE
|
|
1753
|
+
try:
|
|
1754
|
+
descriptor = win.open_relative(
|
|
1755
|
+
descriptors[-1],
|
|
1756
|
+
part,
|
|
1757
|
+
access,
|
|
1758
|
+
_PIN_SHARE,
|
|
1759
|
+
_FILE_OPEN,
|
|
1760
|
+
options,
|
|
1761
|
+
)
|
|
1762
|
+
except OSError as error:
|
|
1763
|
+
if missing_ok and _windows_missing_ok(error):
|
|
1764
|
+
return None
|
|
1765
|
+
raise DetectError(
|
|
1766
|
+
f"filesystem evidence disappeared: {root / relative}"
|
|
1767
|
+
) from error
|
|
1768
|
+
descriptors.append(descriptor)
|
|
1769
|
+
actual = os.fstat(descriptor)
|
|
1770
|
+
if is_link_like(current, actual):
|
|
1771
|
+
raise DetectError(
|
|
1772
|
+
f"link-like {evidence_name} evidence is not allowed: {current}"
|
|
1773
|
+
)
|
|
1774
|
+
if final and not stat.S_ISREG(actual.st_mode):
|
|
1775
|
+
raise DetectError(
|
|
1776
|
+
f"{evidence_name} evidence is not a regular file: {current}"
|
|
1777
|
+
)
|
|
1778
|
+
if not final and not stat.S_ISDIR(actual.st_mode):
|
|
1779
|
+
raise DetectError(
|
|
1780
|
+
f"{evidence_name} evidence parent is not a directory: {current}"
|
|
1781
|
+
)
|
|
1782
|
+
with os.fdopen(descriptors.pop(), "rb") as stream:
|
|
1783
|
+
data = stream.read()
|
|
1784
|
+
except DetectError:
|
|
1785
|
+
raise
|
|
1786
|
+
except OSError as error:
|
|
1787
|
+
raise DetectError(
|
|
1788
|
+
f"cannot read {evidence_name} evidence: {root / relative}"
|
|
1789
|
+
) from error
|
|
1790
|
+
finally:
|
|
1791
|
+
for descriptor in reversed(descriptors):
|
|
1792
|
+
os.close(descriptor)
|
|
1793
|
+
try:
|
|
1794
|
+
return data.decode("utf-8")
|
|
1795
|
+
except UnicodeDecodeError as error:
|
|
1796
|
+
raise DetectError(
|
|
1797
|
+
f"cannot read {evidence_name} evidence: {root / relative}"
|
|
1798
|
+
) from error
|
|
1799
|
+
|
|
1800
|
+
|
|
1801
|
+
def iter_worktree_files_windows_anchored(root: Path) -> Iterable[str]:
|
|
1802
|
+
win = get_windows_handles()
|
|
1803
|
+
try:
|
|
1804
|
+
root_fd = win.open_root(root)
|
|
1805
|
+
except OSError as error:
|
|
1806
|
+
raise DetectError(f"cannot traverse filesystem evidence: {error}") from error
|
|
1807
|
+
try:
|
|
1808
|
+
root_status = os.fstat(root_fd)
|
|
1809
|
+
if not stat.S_ISDIR(root_status.st_mode):
|
|
1810
|
+
raise DetectError(f"repo evidence is not a directory: {root}")
|
|
1811
|
+
yield from iter_from_windows_dir_fd(root_fd, "", root, win)
|
|
1812
|
+
finally:
|
|
1813
|
+
os.close(root_fd)
|
|
1814
|
+
|
|
1815
|
+
|
|
1816
|
+
def iter_from_windows_dir_fd(
|
|
1817
|
+
dir_fd: int, relative_dir: str, root: Path, win: _WindowsHandles
|
|
1818
|
+
) -> Iterable[str]:
|
|
1819
|
+
scan_dir = root if not relative_dir else root.joinpath(*PurePosixPath(relative_dir).parts)
|
|
1820
|
+
try:
|
|
1821
|
+
# Windows CPython cannot listdir(dir_fd); paths come from the pinned walk.
|
|
1822
|
+
names = os.listdir(scan_dir)
|
|
1823
|
+
except OSError as error:
|
|
1824
|
+
raise DetectError(f"cannot traverse filesystem evidence: {error}") from error
|
|
1825
|
+
for name in names:
|
|
1826
|
+
relative = f"{relative_dir}/{name}" if relative_dir else name
|
|
1827
|
+
if is_ignored(relative) or name == ".project":
|
|
1828
|
+
continue
|
|
1829
|
+
path = root.joinpath(*PurePosixPath(relative).parts)
|
|
1830
|
+
try:
|
|
1831
|
+
child_fd = win.open_relative(
|
|
1832
|
+
dir_fd,
|
|
1833
|
+
name,
|
|
1834
|
+
_DIRECTORY_ACCESS,
|
|
1835
|
+
_PIN_SHARE,
|
|
1836
|
+
_FILE_OPEN,
|
|
1837
|
+
_FILE_DIRECTORY_FILE,
|
|
1838
|
+
)
|
|
1839
|
+
except OSError:
|
|
1840
|
+
child_fd = None
|
|
1841
|
+
else:
|
|
1842
|
+
status = os.fstat(child_fd)
|
|
1843
|
+
if is_link_like(path, status):
|
|
1844
|
+
os.close(child_fd)
|
|
1845
|
+
continue
|
|
1846
|
+
if stat.S_ISDIR(status.st_mode):
|
|
1847
|
+
try:
|
|
1848
|
+
if is_verified_skill_bundle_at(relative, child_fd, root):
|
|
1849
|
+
continue
|
|
1850
|
+
yield from iter_from_windows_dir_fd(
|
|
1851
|
+
child_fd, relative, root, win
|
|
1852
|
+
)
|
|
1853
|
+
finally:
|
|
1854
|
+
os.close(child_fd)
|
|
1855
|
+
continue
|
|
1856
|
+
os.close(child_fd)
|
|
1857
|
+
try:
|
|
1858
|
+
file_fd = win.open_relative(
|
|
1859
|
+
dir_fd,
|
|
1860
|
+
name,
|
|
1861
|
+
_FILE_READ_ATTRIBUTES,
|
|
1862
|
+
_PIN_SHARE,
|
|
1863
|
+
_FILE_OPEN,
|
|
1864
|
+
_FILE_NON_DIRECTORY_FILE,
|
|
1865
|
+
)
|
|
1866
|
+
except OSError as error:
|
|
1867
|
+
raise DetectError(
|
|
1868
|
+
f"cannot inspect filesystem evidence: {relative}: {error}"
|
|
1869
|
+
) from error
|
|
1870
|
+
try:
|
|
1871
|
+
status = os.fstat(file_fd)
|
|
1872
|
+
if is_link_like(path, status) or not stat.S_ISREG(status.st_mode):
|
|
1873
|
+
continue
|
|
1874
|
+
if is_fixed_managed_pipeline_artifact(relative):
|
|
1875
|
+
continue
|
|
1876
|
+
yield relative
|
|
1877
|
+
finally:
|
|
1878
|
+
os.close(file_fd)
|
|
1879
|
+
|
|
1880
|
+
|
|
1656
1881
|
def windows_project_entries(project: Path) -> set:
|
|
1657
1882
|
"""project_entries for a .project held open without delete sharing."""
|
|
1658
1883
|
entries = set(os.listdir(project))
|
|
@@ -467,15 +467,17 @@ def completed_field(lines: Sequence[str], field: str, artifact: str) -> str:
|
|
|
467
467
|
|
|
468
468
|
|
|
469
469
|
def completed_bullet_field(lines: Sequence[str], field: str, artifact: str) -> str:
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
for line in lines:
|
|
473
|
-
match = pattern.fullmatch(line)
|
|
474
|
-
if match:
|
|
475
|
-
values.append(match.group(1).strip().strip("`"))
|
|
476
|
-
if len(values) != 1 or not values[0] or archive_milestone.contains_placeholder(values[0]):
|
|
470
|
+
matches = _common.find_review_bullet_field_values(lines, field)
|
|
471
|
+
if len(matches) != 1:
|
|
477
472
|
raise ArchiveError(f"{artifact} requires one completed {field} field")
|
|
478
|
-
|
|
473
|
+
raw_value = matches[0].strip()
|
|
474
|
+
value = raw_value.strip("`")
|
|
475
|
+
# Scan the value as written. Removing an outer backtick first can unbalance
|
|
476
|
+
# every code span after a leading or trailing one. contains_placeholder
|
|
477
|
+
# already handles a wholly quoted placeholder such as `<command>`.
|
|
478
|
+
if not value or archive_milestone.contains_placeholder(raw_value):
|
|
479
|
+
raise ArchiveError(f"{artifact} requires one completed {field} field")
|
|
480
|
+
return value
|
|
479
481
|
|
|
480
482
|
|
|
481
483
|
def section_lines(lines: Sequence[str], heading: str, artifact: str) -> Sequence[str]:
|
|
@@ -509,6 +511,12 @@ def archived_intent_criteria(archive: Path) -> dict[int, str]:
|
|
|
509
511
|
|
|
510
512
|
|
|
511
513
|
def parse_final_review(archive: Path) -> tuple:
|
|
514
|
+
reviewed_head, criteria, _raw_evidence = _final_review(archive)
|
|
515
|
+
return reviewed_head, criteria
|
|
516
|
+
|
|
517
|
+
|
|
518
|
+
def _final_review(archive: Path) -> tuple:
|
|
519
|
+
"""Parse FINAL.md and retain evidence as written for placeholder scans."""
|
|
512
520
|
if __package__:
|
|
513
521
|
from . import check_handoffs
|
|
514
522
|
else:
|
|
@@ -562,18 +570,21 @@ def parse_final_review(archive: Path) -> tuple:
|
|
|
562
570
|
|
|
563
571
|
expected_fields = ("Verdict", "Check", "Observed", "Reference", "Finding", "Fix direction")
|
|
564
572
|
criteria = []
|
|
573
|
+
raw_evidence = []
|
|
565
574
|
for position, (heading_index, _, criterion) in enumerate(headings):
|
|
566
575
|
section_start = heading_index + 1
|
|
567
576
|
section_end = headings[position + 1][0] if position + 1 < len(headings) else criteria_end
|
|
577
|
+
section = lines[section_start:section_end]
|
|
568
578
|
values = {}
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
key, value = match.groups()
|
|
574
|
-
if key in values:
|
|
579
|
+
raw_values = {}
|
|
580
|
+
for key in expected_fields:
|
|
581
|
+
matches = _common.find_review_bullet_field_values(section, key)
|
|
582
|
+
if len(matches) > 1:
|
|
575
583
|
raise ArchiveError(f"FINAL.md repeats {key} for {criterion}")
|
|
576
|
-
|
|
584
|
+
if len(matches) != 1:
|
|
585
|
+
raise ArchiveError(f"FINAL.md criterion is incomplete: {criterion}")
|
|
586
|
+
raw_values[key] = matches[0].strip()
|
|
587
|
+
values[key] = raw_values[key].strip("`")
|
|
577
588
|
surface_values = [
|
|
578
589
|
line.removeprefix("- **Surface**:").strip().strip("`")
|
|
579
590
|
for line in lines[section_start:section_end]
|
|
@@ -593,18 +604,19 @@ def parse_final_review(archive: Path) -> tuple:
|
|
|
593
604
|
check_handoffs._surface_value(value, f"FINAL.md surface {field}")
|
|
594
605
|
except check_handoffs.HandoffError as error:
|
|
595
606
|
raise ArchiveError(str(error)) from error
|
|
596
|
-
elif not value or archive_milestone.contains_placeholder(
|
|
607
|
+
elif not value or archive_milestone.contains_placeholder(raw_values[field]):
|
|
597
608
|
raise ArchiveError(f"FINAL.md criterion is incomplete: {criterion}")
|
|
598
609
|
if values["Verdict"] != "met":
|
|
599
610
|
raise ArchiveError(f"FINAL.md criterion lacks passing evidence: {criterion}")
|
|
600
|
-
|
|
601
|
-
(
|
|
611
|
+
evidence_field = next(
|
|
612
|
+
(field for field in ("Reference", "Check", "Observed") if values[field] != "none"),
|
|
602
613
|
None,
|
|
603
614
|
)
|
|
604
|
-
if
|
|
615
|
+
if evidence_field is None:
|
|
605
616
|
raise ArchiveError(f"FINAL.md criterion lacks passing evidence: {criterion}")
|
|
606
|
-
criteria.append((criterion, "met",
|
|
607
|
-
|
|
617
|
+
criteria.append((criterion, "met", values[evidence_field]))
|
|
618
|
+
raw_evidence.append(raw_values[evidence_field])
|
|
619
|
+
return reviewed_head.lower(), criteria, raw_evidence
|
|
608
620
|
|
|
609
621
|
|
|
610
622
|
def _member_heads(text: str, source: str) -> dict:
|
|
@@ -814,10 +826,14 @@ def validate_wave_review(
|
|
|
814
826
|
expected_depth: str,
|
|
815
827
|
expected_lens: Optional[str] = None,
|
|
816
828
|
task_texts: Optional[dict[str, str]] = None,
|
|
829
|
+
*,
|
|
830
|
+
is_last_cycle: bool = True,
|
|
817
831
|
) -> Sequence[str]:
|
|
818
832
|
lines = path.read_text(encoding="utf-8").splitlines()
|
|
819
833
|
depth = completed_field(lines, "Depth:", path.name)
|
|
820
|
-
if depth
|
|
834
|
+
if depth not in {"full", "deep", "verify-only"}:
|
|
835
|
+
raise ArchiveError(f"{path.name} has an invalid review depth")
|
|
836
|
+
if is_last_cycle and depth != expected_depth:
|
|
821
837
|
raise ArchiveError(f"{path.name} has an invalid review depth")
|
|
822
838
|
if expected_lens is not None:
|
|
823
839
|
lens = completed_field(lines, "Lens:", path.name)
|
|
@@ -842,10 +858,32 @@ def validate_wave_review(
|
|
|
842
858
|
normalized_expected = [
|
|
843
859
|
(task_id, " ".join(title.split())) for task_id, title in expected_tasks
|
|
844
860
|
]
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
861
|
+
expected_by_id = dict(normalized_expected)
|
|
862
|
+
enforce_plan_tasks = is_last_cycle and wave_verdict != "blocked"
|
|
863
|
+
if enforce_plan_tasks:
|
|
864
|
+
if actual_tasks != normalized_expected:
|
|
865
|
+
raise ArchiveError(
|
|
866
|
+
f"{path.name} tasks and titles do not match its wave task files in order"
|
|
867
|
+
)
|
|
868
|
+
else:
|
|
869
|
+
known_ids = set(expected_by_id)
|
|
870
|
+
for task_id in task_ids:
|
|
871
|
+
if task_id not in known_ids:
|
|
872
|
+
raise ArchiveError(
|
|
873
|
+
f"{path.name} tasks and titles do not match its wave task files in order"
|
|
874
|
+
)
|
|
875
|
+
plan_order = [task_id for task_id, _title in normalized_expected]
|
|
876
|
+
reviewed_order = [task_id for task_id in plan_order if task_id in task_ids]
|
|
877
|
+
if task_ids != reviewed_order:
|
|
878
|
+
raise ArchiveError(
|
|
879
|
+
f"{path.name} tasks and titles do not match its wave task files in order"
|
|
880
|
+
)
|
|
881
|
+
for _index, task_id, title, _verdict in headings:
|
|
882
|
+
expected_title = expected_by_id.get(task_id)
|
|
883
|
+
if expected_title is not None and " ".join(title.split()) != expected_title:
|
|
884
|
+
raise ArchiveError(
|
|
885
|
+
f"{path.name} tasks and titles do not match its wave task files in order"
|
|
886
|
+
)
|
|
849
887
|
|
|
850
888
|
verdicts = []
|
|
851
889
|
for position, (heading_index, _task_id, _title, verdict) in enumerate(headings):
|
|
@@ -870,6 +908,9 @@ def validate_wave_review(
|
|
|
870
908
|
)
|
|
871
909
|
verdicts.append(verdict)
|
|
872
910
|
|
|
911
|
+
if not is_last_cycle:
|
|
912
|
+
return verdicts
|
|
913
|
+
|
|
873
914
|
coverage_indexes = [
|
|
874
915
|
index for index, line in enumerate(lines) if line == "## Intent coverage"
|
|
875
916
|
]
|
|
@@ -1083,10 +1124,17 @@ def review_cycle_counts(archive: Path) -> Sequence[int]:
|
|
|
1083
1124
|
depth,
|
|
1084
1125
|
lens,
|
|
1085
1126
|
task_texts,
|
|
1127
|
+
is_last_cycle=cycle == max(cycles),
|
|
1086
1128
|
)
|
|
1087
1129
|
)
|
|
1088
1130
|
|
|
1089
1131
|
last_cycle = max(cycles)
|
|
1132
|
+
for cycle, verdicts in wave_verdicts.items():
|
|
1133
|
+
if cycle != last_cycle and "blocked" not in verdicts:
|
|
1134
|
+
raise ArchiveError(
|
|
1135
|
+
f"superseded review cycle {cycle} for wave {wave} "
|
|
1136
|
+
"must include a blocked Wave verdict"
|
|
1137
|
+
)
|
|
1090
1138
|
if any(verdict != "pass" for verdict in wave_verdicts[last_cycle]):
|
|
1091
1139
|
raise ArchiveError(f"last review cycle for wave {wave} did not pass")
|
|
1092
1140
|
if any(task_verdict != "pass" for task_verdict in task_verdicts[last_cycle]):
|
|
@@ -1220,7 +1268,7 @@ def validate_manifest(project: Path, archive: Path, state: PipelineState) -> tup
|
|
|
1220
1268
|
if fields["Final verdict:"] != "all criteria met; project verify passed":
|
|
1221
1269
|
raise ArchiveError("manifest final verdict does not record the passed final gate")
|
|
1222
1270
|
|
|
1223
|
-
reviewed_head, final_criteria =
|
|
1271
|
+
reviewed_head, final_criteria, final_raw_evidence = _final_review(archive)
|
|
1224
1272
|
validate_gap_reviews(archive, reviewed_head)
|
|
1225
1273
|
task_files, attested = archive_milestone.landed_task_evidence(project, archive / "tasks", reviewed_head)
|
|
1226
1274
|
cycle_counts = review_cycle_counts(archive)
|
|
@@ -1264,14 +1312,23 @@ def validate_manifest(project: Path, archive: Path, state: PipelineState) -> tup
|
|
|
1264
1312
|
continue
|
|
1265
1313
|
criteria_rows.append(cells)
|
|
1266
1314
|
criteria_line_indexes.add(index)
|
|
1315
|
+
|
|
1316
|
+
def evidence_as_written(position: int, row: Sequence[str]) -> str:
|
|
1317
|
+
# Manifest evidence stores FINAL.md's normalized value. Scan the source
|
|
1318
|
+
# text when the row matches, so its inner code spans stay balanced.
|
|
1319
|
+
if position < len(final_criteria) and tuple(row) == tuple(final_criteria[position]):
|
|
1320
|
+
return final_raw_evidence[position]
|
|
1321
|
+
return row[2]
|
|
1322
|
+
|
|
1267
1323
|
if not criteria_rows or any(
|
|
1268
1324
|
len(row) != 3
|
|
1269
1325
|
or not row[0]
|
|
1270
1326
|
or row[1] != "met"
|
|
1271
1327
|
or not row[2]
|
|
1272
1328
|
or row[2] == "none"
|
|
1273
|
-
or
|
|
1274
|
-
|
|
1329
|
+
or archive_milestone.contains_placeholder(row[1])
|
|
1330
|
+
or archive_milestone.contains_placeholder(evidence_as_written(position, row))
|
|
1331
|
+
for position, row in enumerate(criteria_rows)
|
|
1275
1332
|
):
|
|
1276
1333
|
raise ArchiveError("manifest success-criteria rows are incomplete")
|
|
1277
1334
|
if [tuple(row) for row in criteria_rows] != list(final_criteria):
|