@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
|
@@ -33,7 +33,11 @@ DISPATCH_PATTERN = re.compile(
|
|
|
33
33
|
r"^- `(?P<dimension>[^`]+)` — (?P<status>dispatched|skipped) "
|
|
34
34
|
r"→ (?P<output>[^—]+) — (?P<reason>.+)$"
|
|
35
35
|
)
|
|
36
|
-
QUESTION_PATTERN = re.compile(
|
|
36
|
+
QUESTION_PATTERN = re.compile(
|
|
37
|
+
r"^- (?P<fence>`+)(?!`)(?P<body>.+?)(?<!`)(?P=fence)(?!`) → "
|
|
38
|
+
r"(?P<dimensions>`[^`,\s]+`(?:,\s*`[^`,\s]+`)*)$"
|
|
39
|
+
)
|
|
40
|
+
QUESTION_PREFIX = "[RESEARCH] "
|
|
37
41
|
FINDING_PATTERN = re.compile(r"^### (?P<id>P\d{3}) — (?P<title>.+)$")
|
|
38
42
|
CRITERION_LOCATOR_PATTERN = re.compile(r"^SC[1-9]\d*$")
|
|
39
43
|
COMMENT_PATTERN = re.compile(r"<!--.*?-->", re.DOTALL)
|
|
@@ -56,6 +60,21 @@ WAVE_REVIEW_NAME = re.compile(
|
|
|
56
60
|
r"wave-(?P<wave>[1-9]\d*)\.cycle(?P<cycle>[1-9]\d*)"
|
|
57
61
|
r"(?:\.(?P<lens>contract|adversarial))?\.md$"
|
|
58
62
|
)
|
|
63
|
+
VALID_REVIEW_DEPTHS = frozenset({"full", "deep", "verify-only"})
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _max_wave_cycle(root: Path, project_dir: str, wave: int) -> int:
|
|
67
|
+
review_dir = root / PurePosixPath(project_dir) / "review"
|
|
68
|
+
if not review_dir.is_dir():
|
|
69
|
+
return 0
|
|
70
|
+
maximum = 0
|
|
71
|
+
for path in review_dir.iterdir():
|
|
72
|
+
named = WAVE_REVIEW_NAME.fullmatch(path.name)
|
|
73
|
+
if named and int(named.group("wave")) == wave:
|
|
74
|
+
maximum = max(maximum, int(named.group("cycle")))
|
|
75
|
+
return maximum
|
|
76
|
+
|
|
77
|
+
|
|
59
78
|
WAVE_SC_HEADING = re.compile(r"^### (SC[1-9]\d*) — (.+)$")
|
|
60
79
|
WAVE_TASK_HEADING = re.compile(
|
|
61
80
|
r"^## (?P<task>T\d{3}) — (?P<title>.+): (?P<verdict>pass|fail)$"
|
|
@@ -73,6 +92,23 @@ SURFACE_HEADING = re.compile(
|
|
|
73
92
|
r"(?m)^### (?P<surface>\S.*?) — (?P<task>T\d{3})\s*$"
|
|
74
93
|
)
|
|
75
94
|
SURFACE_CONTRACT_HEADING = re.compile(r"(?m)^## Surface contract\s*$")
|
|
95
|
+
# Spec-reach probes (see references/spec-probes.md). The closed edge taxonomy
|
|
96
|
+
# names the classic black-box categories; `none` records a criterion with no
|
|
97
|
+
# data-shape edge and must be dismissed with a reason.
|
|
98
|
+
EDGE_CATEGORIES = (
|
|
99
|
+
"boundary",
|
|
100
|
+
"adjacency",
|
|
101
|
+
"empty",
|
|
102
|
+
"encoding",
|
|
103
|
+
"ordering",
|
|
104
|
+
"precision",
|
|
105
|
+
"idempotency",
|
|
106
|
+
"concurrency",
|
|
107
|
+
)
|
|
108
|
+
EDGE_ID_PATTERN = re.compile(r"^E[1-9]\d*$")
|
|
109
|
+
PROHIBITION_ID_PATTERN = re.compile(r"^N[1-9]\d*$")
|
|
110
|
+
CRITERION_DISPOSITION_PATTERN = re.compile(r"^criterion\s+(SC[1-9]\d*)$")
|
|
111
|
+
TABLE_SEPARATOR_PATTERN = re.compile(r"^:?-+:?$")
|
|
76
112
|
GAP_NAME_PATTERN = re.compile(r"^final-gap-(?P<number>[1-9]\d*)\.md$")
|
|
77
113
|
GAP_HEADING_PATTERN = re.compile(
|
|
78
114
|
r"^# Gap Review — (?P<number>[1-9]\d*): (?P<risk>\S.*)$"
|
|
@@ -181,7 +217,7 @@ def _read(root: Path, relative: str) -> str:
|
|
|
181
217
|
path = root / relative
|
|
182
218
|
if not path.is_file() or path.is_symlink():
|
|
183
219
|
raise HandoffError(f"missing real hand-off file: {relative}")
|
|
184
|
-
return
|
|
220
|
+
return _common.read_user_text(path)
|
|
185
221
|
|
|
186
222
|
|
|
187
223
|
def _require_pipeline(root: Path, project_dir: str) -> Dict[str, str]:
|
|
@@ -361,7 +397,7 @@ def _research_questions(intent: str) -> List[str]:
|
|
|
361
397
|
]
|
|
362
398
|
|
|
363
399
|
|
|
364
|
-
def _research_assignments(section: str) -> List[Tuple[str, str]]:
|
|
400
|
+
def _research_assignments(section: str) -> List[Tuple[str, List[str]]]:
|
|
365
401
|
assignments = []
|
|
366
402
|
lines = [line.strip() for line in section.splitlines() if line.strip()]
|
|
367
403
|
if lines == ["- none"]:
|
|
@@ -372,7 +408,25 @@ def _research_assignments(section: str) -> List[Tuple[str, str]]:
|
|
|
372
408
|
match = QUESTION_PATTERN.fullmatch(line)
|
|
373
409
|
if match is None:
|
|
374
410
|
raise HandoffError("RESEARCH.md has a malformed question assignment")
|
|
375
|
-
|
|
411
|
+
body = match.group("body")
|
|
412
|
+
fence_length = len(match.group("fence"))
|
|
413
|
+
if any(len(run) == fence_length for run in re.findall(r"`+", body)):
|
|
414
|
+
raise HandoffError("RESEARCH.md has a malformed question assignment")
|
|
415
|
+
if (
|
|
416
|
+
body.startswith(" ")
|
|
417
|
+
and body.endswith(" ")
|
|
418
|
+
and body.strip(" ")
|
|
419
|
+
):
|
|
420
|
+
body = body[1:-1]
|
|
421
|
+
if not body.startswith(QUESTION_PREFIX):
|
|
422
|
+
raise HandoffError("RESEARCH.md has a malformed question assignment")
|
|
423
|
+
question = body[len(QUESTION_PREFIX) :]
|
|
424
|
+
if not question:
|
|
425
|
+
raise HandoffError("RESEARCH.md has a malformed question assignment")
|
|
426
|
+
dimensions = re.findall(r"`([^`]+)`", match.group("dimensions"))
|
|
427
|
+
if len(dimensions) != len(set(dimensions)):
|
|
428
|
+
raise HandoffError("RESEARCH.md repeats a target in a question assignment")
|
|
429
|
+
assignments.append((question, dimensions))
|
|
376
430
|
return assignments
|
|
377
431
|
|
|
378
432
|
|
|
@@ -446,19 +500,23 @@ def validate_research_artifacts(
|
|
|
446
500
|
|
|
447
501
|
assignments = _research_assignments(_section(handoff, "Question assignments"))
|
|
448
502
|
questions = _research_questions(intent)
|
|
449
|
-
assigned_questions = [question for question,
|
|
450
|
-
if sorted(assigned_questions) != sorted(questions):
|
|
451
|
-
raise HandoffError("RESEARCH.md question assignments do not match the intent source")
|
|
503
|
+
assigned_questions = [question for question, _dimensions in assignments]
|
|
452
504
|
if len(assigned_questions) != len(set(assigned_questions)):
|
|
453
505
|
raise HandoffError("RESEARCH.md repeats a question assignment")
|
|
506
|
+
if sorted(assigned_questions) != sorted(questions):
|
|
507
|
+
raise HandoffError("RESEARCH.md question assignments do not match the intent source")
|
|
454
508
|
dispatched_set = set(dispatched)
|
|
455
|
-
if any(
|
|
509
|
+
if any(
|
|
510
|
+
dimension not in dispatched_set
|
|
511
|
+
for _question, assigned_dimensions in assignments
|
|
512
|
+
for dimension in assigned_dimensions
|
|
513
|
+
):
|
|
456
514
|
raise HandoffError("every assigned question must target a dispatched dimension")
|
|
457
515
|
for dimension in dispatched:
|
|
458
516
|
dimension_questions = [
|
|
459
517
|
question
|
|
460
|
-
for question,
|
|
461
|
-
if
|
|
518
|
+
for question, assigned_dimensions in assignments
|
|
519
|
+
if dimension in assigned_dimensions
|
|
462
520
|
]
|
|
463
521
|
if not dimension_questions:
|
|
464
522
|
raise HandoffError(f"dispatched {dimension} has no assigned research question")
|
|
@@ -496,7 +554,7 @@ def _field(block: str, field: str) -> str:
|
|
|
496
554
|
|
|
497
555
|
|
|
498
556
|
def _raw_source_field(block: str, field: str, source: str) -> str:
|
|
499
|
-
matches =
|
|
557
|
+
matches = _common.find_review_bullet_field_values(block.splitlines(), field)
|
|
500
558
|
if not matches:
|
|
501
559
|
raise HandoffError(f"{source} is missing {field}")
|
|
502
560
|
if len(matches) != 1:
|
|
@@ -521,10 +579,16 @@ def _heading_block(text: str, heading: re.Match[str]) -> str:
|
|
|
521
579
|
def _source_repair_fields(source: str, text: str, locator: str) -> Tuple[str, str]:
|
|
522
580
|
if source.endswith("FINAL.md"):
|
|
523
581
|
if not CRITERION_LOCATOR_PATTERN.fullmatch(locator):
|
|
524
|
-
raise HandoffError(
|
|
582
|
+
raise HandoffError(
|
|
583
|
+
f"{source} locator must be an SC<n> id; copy the id from a "
|
|
584
|
+
"criterion heading"
|
|
585
|
+
)
|
|
525
586
|
heading = re.search(rf"(?m)^### {re.escape(locator)} — .+$", text)
|
|
526
587
|
if not heading:
|
|
527
|
-
raise HandoffError(
|
|
588
|
+
raise HandoffError(
|
|
589
|
+
f"{source} has no criterion heading for locator {locator}; "
|
|
590
|
+
"copy an SC<n> id from a heading"
|
|
591
|
+
)
|
|
528
592
|
block = _heading_block(text, heading)
|
|
529
593
|
verdict = _source_field(block, "Verdict", source)
|
|
530
594
|
if verdict not in {"not-met", "unverifiable"}:
|
|
@@ -532,8 +596,13 @@ def _source_repair_fields(source: str, text: str, locator: str) -> Tuple[str, st
|
|
|
532
596
|
evidence = _source_field(block, "Finding", source)
|
|
533
597
|
fix_direction = _source_field(block, "Fix direction", source)
|
|
534
598
|
else:
|
|
535
|
-
|
|
536
|
-
|
|
599
|
+
risk_lines = [line for line in text.splitlines() if line.startswith("Risk: ")]
|
|
600
|
+
if not locator.startswith("Risk: ") or locator not in risk_lines:
|
|
601
|
+
candidates = ", ".join(repr(line) for line in risk_lines) or "none found"
|
|
602
|
+
raise HandoffError(
|
|
603
|
+
f"{source} locator must equal a full 'Risk: ...' line from the "
|
|
604
|
+
f"gap review; copy one of {candidates}"
|
|
605
|
+
)
|
|
537
606
|
block = _section(text, "Finding")
|
|
538
607
|
evidence = _source_field(block, "Found", source)
|
|
539
608
|
fix_direction = _source_field(block, "Fix direction", source)
|
|
@@ -627,6 +696,42 @@ def _success_criteria(intent: str) -> Dict[str, str]:
|
|
|
627
696
|
return {f"SC{number}": text for number, text in items.items()}
|
|
628
697
|
|
|
629
698
|
|
|
699
|
+
def _split_top_level_commas(value: str) -> List[str]:
|
|
700
|
+
"""Split on commas outside parentheses, brackets, and simple quoted strings."""
|
|
701
|
+
|
|
702
|
+
parts: List[str] = []
|
|
703
|
+
start = 0
|
|
704
|
+
depth_paren = 0
|
|
705
|
+
depth_bracket = 0
|
|
706
|
+
quote: Optional[str] = None
|
|
707
|
+
index = 0
|
|
708
|
+
while index < len(value):
|
|
709
|
+
character = value[index]
|
|
710
|
+
if quote is not None:
|
|
711
|
+
if character == quote:
|
|
712
|
+
quote = None
|
|
713
|
+
index += 1
|
|
714
|
+
continue
|
|
715
|
+
if character in {"'", '"'}:
|
|
716
|
+
quote = character
|
|
717
|
+
index += 1
|
|
718
|
+
continue
|
|
719
|
+
if character == "(":
|
|
720
|
+
depth_paren += 1
|
|
721
|
+
elif character == ")":
|
|
722
|
+
depth_paren = max(0, depth_paren - 1)
|
|
723
|
+
elif character == "[":
|
|
724
|
+
depth_bracket += 1
|
|
725
|
+
elif character == "]":
|
|
726
|
+
depth_bracket = max(0, depth_bracket - 1)
|
|
727
|
+
elif character == "," and depth_paren == 0 and depth_bracket == 0:
|
|
728
|
+
parts.append(value[start:index])
|
|
729
|
+
start = index + 1
|
|
730
|
+
index += 1
|
|
731
|
+
parts.append(value[start:])
|
|
732
|
+
return parts
|
|
733
|
+
|
|
734
|
+
|
|
630
735
|
def _surfaces(text: str, label: str) -> List[str]:
|
|
631
736
|
"""Human-facing surfaces one milestone delivers; empty when `none`."""
|
|
632
737
|
|
|
@@ -643,7 +748,7 @@ def _surfaces(text: str, label: str) -> List[str]:
|
|
|
643
748
|
_non_placeholder(value, f"{label} Surfaces")
|
|
644
749
|
named: List[str] = []
|
|
645
750
|
seen = set()
|
|
646
|
-
for item in value
|
|
751
|
+
for item in _split_top_level_commas(value):
|
|
647
752
|
if not item.strip():
|
|
648
753
|
continue
|
|
649
754
|
surface = _unquoted(item)
|
|
@@ -790,6 +895,338 @@ def _coverage_rows(plan: str) -> List[Tuple[str, str, str]]:
|
|
|
790
895
|
return rows
|
|
791
896
|
|
|
792
897
|
|
|
898
|
+
def _probe_table(
|
|
899
|
+
body: str, label: str, header: str, id_pattern: "re.Pattern[str]", width: int
|
|
900
|
+
) -> List[List[str]]:
|
|
901
|
+
"""Rows of one fixed-width probe table; the last cell keeps any `|` it holds."""
|
|
902
|
+
|
|
903
|
+
rows: List[List[str]] = []
|
|
904
|
+
for line in _strip_comments(body).splitlines():
|
|
905
|
+
stripped = line.strip()
|
|
906
|
+
if not stripped.startswith("|"):
|
|
907
|
+
# A row that lost its leading pipe would otherwise vanish silently.
|
|
908
|
+
if "|" in stripped and id_pattern.fullmatch(_unquoted(stripped.split("|", 1)[0])):
|
|
909
|
+
raise HandoffError(f"{label} row must start with |: {stripped}")
|
|
910
|
+
continue
|
|
911
|
+
cells = [_unquoted(cell) for cell in stripped.strip("|").split("|")]
|
|
912
|
+
first = cells[0] if cells else ""
|
|
913
|
+
if first.casefold() == header.casefold() or all(
|
|
914
|
+
TABLE_SEPARATOR_PATTERN.fullmatch(cell) for cell in cells if cell
|
|
915
|
+
):
|
|
916
|
+
continue
|
|
917
|
+
if not id_pattern.fullmatch(first):
|
|
918
|
+
raise HandoffError(f"{label} has an invalid row: {stripped}")
|
|
919
|
+
if len(cells) < width:
|
|
920
|
+
raise HandoffError(f"{label} {first} needs {width} cells")
|
|
921
|
+
rows.append(cells[: width - 1] + ["|".join(cells[width - 1 :]).strip()])
|
|
922
|
+
ids = [row[0] for row in rows]
|
|
923
|
+
repeated = sorted({value for value in ids if ids.count(value) > 1})
|
|
924
|
+
if repeated:
|
|
925
|
+
raise HandoffError(f"{label} repeats " + ", ".join(repeated))
|
|
926
|
+
# id_pattern fixes a one-letter prefix, so the ids sort by their number.
|
|
927
|
+
expected = [f"{ids[0][0]}{number}" for number in range(1, len(ids) + 1)] if ids else []
|
|
928
|
+
if sorted(ids, key=lambda value: int(value[1:])) != expected:
|
|
929
|
+
raise HandoffError(f"{label} ids must be contiguous from 1")
|
|
930
|
+
return rows
|
|
931
|
+
|
|
932
|
+
|
|
933
|
+
def _probe_section(text: str, heading: str, label: str) -> Optional[str]:
|
|
934
|
+
"""Body of one exact `## <heading>` section; a near-miss heading is an error.
|
|
935
|
+
|
|
936
|
+
The probe gates key on this heading, so a misspelled or re-levelled one
|
|
937
|
+
must fail loudly instead of reading as an intent written before the probe.
|
|
938
|
+
"""
|
|
939
|
+
|
|
940
|
+
live = _strip_comments(text)
|
|
941
|
+
exact = re.findall(rf"(?m)^## {re.escape(heading)}[ \t]*$", live)
|
|
942
|
+
near = [
|
|
943
|
+
line.strip()
|
|
944
|
+
for line in re.findall(rf"(?im)^[ \t]*#{{1,6}}[ \t]*{re.escape(heading)}\b.*$", live)
|
|
945
|
+
if not re.fullmatch(rf"## {re.escape(heading)}", line.strip())
|
|
946
|
+
]
|
|
947
|
+
if near:
|
|
948
|
+
raise HandoffError(f"{label} heading {near[0]!r} must be exactly '## {heading}'")
|
|
949
|
+
if len(exact) > 1:
|
|
950
|
+
raise HandoffError(f"{label} repeats ## {heading}")
|
|
951
|
+
if not exact:
|
|
952
|
+
return None
|
|
953
|
+
return _common.section_body(live, heading)
|
|
954
|
+
|
|
955
|
+
|
|
956
|
+
def _criterion_disposition(
|
|
957
|
+
value: str, criteria: Dict[str, str], label: str
|
|
958
|
+
) -> Optional[str]:
|
|
959
|
+
match = CRITERION_DISPOSITION_PATTERN.fullmatch(" ".join(value.split()))
|
|
960
|
+
if match is None:
|
|
961
|
+
return None
|
|
962
|
+
target = match.group(1)
|
|
963
|
+
if target not in criteria:
|
|
964
|
+
raise HandoffError(f"{label} names unknown {target}")
|
|
965
|
+
return target
|
|
966
|
+
|
|
967
|
+
|
|
968
|
+
def edge_coverage(
|
|
969
|
+
intent: str, criteria: Dict[str, str]
|
|
970
|
+
) -> Optional[Dict[str, Dict[str, str]]]:
|
|
971
|
+
"""INTENT.md `## Edge coverage`, or None for an intent written before the probe.
|
|
972
|
+
|
|
973
|
+
Each row rules on one applicable edge of one success criterion:
|
|
974
|
+
`criterion SCn` (the edge is stated in a success criterion), `held-out`
|
|
975
|
+
(a named test must pin the ruling in Detail), or `dismissed` (Detail says
|
|
976
|
+
why it does not apply). Every criterion appears at least once, so a
|
|
977
|
+
skipped walk is visible rather than silent.
|
|
978
|
+
"""
|
|
979
|
+
|
|
980
|
+
label = "INTENT.md Edge coverage"
|
|
981
|
+
body = _probe_section(intent, "Edge coverage", label)
|
|
982
|
+
if body is None:
|
|
983
|
+
return None
|
|
984
|
+
edges: Dict[str, Dict[str, str]] = {}
|
|
985
|
+
seen: Set[Tuple[str, str, str, str]] = set()
|
|
986
|
+
categories: Dict[str, Set[str]] = {}
|
|
987
|
+
for edge_id, criterion, category, disposition, detail in _probe_table(
|
|
988
|
+
body, label, "Edge", EDGE_ID_PATTERN, 5
|
|
989
|
+
):
|
|
990
|
+
row_label = f"{label} {edge_id}"
|
|
991
|
+
if criterion not in criteria:
|
|
992
|
+
raise HandoffError(f"{row_label} names unknown criterion {criterion or 'none'}")
|
|
993
|
+
category = category.casefold()
|
|
994
|
+
if category not in EDGE_CATEGORIES + ("none",):
|
|
995
|
+
raise HandoffError(f"{row_label} has unknown category {category or 'none'}")
|
|
996
|
+
disposition = " ".join(disposition.split())
|
|
997
|
+
key = (criterion, category, disposition, _normalize_ws(detail))
|
|
998
|
+
if key in seen:
|
|
999
|
+
raise HandoffError(f"{row_label} repeats an earlier row")
|
|
1000
|
+
seen.add(key)
|
|
1001
|
+
categories.setdefault(criterion, set()).add(category)
|
|
1002
|
+
target = _criterion_disposition(disposition, criteria, row_label)
|
|
1003
|
+
if target is None and disposition not in {"held-out", "dismissed"}:
|
|
1004
|
+
raise HandoffError(
|
|
1005
|
+
f"{row_label} disposition must be criterion SCn, held-out, or dismissed"
|
|
1006
|
+
)
|
|
1007
|
+
if category == "none" and disposition != "dismissed":
|
|
1008
|
+
raise HandoffError(f"{row_label} category none must be dismissed")
|
|
1009
|
+
if target is None:
|
|
1010
|
+
detail = _non_placeholder(detail, f"{row_label} Detail")
|
|
1011
|
+
elif detail:
|
|
1012
|
+
_reject_placeholder(detail, f"{row_label} Detail")
|
|
1013
|
+
edges[edge_id] = {
|
|
1014
|
+
"criterion": criterion,
|
|
1015
|
+
"category": category,
|
|
1016
|
+
"disposition": "criterion" if target else disposition,
|
|
1017
|
+
"target": target or "",
|
|
1018
|
+
"detail": detail,
|
|
1019
|
+
}
|
|
1020
|
+
if not edges:
|
|
1021
|
+
raise HandoffError(f"{label} has no rows")
|
|
1022
|
+
for criterion, raised in categories.items():
|
|
1023
|
+
if "none" in raised and len(raised) > 1:
|
|
1024
|
+
raise HandoffError(f"{label} {criterion} mixes category none with edges")
|
|
1025
|
+
walked = {edge["criterion"] for edge in edges.values()}
|
|
1026
|
+
unwalked = [criterion for criterion in criteria if criterion not in walked]
|
|
1027
|
+
if unwalked:
|
|
1028
|
+
raise HandoffError(f"{label} does not walk " + ", ".join(unwalked))
|
|
1029
|
+
return edges
|
|
1030
|
+
|
|
1031
|
+
|
|
1032
|
+
def prohibitions(
|
|
1033
|
+
intent: str, criteria: Dict[str, str]
|
|
1034
|
+
) -> Optional[Dict[str, Dict[str, str]]]:
|
|
1035
|
+
"""INTENT.md `## Prohibitions`, or None for an intent written before the probe.
|
|
1036
|
+
|
|
1037
|
+
Each row names what one criterion must never silently become:
|
|
1038
|
+
`criterion SCn` (stated as a checkable success criterion), `judgment`
|
|
1039
|
+
(the final reviewer judges it; Detail says what to look for), or
|
|
1040
|
+
`dismissed`. Criterion `all` is allowed only for a dismissed row, so work
|
|
1041
|
+
no person touches needs one row instead of one per criterion.
|
|
1042
|
+
"""
|
|
1043
|
+
|
|
1044
|
+
label = "INTENT.md Prohibitions"
|
|
1045
|
+
body = _probe_section(intent, "Prohibitions", label)
|
|
1046
|
+
if body is None:
|
|
1047
|
+
return None
|
|
1048
|
+
rows: Dict[str, Dict[str, str]] = {}
|
|
1049
|
+
for prohibition_id, criterion, must_not, disposition, detail in _probe_table(
|
|
1050
|
+
body, label, "Prohibition", PROHIBITION_ID_PATTERN, 5
|
|
1051
|
+
):
|
|
1052
|
+
row_label = f"{label} {prohibition_id}"
|
|
1053
|
+
disposition = " ".join(disposition.split())
|
|
1054
|
+
if criterion.casefold() == "all":
|
|
1055
|
+
if disposition != "dismissed":
|
|
1056
|
+
raise HandoffError(f"{row_label} criterion all must be dismissed")
|
|
1057
|
+
criterion = "all"
|
|
1058
|
+
elif criterion not in criteria:
|
|
1059
|
+
raise HandoffError(f"{row_label} names unknown criterion {criterion or 'none'}")
|
|
1060
|
+
target = _criterion_disposition(disposition, criteria, row_label)
|
|
1061
|
+
if target is None and disposition not in {"judgment", "dismissed"}:
|
|
1062
|
+
raise HandoffError(
|
|
1063
|
+
f"{row_label} disposition must be criterion SCn, judgment, or dismissed"
|
|
1064
|
+
)
|
|
1065
|
+
if disposition != "dismissed":
|
|
1066
|
+
must_not = _non_placeholder(must_not, f"{row_label} Must not")
|
|
1067
|
+
elif must_not:
|
|
1068
|
+
_reject_placeholder(must_not, f"{row_label} Must not")
|
|
1069
|
+
if target is None:
|
|
1070
|
+
detail = _non_placeholder(detail, f"{row_label} Detail")
|
|
1071
|
+
elif detail:
|
|
1072
|
+
_reject_placeholder(detail, f"{row_label} Detail")
|
|
1073
|
+
rows[prohibition_id] = {
|
|
1074
|
+
"criterion": criterion,
|
|
1075
|
+
"must_not": must_not,
|
|
1076
|
+
"disposition": "criterion" if target else disposition,
|
|
1077
|
+
"target": target or "",
|
|
1078
|
+
"detail": detail,
|
|
1079
|
+
}
|
|
1080
|
+
if not rows:
|
|
1081
|
+
raise HandoffError(f"{label} has no rows")
|
|
1082
|
+
walked = {row["criterion"] for row in rows.values()}
|
|
1083
|
+
if "all" in walked:
|
|
1084
|
+
# `all` records that no person is affected at all, so it stands alone.
|
|
1085
|
+
if len(rows) != 1 or next(iter(rows.values()))["must_not"].casefold() != "none":
|
|
1086
|
+
raise HandoffError(f"{label} criterion all must be the only row, with Must not none")
|
|
1087
|
+
return rows
|
|
1088
|
+
unwalked = [criterion for criterion in criteria if criterion not in walked]
|
|
1089
|
+
if unwalked:
|
|
1090
|
+
raise HandoffError(f"{label} does not walk " + ", ".join(unwalked))
|
|
1091
|
+
return rows
|
|
1092
|
+
|
|
1093
|
+
|
|
1094
|
+
def evidence_tags(intent: str, criteria: Dict[str, str]) -> Dict[str, List[str]]:
|
|
1095
|
+
"""Per criterion, the held-out edges and judgment prohibitions a `met` verdict must cite."""
|
|
1096
|
+
|
|
1097
|
+
tags: Dict[str, List[str]] = {}
|
|
1098
|
+
for edge_id, edge in (edge_coverage(intent, criteria) or {}).items():
|
|
1099
|
+
if edge["disposition"] == "held-out":
|
|
1100
|
+
tags.setdefault(edge["criterion"], []).append(edge_id)
|
|
1101
|
+
for prohibition_id, row in (prohibitions(intent, criteria) or {}).items():
|
|
1102
|
+
if row["disposition"] == "judgment":
|
|
1103
|
+
tags.setdefault(row["criterion"], []).append(prohibition_id)
|
|
1104
|
+
return tags
|
|
1105
|
+
|
|
1106
|
+
|
|
1107
|
+
def _cites(text: str, tag: str) -> bool:
|
|
1108
|
+
"""A citation of an E#/N# id as a word, not inside a path, name, or longer id."""
|
|
1109
|
+
|
|
1110
|
+
return re.search(rf"(?<![A-Za-z0-9_./\\-]){re.escape(tag)}(?![A-Za-z0-9_])", text) is not None
|
|
1111
|
+
|
|
1112
|
+
|
|
1113
|
+
def _held_out_checks(plan: str) -> Dict[str, Tuple[str, str]]:
|
|
1114
|
+
body = _common.section_body(plan, "Held-out checks")
|
|
1115
|
+
if body is None:
|
|
1116
|
+
return {}
|
|
1117
|
+
label = "PLAN.md Held-out checks"
|
|
1118
|
+
checks: Dict[str, Tuple[str, str]] = {}
|
|
1119
|
+
for line in _strip_comments(body).splitlines():
|
|
1120
|
+
stripped = line.strip()
|
|
1121
|
+
if not stripped.startswith("|"):
|
|
1122
|
+
continue
|
|
1123
|
+
cells = [cell.strip() for cell in stripped.strip("|").split("|")]
|
|
1124
|
+
if cells[0].casefold() == "edge" or all(
|
|
1125
|
+
TABLE_SEPARATOR_PATTERN.fullmatch(cell) for cell in cells if cell
|
|
1126
|
+
):
|
|
1127
|
+
continue
|
|
1128
|
+
if len(cells) != 3 or not EDGE_ID_PATTERN.fullmatch(cells[0]):
|
|
1129
|
+
raise HandoffError(f"{label} has an invalid row: {stripped}")
|
|
1130
|
+
edge_id, task_id, test = (_unquoted(cell) for cell in cells)
|
|
1131
|
+
if edge_id in checks:
|
|
1132
|
+
raise HandoffError(f"{label} repeats {edge_id}")
|
|
1133
|
+
# A pytest-style node id (`tests/x.py::test_y`) names its file before `::`.
|
|
1134
|
+
test = _canonical_repo_path(test.split("::", 1)[0], f"{label} {edge_id} Test")
|
|
1135
|
+
checks[edge_id] = (task_id, test)
|
|
1136
|
+
return checks
|
|
1137
|
+
|
|
1138
|
+
|
|
1139
|
+
def _verify_without_comments(task_text: str) -> str:
|
|
1140
|
+
body = _section(task_text, "Verify")
|
|
1141
|
+
block = VERIFY_BLOCK_PATTERN.search(body)
|
|
1142
|
+
if block is None:
|
|
1143
|
+
return ""
|
|
1144
|
+
return _normalize_ws(re.sub(r"(?m)(?:^|\s)#.*$", "", block.group("block")))
|
|
1145
|
+
|
|
1146
|
+
|
|
1147
|
+
def _validate_held_out_checks(
|
|
1148
|
+
plan: str,
|
|
1149
|
+
edges: Optional[Dict[str, Dict[str, str]]],
|
|
1150
|
+
tasks: Dict[str, str],
|
|
1151
|
+
) -> Dict[str, str]:
|
|
1152
|
+
"""Each held-out edge maps to one task whose Verify runs the named test."""
|
|
1153
|
+
|
|
1154
|
+
held_out = sorted(
|
|
1155
|
+
(edge_id for edge_id, edge in (edges or {}).items() if edge["disposition"] == "held-out"),
|
|
1156
|
+
key=lambda value: int(value[1:]),
|
|
1157
|
+
)
|
|
1158
|
+
checks = _held_out_checks(plan)
|
|
1159
|
+
label = "PLAN.md Held-out checks"
|
|
1160
|
+
missing = [edge_id for edge_id in held_out if edge_id not in checks]
|
|
1161
|
+
if missing:
|
|
1162
|
+
raise HandoffError(f"{label} omits held-out " + ", ".join(missing))
|
|
1163
|
+
extra = sorted(set(checks) - set(held_out), key=lambda value: int(value[1:]))
|
|
1164
|
+
if extra:
|
|
1165
|
+
raise HandoffError(f"{label} names edges that are not held-out: " + ", ".join(extra))
|
|
1166
|
+
for edge_id in held_out:
|
|
1167
|
+
task_id, test = checks[edge_id]
|
|
1168
|
+
if task_id not in tasks:
|
|
1169
|
+
raise HandoffError(f"{label} {edge_id} names unknown {task_id}")
|
|
1170
|
+
text = tasks[task_id]
|
|
1171
|
+
criterion = (edges or {})[edge_id]["criterion"]
|
|
1172
|
+
if criterion not in _owned_criteria(text, task_id):
|
|
1173
|
+
raise HandoffError(
|
|
1174
|
+
f"{label} {edge_id} belongs to {criterion}, which {task_id} does not own"
|
|
1175
|
+
)
|
|
1176
|
+
files = [_posix_path(path) for path in _frontmatter_files(text)]
|
|
1177
|
+
if test not in files:
|
|
1178
|
+
raise HandoffError(f"{label} {edge_id} test {test} is not listed in {task_id} files")
|
|
1179
|
+
if any(other != test and other.startswith(test + "/") for other in files):
|
|
1180
|
+
raise HandoffError(f"{label} {edge_id} test {test} names a directory, not a test file")
|
|
1181
|
+
if not _verify_mentions_a_file(_verify_without_comments(text), [test]):
|
|
1182
|
+
raise HandoffError(f"{task_id} Verify must run held-out test {test} for {edge_id}")
|
|
1183
|
+
acceptance = " ".join(_acceptance_items(text, task_id).values())
|
|
1184
|
+
if not _cites(acceptance, edge_id):
|
|
1185
|
+
raise HandoffError(f"{task_id} Acceptance criteria must state held-out {edge_id}")
|
|
1186
|
+
return {edge_id: checks[edge_id][0] for edge_id in held_out}
|
|
1187
|
+
|
|
1188
|
+
|
|
1189
|
+
def validate_intent_probes(root: Path, project_dir: str = DEFAULT_PROJECT_DIR) -> None:
|
|
1190
|
+
"""Probe-table check for leaving define; a no-op without INTENT.md or its tables."""
|
|
1191
|
+
|
|
1192
|
+
path = root / _intent_path(project_dir)
|
|
1193
|
+
if not path.is_file():
|
|
1194
|
+
return
|
|
1195
|
+
intent = _read(root, _intent_path(project_dir))
|
|
1196
|
+
live = _strip_comments(intent)
|
|
1197
|
+
if not re.search(r"(?im)^[ \t]*#{1,6}[ \t]*(edge coverage|prohibitions)\b", live):
|
|
1198
|
+
return
|
|
1199
|
+
criteria = _success_criteria(intent)
|
|
1200
|
+
edge_coverage(intent, criteria)
|
|
1201
|
+
prohibitions(intent, criteria)
|
|
1202
|
+
|
|
1203
|
+
|
|
1204
|
+
def validate_intent(
|
|
1205
|
+
root: Path, project_dir: str = DEFAULT_PROJECT_DIR
|
|
1206
|
+
) -> Dict[str, object]:
|
|
1207
|
+
"""Check INTENT.md success criteria and its spec-reach probe tables before approval."""
|
|
1208
|
+
|
|
1209
|
+
intent = _read(root, _intent_path(project_dir))
|
|
1210
|
+
criteria = _success_criteria(intent)
|
|
1211
|
+
_surfaces(intent, "INTENT.md")
|
|
1212
|
+
edges = edge_coverage(intent, criteria)
|
|
1213
|
+
rows = prohibitions(intent, criteria)
|
|
1214
|
+
return {
|
|
1215
|
+
"phase": "define",
|
|
1216
|
+
"criteria": sorted(criteria, key=lambda value: int(value[2:])),
|
|
1217
|
+
"edge_coverage": "absent" if edges is None else len(edges),
|
|
1218
|
+
"held_out": sorted(
|
|
1219
|
+
(edge_id for edge_id, edge in (edges or {}).items() if edge["disposition"] == "held-out"),
|
|
1220
|
+
key=lambda value: int(value[1:]),
|
|
1221
|
+
),
|
|
1222
|
+
"prohibitions": "absent" if rows is None else len(rows),
|
|
1223
|
+
"judgment": sorted(
|
|
1224
|
+
(row_id for row_id, row in (rows or {}).items() if row["disposition"] == "judgment"),
|
|
1225
|
+
key=lambda value: int(value[1:]),
|
|
1226
|
+
),
|
|
1227
|
+
}
|
|
1228
|
+
|
|
1229
|
+
|
|
793
1230
|
def _task_texts(root: Path, project_dir: str) -> Dict[str, str]:
|
|
794
1231
|
tasks_dir = root / project_dir / "tasks"
|
|
795
1232
|
if not tasks_dir.is_dir():
|
|
@@ -965,6 +1402,20 @@ def _task_deps(task_text: str, task_id: str) -> List[str]:
|
|
|
965
1402
|
return _inline_ids(rendered, "T", f"{task_id} deps")
|
|
966
1403
|
|
|
967
1404
|
|
|
1405
|
+
def _dependency_reachable(graph: Dict[str, List[str]], start: str, target: str) -> bool:
|
|
1406
|
+
seen: Set[str] = set()
|
|
1407
|
+
pending = list(graph[start])
|
|
1408
|
+
while pending:
|
|
1409
|
+
task_id = pending.pop()
|
|
1410
|
+
if task_id in seen:
|
|
1411
|
+
continue
|
|
1412
|
+
if task_id == target:
|
|
1413
|
+
return True
|
|
1414
|
+
seen.add(task_id)
|
|
1415
|
+
pending.extend(graph[task_id])
|
|
1416
|
+
return False
|
|
1417
|
+
|
|
1418
|
+
|
|
968
1419
|
def _canonical_repo_path(value: str, label: str) -> str:
|
|
969
1420
|
cleaned = value.strip().strip("`\"'")
|
|
970
1421
|
path = PurePosixPath(cleaned)
|
|
@@ -1083,6 +1534,17 @@ def _validate_task_graph(
|
|
|
1083
1534
|
continue
|
|
1084
1535
|
overlap = sorted(set(task_files[left]) & set(task_files[right]))
|
|
1085
1536
|
if overlap:
|
|
1537
|
+
if (
|
|
1538
|
+
_task_scalar(tasks[left], left, "status") == "done"
|
|
1539
|
+
and _task_scalar(tasks[right], right, "status") == "done"
|
|
1540
|
+
):
|
|
1541
|
+
continue
|
|
1542
|
+
# Dependency-ordered tasks serialize safely; only unordered
|
|
1543
|
+
# same-wave overlap races on a shared file.
|
|
1544
|
+
if _dependency_reachable(graph, left, right) or _dependency_reachable(
|
|
1545
|
+
graph, right, left
|
|
1546
|
+
):
|
|
1547
|
+
continue
|
|
1086
1548
|
raise HandoffError(
|
|
1087
1549
|
f"same-wave file overlap between {left} and {right}: {', '.join(overlap)}"
|
|
1088
1550
|
)
|
|
@@ -1116,11 +1578,12 @@ def _validate_task_graph(
|
|
|
1116
1578
|
|
|
1117
1579
|
|
|
1118
1580
|
def plan_brief_inputs(
|
|
1119
|
-
root: Path, project_dir: str = DEFAULT_PROJECT_DIR
|
|
1581
|
+
root: Path, project_dir: str = DEFAULT_PROJECT_DIR, *, initial: bool = True
|
|
1120
1582
|
) -> Tuple[Dict[str, str], Dict[str, Set[str]]]:
|
|
1583
|
+
"""Validate the plan graph, requiring clean unlanded tasks only at approval."""
|
|
1121
1584
|
tasks = _task_texts(root, project_dir)
|
|
1122
1585
|
wave_depths, _ = _plan_waves(_read(root, f"{project_dir}/plan/PLAN.md"))
|
|
1123
|
-
return tasks, _validate_task_graph(wave_depths, tasks, initial=
|
|
1586
|
+
return tasks, _validate_task_graph(wave_depths, tasks, initial=initial)
|
|
1124
1587
|
|
|
1125
1588
|
|
|
1126
1589
|
def _intent_path(project_dir: str) -> str:
|
|
@@ -1475,6 +1938,9 @@ def validate_plan(
|
|
|
1475
1938
|
continue
|
|
1476
1939
|
if not _verify_mentions_a_file(command, _frontmatter_files(text)):
|
|
1477
1940
|
raise HandoffError(f"{task_id} Verify must name a path from files")
|
|
1941
|
+
edges = edge_coverage(intent, criteria)
|
|
1942
|
+
prohibitions(intent, criteria)
|
|
1943
|
+
held_out = _validate_held_out_checks(plan, edges, tasks)
|
|
1478
1944
|
return {
|
|
1479
1945
|
"phase": "plan",
|
|
1480
1946
|
"criteria": sorted(criteria),
|
|
@@ -1482,6 +1948,7 @@ def validate_plan(
|
|
|
1482
1948
|
"tasks": len(tasks),
|
|
1483
1949
|
"waves": waves,
|
|
1484
1950
|
"surfaces": {surface: task for surface, (task, _) in owners.items()},
|
|
1951
|
+
"held_out": held_out,
|
|
1485
1952
|
}
|
|
1486
1953
|
|
|
1487
1954
|
|
|
@@ -1543,17 +2010,27 @@ def validate_wave_evidence(
|
|
|
1543
2010
|
assigned[task_id].add(criterion)
|
|
1544
2011
|
owned = _owned_by_wave(tasks, assigned, wave)
|
|
1545
2012
|
text = _read(root, review_path.as_posix())
|
|
2013
|
+
is_last_cycle = cycle >= _max_wave_cycle(root, project_dir, wave)
|
|
1546
2014
|
if _line_value(text, "Cycle:") != str(cycle):
|
|
1547
2015
|
raise HandoffError(f"{name} Cycle field does not match its filename")
|
|
1548
|
-
|
|
2016
|
+
recorded_depth = _line_value(text, "Depth:")
|
|
2017
|
+
if recorded_depth not in VALID_REVIEW_DEPTHS:
|
|
2018
|
+
raise HandoffError(f"{name} Depth does not match PLAN.md")
|
|
2019
|
+
if is_last_cycle and recorded_depth != expected_depth:
|
|
1549
2020
|
raise HandoffError(f"{name} Depth does not match PLAN.md")
|
|
1550
2021
|
lens_fields = re.findall(r"(?m)^Lens:[ \t]*(\S.*?)[ \t]*$", text)
|
|
1551
2022
|
if lens is None and lens_fields:
|
|
1552
2023
|
raise HandoffError(f"{name} must not declare a review lens")
|
|
1553
2024
|
if lens is not None and lens_fields != [lens]:
|
|
1554
2025
|
raise HandoffError(f"{name} Lens field does not match its filename")
|
|
2026
|
+
overall = _line_value(text, "Wave verdict:")
|
|
2027
|
+
if overall not in {"pass", "blocked"}:
|
|
2028
|
+
raise HandoffError(f"{name} Wave verdict is invalid")
|
|
1555
2029
|
reviewed = _line_value(text, "Tasks reviewed:")
|
|
1556
|
-
|
|
2030
|
+
enforce_plan_tasks = is_last_cycle and overall != "blocked"
|
|
2031
|
+
if not reviewed.isdigit() or int(reviewed) < 1:
|
|
2032
|
+
raise HandoffError(f"{name} Tasks reviewed does not match Wave {wave}")
|
|
2033
|
+
if enforce_plan_tasks and int(reviewed) != len(expected_tasks):
|
|
1557
2034
|
raise HandoffError(f"{name} Tasks reviewed does not match Wave {wave}")
|
|
1558
2035
|
|
|
1559
2036
|
lines = text.splitlines()
|
|
@@ -1562,11 +2039,25 @@ def validate_wave_evidence(
|
|
|
1562
2039
|
match = WAVE_TASK_HEADING.fullmatch(line)
|
|
1563
2040
|
if match:
|
|
1564
2041
|
task_headings.append((index, match))
|
|
2042
|
+
if len(task_headings) != int(reviewed):
|
|
2043
|
+
raise HandoffError(f"{name} Tasks reviewed does not match Wave {wave}")
|
|
1565
2044
|
reviewed_tasks = [match.group("task") for _, match in task_headings]
|
|
1566
|
-
if
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
2045
|
+
if enforce_plan_tasks:
|
|
2046
|
+
if reviewed_tasks != expected_tasks:
|
|
2047
|
+
raise HandoffError(
|
|
2048
|
+
f"{name} must review exactly {', '.join(expected_tasks)} in plan order"
|
|
2049
|
+
)
|
|
2050
|
+
else:
|
|
2051
|
+
unknown = [task_id for task_id in reviewed_tasks if task_id not in expected_tasks]
|
|
2052
|
+
if unknown:
|
|
2053
|
+
raise HandoffError(
|
|
2054
|
+
f"{name} must review exactly {', '.join(expected_tasks)} in plan order"
|
|
2055
|
+
)
|
|
2056
|
+
plan_order = [task_id for task_id in expected_tasks if task_id in reviewed_tasks]
|
|
2057
|
+
if reviewed_tasks != plan_order:
|
|
2058
|
+
raise HandoffError(
|
|
2059
|
+
f"{name} must review exactly {', '.join(expected_tasks)} in plan order"
|
|
2060
|
+
)
|
|
1570
2061
|
task_verdicts = []
|
|
1571
2062
|
for heading_index, heading in task_headings:
|
|
1572
2063
|
task_id = heading.group("task")
|
|
@@ -1595,9 +2086,16 @@ def validate_wave_evidence(
|
|
|
1595
2086
|
_non_placeholder(item, f"{name} task {task_id} {verdict} evidence")
|
|
1596
2087
|
task_verdicts.append(verdict)
|
|
1597
2088
|
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
2089
|
+
if not is_last_cycle:
|
|
2090
|
+
return {
|
|
2091
|
+
"phase": "wave",
|
|
2092
|
+
"wave": wave,
|
|
2093
|
+
"cycle": cycle,
|
|
2094
|
+
"owned": owned if owned else [],
|
|
2095
|
+
"review": review,
|
|
2096
|
+
"verdict": overall,
|
|
2097
|
+
}
|
|
2098
|
+
|
|
1601
2099
|
if overall == "pass" and "fail" in task_verdicts:
|
|
1602
2100
|
raise HandoffError(f"{name} Wave verdict is pass while a task failed")
|
|
1603
2101
|
try:
|
|
@@ -1692,6 +2190,7 @@ def validate_final(
|
|
|
1692
2190
|
intent = _read(root, _intent_path(project_dir))
|
|
1693
2191
|
criteria = _success_criteria(intent)
|
|
1694
2192
|
surface_of = criterion_surfaces(root, project_dir)
|
|
2193
|
+
tags = evidence_tags(intent, criteria)
|
|
1695
2194
|
relative = f"{project_dir}/review/FINAL.md"
|
|
1696
2195
|
text = _read(root, relative) if final_text is None else final_text
|
|
1697
2196
|
reviewed_head = _reviewed_head(text, relative)
|
|
@@ -1769,6 +2268,16 @@ def validate_final(
|
|
|
1769
2268
|
raise HandoffError(
|
|
1770
2269
|
f"FINAL.md {sc_id} met verdict must have no Finding or Fix direction"
|
|
1771
2270
|
)
|
|
2271
|
+
# Honest verifier: a spec-tagged criterion is met only on cited
|
|
2272
|
+
# evidence for each tag; otherwise the reviewer records unverifiable.
|
|
2273
|
+
cited = " ".join((check, observed, reference))
|
|
2274
|
+
uncited = [tag for tag in tags.get(sc_id, []) if not _cites(cited, tag)]
|
|
2275
|
+
if uncited:
|
|
2276
|
+
raise HandoffError(
|
|
2277
|
+
f"FINAL.md {sc_id} is met without cited evidence for "
|
|
2278
|
+
+ ", ".join(uncited)
|
|
2279
|
+
+ "; cite each held-out check or judgment, or record unverifiable"
|
|
2280
|
+
)
|
|
1772
2281
|
elif finding.casefold() == "none" or fix_direction.casefold() == "none":
|
|
1773
2282
|
raise HandoffError(
|
|
1774
2283
|
f"FINAL.md {sc_id} {verdict} verdict requires a Finding and Fix direction"
|
|
@@ -1889,10 +2398,15 @@ def validate_patch_findings(
|
|
|
1889
2398
|
source, source_text, locator
|
|
1890
2399
|
)
|
|
1891
2400
|
if evidence != source_evidence:
|
|
1892
|
-
|
|
2401
|
+
evidence_field = "Finding" if source.endswith("FINAL.md") else "Found"
|
|
2402
|
+
raise HandoffError(
|
|
2403
|
+
f"{source} locator {locator} Evidence must equal the source's "
|
|
2404
|
+
f"{evidence_field} field; copy it verbatim: {source_evidence!r}"
|
|
2405
|
+
)
|
|
1893
2406
|
if fix_direction != source_fix_direction:
|
|
1894
2407
|
raise HandoffError(
|
|
1895
|
-
f"{source} locator {locator}
|
|
2408
|
+
f"{source} locator {locator} Fix direction must equal the source's "
|
|
2409
|
+
f"Fix direction field; copy it verbatim: {source_fix_direction!r}"
|
|
1896
2410
|
)
|
|
1897
2411
|
sources.append(source)
|
|
1898
2412
|
|
|
@@ -1910,7 +2424,8 @@ def validate_patch_findings(
|
|
|
1910
2424
|
def parser() -> argparse.ArgumentParser:
|
|
1911
2425
|
argument_parser = argparse.ArgumentParser(description=__doc__)
|
|
1912
2426
|
argument_parser.add_argument(
|
|
1913
|
-
"phase",
|
|
2427
|
+
"phase",
|
|
2428
|
+
choices=("intent", "research", "decide", "roadmap", "patch", "plan", "wave", "final"),
|
|
1914
2429
|
)
|
|
1915
2430
|
argument_parser.add_argument("--repo", type=Path, required=True)
|
|
1916
2431
|
argument_parser.add_argument(
|
|
@@ -1938,6 +2453,7 @@ def main(argv: Optional[Sequence[str]] = None) -> int:
|
|
|
1938
2453
|
result = validate_wave(repo, arguments.project_dir, arguments.review)
|
|
1939
2454
|
else:
|
|
1940
2455
|
validators = {
|
|
2456
|
+
"intent": validate_intent,
|
|
1941
2457
|
"research": validate_research,
|
|
1942
2458
|
"decide": validate_decide,
|
|
1943
2459
|
"roadmap": validate_roadmap,
|