@opengsd/gsd-path 1.3.1 → 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/AGENTS.md +2 -207
- package/CHANGELOG.md +331 -69
- package/DOCS.md +25 -9
- package/FULL.md +12 -5
- package/GUIDE.md +1 -1
- package/HOOKS.md +104 -14
- package/QUICK.md +5 -2
- package/README.md +131 -111
- package/RUNTIME.md +2 -2
- package/UPDATE.md +61 -11
- package/WORKFLOW.md +44 -7
- package/package.json +13 -9
- package/platforms/shared-agents/dispatch.md +13 -0
- package/scripts/_common.py +619 -18
- package/scripts/archive_milestone.py +160 -86
- package/scripts/bootstrap_repository.py +8 -5
- package/scripts/build_recovery.py +100 -32
- package/scripts/build_state.py +143 -24
- package/scripts/check_docs_audit.py +91 -7
- package/scripts/check_handoffs.py +662 -50
- package/scripts/check_task_briefs.py +338 -30
- package/scripts/check_update.py +2 -2
- package/scripts/core_hook_gate.py +10 -1
- package/scripts/core_hook_settings.py +11 -4
- package/scripts/detect_project.py +587 -32
- package/scripts/discussion_records.py +19 -13
- package/scripts/discussion_validate.py +152 -50
- package/scripts/dispatch_driver.py +501 -46
- package/scripts/git_guard.py +118 -31
- package/scripts/guard_hook.py +1567 -194
- package/scripts/install.mjs +77 -46
- package/scripts/install.py +611 -50
- package/scripts/integration.py +580 -25
- package/scripts/isolation.py +1438 -57
- package/scripts/lean_verification.py +99 -19
- package/scripts/loop_run.py +31 -29
- package/scripts/members.py +706 -0
- package/scripts/migrate_core.py +2 -2
- package/scripts/path_config.py +5 -5
- package/scripts/pipeline_diagnose.py +82 -31
- package/scripts/pipeline_git.py +124 -34
- package/scripts/pipeline_state.py +287 -87
- package/scripts/pipeline_undo.py +218 -30
- package/scripts/promote_lookahead.py +1 -1
- package/scripts/review_panel.py +1 -1
- package/scripts/runtime_store.py +55 -11
- package/scripts/skill-resources.json +37 -5
- package/scripts/state_checkpoint.py +135 -44
- package/scripts/state_promote.py +2 -0
- package/scripts/status_runtime.py +58 -10
- package/scripts/sync_skill_resources.py +8 -7
- package/scripts/task_context.py +12 -3
- package/scripts/token_budget.py +12 -8
- package/scripts/wizard.mjs +1 -0
- package/scripts/workflow_run.py +107 -30
- package/scripts/worktree_paths.py +72 -10
- package/skills/gsd-path/BUILD.md +7 -1
- package/skills/gsd-path/DECIDE.md +11 -4
- package/skills/gsd-path/DEFINE.md +36 -4
- package/skills/gsd-path/DOCS-AUDIT.md +6 -4
- package/skills/gsd-path/INSPECT.md +5 -4
- package/skills/gsd-path/PLAN.md +18 -4
- package/skills/gsd-path/RESEARCH.md +13 -1
- package/skills/gsd-path/ROADMAP.md +2 -1
- package/skills/gsd-path/SHIP.md +33 -11
- package/skills/gsd-path/SKILL.md +45 -1
- package/skills/gsd-path/references/build-native.md +63 -15
- package/skills/gsd-path/references/codebase-mapper.md +2 -1
- package/skills/gsd-path/references/dispatch.md +13 -0
- package/skills/gsd-path/references/docs-auditor.md +7 -1
- package/skills/gsd-path/references/operating-rules.md +214 -0
- package/skills/gsd-path/references/planner.md +22 -4
- package/skills/gsd-path/references/reviewer-final.md +20 -2
- package/skills/gsd-path/references/reviewer-wave.md +18 -5
- 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 +619 -18
- package/skills/gsd-path/scripts/archive_milestone.py +160 -86
- package/skills/gsd-path/scripts/bootstrap_repository.py +8 -5
- package/skills/gsd-path/scripts/build_recovery.py +100 -32
- package/skills/gsd-path/scripts/build_state.py +143 -24
- package/skills/gsd-path/scripts/check_docs_audit.py +91 -7
- package/skills/gsd-path/scripts/check_handoffs.py +662 -50
- package/skills/gsd-path/scripts/check_task_briefs.py +338 -30
- package/skills/gsd-path/scripts/check_update.py +2 -2
- package/skills/gsd-path/scripts/detect_project.py +587 -32
- package/skills/gsd-path/scripts/discussion_records.py +19 -13
- package/skills/gsd-path/scripts/discussion_validate.py +152 -50
- package/skills/gsd-path/scripts/dispatch_driver.py +501 -46
- package/skills/gsd-path/scripts/git_guard.py +118 -31
- package/skills/gsd-path/scripts/integration.py +580 -25
- package/skills/gsd-path/scripts/isolation.py +1438 -57
- package/skills/gsd-path/scripts/lean_verification.py +99 -19
- package/skills/gsd-path/scripts/loop_run.py +31 -29
- package/skills/gsd-path/scripts/members.py +706 -0
- package/skills/gsd-path/scripts/path_config.py +5 -5
- package/skills/gsd-path/scripts/pipeline_git.py +124 -34
- package/skills/gsd-path/scripts/pipeline_state.py +287 -87
- package/skills/gsd-path/scripts/promote_lookahead.py +1 -1
- package/skills/gsd-path/scripts/review_panel.py +1 -1
- package/skills/gsd-path/scripts/state_checkpoint.py +135 -44
- package/skills/gsd-path/scripts/state_promote.py +2 -0
- package/skills/gsd-path/scripts/status_runtime.py +58 -10
- package/skills/gsd-path/scripts/task_context.py +12 -3
- package/skills/gsd-path/scripts/token_budget.py +12 -8
- package/skills/gsd-path/scripts/workflow_run.py +107 -30
- package/skills/gsd-path/scripts/worktree_paths.py +72 -10
- package/skills/gsd-path/templates/codebase.md +1 -1
- package/skills/gsd-path/templates/docs-audit.md +6 -2
- package/skills/gsd-path/templates/final-review.md +16 -4
- package/skills/gsd-path/templates/gap-review.md +2 -1
- 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/task.md +1 -0
- package/skills/gsd-path/templates/wave-review.md +16 -4
- package/skills/gsd-path-build/SKILL.md +7 -1
- package/skills/gsd-path-build/references/build-native.md +63 -15
- package/skills/gsd-path-build/references/dispatch.md +13 -0
- package/skills/gsd-path-build/references/operating-rules.md +214 -0
- package/skills/gsd-path-build/references/reviewer-final.md +20 -2
- package/skills/gsd-path-build/references/reviewer-wave.md +18 -5
- 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 +619 -18
- package/skills/gsd-path-build/scripts/archive_milestone.py +160 -86
- package/skills/gsd-path-build/scripts/build_recovery.py +100 -32
- package/skills/gsd-path-build/scripts/build_state.py +143 -24
- package/skills/gsd-path-build/scripts/check_handoffs.py +662 -50
- package/skills/gsd-path-build/scripts/check_task_briefs.py +338 -30
- package/skills/gsd-path-build/scripts/detect_project.py +587 -32
- package/skills/gsd-path-build/scripts/discussion_records.py +19 -13
- package/skills/gsd-path-build/scripts/discussion_validate.py +152 -50
- package/skills/gsd-path-build/scripts/dispatch_driver.py +501 -46
- package/skills/gsd-path-build/scripts/integration.py +580 -25
- package/skills/gsd-path-build/scripts/isolation.py +1438 -57
- package/skills/gsd-path-build/scripts/loop_run.py +31 -29
- package/skills/gsd-path-build/scripts/members.py +706 -0
- package/skills/gsd-path-build/scripts/path_config.py +5 -5
- package/skills/gsd-path-build/scripts/pipeline_git.py +124 -34
- package/skills/gsd-path-build/scripts/pipeline_state.py +287 -87
- package/skills/gsd-path-build/scripts/promote_lookahead.py +1 -1
- package/skills/gsd-path-build/scripts/review_panel.py +1 -1
- package/skills/gsd-path-build/scripts/state_checkpoint.py +135 -44
- package/skills/gsd-path-build/scripts/state_promote.py +2 -0
- package/skills/gsd-path-build/scripts/task_context.py +12 -3
- package/skills/gsd-path-build/scripts/token_budget.py +12 -8
- package/skills/gsd-path-build/scripts/workflow_run.py +107 -30
- package/skills/gsd-path-build/scripts/worktree_paths.py +72 -10
- package/skills/gsd-path-build/templates/task.md +1 -0
- package/skills/gsd-path-build/templates/wave-review.md +16 -4
- package/skills/gsd-path-decide/SKILL.md +11 -4
- package/skills/gsd-path-decide/references/dispatch.md +13 -0
- package/skills/gsd-path-decide/references/operating-rules.md +214 -0
- package/skills/gsd-path-decide/scripts/_common.py +619 -18
- package/skills/gsd-path-decide/scripts/archive_milestone.py +160 -86
- package/skills/gsd-path-decide/scripts/build_recovery.py +100 -32
- package/skills/gsd-path-decide/scripts/check_handoffs.py +662 -50
- package/skills/gsd-path-decide/scripts/check_task_briefs.py +338 -30
- package/skills/gsd-path-decide/scripts/detect_project.py +587 -32
- package/skills/gsd-path-decide/scripts/discussion_records.py +19 -13
- package/skills/gsd-path-decide/scripts/discussion_validate.py +152 -50
- package/skills/gsd-path-decide/scripts/integration.py +580 -25
- package/skills/gsd-path-decide/scripts/isolation.py +1438 -57
- package/skills/gsd-path-decide/scripts/loop_run.py +31 -29
- package/skills/gsd-path-decide/scripts/members.py +706 -0
- package/skills/gsd-path-decide/scripts/path_config.py +5 -5
- package/skills/gsd-path-decide/scripts/pipeline_git.py +124 -34
- package/skills/gsd-path-decide/scripts/pipeline_state.py +287 -87
- package/skills/gsd-path-decide/scripts/promote_lookahead.py +1 -1
- package/skills/gsd-path-decide/scripts/review_panel.py +1 -1
- package/skills/gsd-path-decide/scripts/state_checkpoint.py +135 -44
- package/skills/gsd-path-decide/scripts/state_promote.py +2 -0
- package/skills/gsd-path-decide/scripts/worktree_paths.py +72 -10
- package/skills/gsd-path-define/SKILL.md +36 -4
- package/skills/gsd-path-define/references/operating-rules.md +214 -0
- package/skills/gsd-path-define/references/spec-probes.md +150 -0
- package/skills/gsd-path-define/scripts/_common.py +619 -18
- package/skills/gsd-path-define/scripts/archive_milestone.py +160 -86
- package/skills/gsd-path-define/scripts/build_recovery.py +100 -32
- package/skills/gsd-path-define/scripts/check_handoffs.py +662 -50
- package/skills/gsd-path-define/scripts/check_task_briefs.py +338 -30
- package/skills/gsd-path-define/scripts/detect_project.py +587 -32
- package/skills/gsd-path-define/scripts/discussion_records.py +19 -13
- package/skills/gsd-path-define/scripts/discussion_validate.py +152 -50
- package/skills/gsd-path-define/scripts/integration.py +580 -25
- package/skills/gsd-path-define/scripts/isolation.py +1438 -57
- package/skills/gsd-path-define/scripts/loop_run.py +31 -29
- package/skills/gsd-path-define/scripts/members.py +706 -0
- package/skills/gsd-path-define/scripts/path_config.py +5 -5
- package/skills/gsd-path-define/scripts/pipeline_git.py +124 -34
- package/skills/gsd-path-define/scripts/pipeline_state.py +287 -87
- package/skills/gsd-path-define/scripts/promote_lookahead.py +1 -1
- package/skills/gsd-path-define/scripts/review_panel.py +1 -1
- package/skills/gsd-path-define/scripts/state_checkpoint.py +135 -44
- package/skills/gsd-path-define/scripts/state_promote.py +2 -0
- package/skills/gsd-path-define/scripts/worktree_paths.py +72 -10
- 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/SKILL.md +2 -1
- package/skills/gsd-path-discuss/references/operating-rules.md +214 -0
- package/skills/gsd-path-discuss/scripts/_common.py +619 -18
- package/skills/gsd-path-discuss/scripts/discussion_records.py +19 -13
- package/skills/gsd-path-discuss/scripts/loop_run.py +31 -29
- package/skills/gsd-path-discuss/scripts/path_config.py +5 -5
- package/skills/gsd-path-discuss/scripts/review_panel.py +1 -1
- package/skills/gsd-path-discuss/scripts/worktree_paths.py +72 -10
- package/skills/gsd-path-docs-audit/SKILL.md +6 -4
- package/skills/gsd-path-docs-audit/references/dispatch.md +13 -0
- package/skills/gsd-path-docs-audit/references/docs-auditor.md +7 -1
- package/skills/gsd-path-docs-audit/references/operating-rules.md +214 -0
- package/skills/gsd-path-docs-audit/scripts/_common.py +619 -18
- package/skills/gsd-path-docs-audit/scripts/archive_milestone.py +160 -86
- package/skills/gsd-path-docs-audit/scripts/build_recovery.py +100 -32
- package/skills/gsd-path-docs-audit/scripts/check_docs_audit.py +91 -7
- package/skills/gsd-path-docs-audit/scripts/check_handoffs.py +662 -50
- package/skills/gsd-path-docs-audit/scripts/check_task_briefs.py +338 -30
- package/skills/gsd-path-docs-audit/scripts/detect_project.py +587 -32
- package/skills/gsd-path-docs-audit/scripts/discussion_records.py +19 -13
- package/skills/gsd-path-docs-audit/scripts/discussion_validate.py +152 -50
- package/skills/gsd-path-docs-audit/scripts/integration.py +580 -25
- package/skills/gsd-path-docs-audit/scripts/isolation.py +1438 -57
- package/skills/gsd-path-docs-audit/scripts/loop_run.py +31 -29
- package/skills/gsd-path-docs-audit/scripts/members.py +706 -0
- package/skills/gsd-path-docs-audit/scripts/path_config.py +5 -5
- package/skills/gsd-path-docs-audit/scripts/pipeline_git.py +124 -34
- package/skills/gsd-path-docs-audit/scripts/pipeline_state.py +287 -87
- package/skills/gsd-path-docs-audit/scripts/promote_lookahead.py +1 -1
- package/skills/gsd-path-docs-audit/scripts/review_panel.py +1 -1
- package/skills/gsd-path-docs-audit/scripts/state_checkpoint.py +135 -44
- package/skills/gsd-path-docs-audit/scripts/state_promote.py +2 -0
- package/skills/gsd-path-docs-audit/scripts/worktree_paths.py +72 -10
- package/skills/gsd-path-docs-audit/templates/docs-audit.md +6 -2
- package/skills/gsd-path-forensics/SKILL.md +2 -1
- package/skills/gsd-path-forensics/references/operating-rules.md +214 -0
- package/skills/gsd-path-forensics/scripts/_common.py +619 -18
- package/skills/gsd-path-forensics/scripts/discussion_records.py +19 -13
- package/skills/gsd-path-forensics/scripts/loop_run.py +31 -29
- package/skills/gsd-path-forensics/scripts/path_config.py +5 -5
- package/skills/gsd-path-forensics/scripts/pipeline_diagnose.py +82 -31
- package/skills/gsd-path-forensics/scripts/pipeline_undo.py +218 -30
- package/skills/gsd-path-forensics/scripts/review_panel.py +1 -1
- package/skills/gsd-path-forensics/scripts/worktree_paths.py +72 -10
- package/skills/gsd-path-inspect/SKILL.md +5 -4
- package/skills/gsd-path-inspect/references/codebase-mapper.md +2 -1
- package/skills/gsd-path-inspect/references/dispatch.md +13 -0
- package/skills/gsd-path-inspect/references/docs-auditor.md +7 -1
- package/skills/gsd-path-inspect/references/operating-rules.md +214 -0
- package/skills/gsd-path-inspect/scripts/_common.py +619 -18
- package/skills/gsd-path-inspect/scripts/archive_milestone.py +160 -86
- package/skills/gsd-path-inspect/scripts/build_recovery.py +100 -32
- package/skills/gsd-path-inspect/scripts/check_docs_audit.py +91 -7
- package/skills/gsd-path-inspect/scripts/check_handoffs.py +662 -50
- package/skills/gsd-path-inspect/scripts/check_task_briefs.py +338 -30
- package/skills/gsd-path-inspect/scripts/detect_project.py +587 -32
- package/skills/gsd-path-inspect/scripts/discussion_records.py +19 -13
- package/skills/gsd-path-inspect/scripts/discussion_validate.py +152 -50
- package/skills/gsd-path-inspect/scripts/integration.py +580 -25
- package/skills/gsd-path-inspect/scripts/isolation.py +1438 -57
- package/skills/gsd-path-inspect/scripts/loop_run.py +31 -29
- package/skills/gsd-path-inspect/scripts/members.py +706 -0
- package/skills/gsd-path-inspect/scripts/path_config.py +5 -5
- package/skills/gsd-path-inspect/scripts/pipeline_git.py +124 -34
- package/skills/gsd-path-inspect/scripts/pipeline_state.py +287 -87
- package/skills/gsd-path-inspect/scripts/promote_lookahead.py +1 -1
- package/skills/gsd-path-inspect/scripts/review_panel.py +1 -1
- package/skills/gsd-path-inspect/scripts/state_checkpoint.py +135 -44
- package/skills/gsd-path-inspect/scripts/state_promote.py +2 -0
- package/skills/gsd-path-inspect/scripts/workflow_run.py +107 -30
- package/skills/gsd-path-inspect/scripts/worktree_paths.py +72 -10
- package/skills/gsd-path-inspect/templates/codebase.md +1 -1
- package/skills/gsd-path-inspect/templates/docs-audit.md +6 -2
- package/skills/gsd-path-inspect/templates/state.md +1 -1
- package/skills/gsd-path-loop/SKILL.md +2 -1
- package/skills/gsd-path-loop/references/operating-rules.md +214 -0
- package/skills/gsd-path-loop/scripts/_common.py +619 -18
- package/skills/gsd-path-loop/scripts/discussion_records.py +19 -13
- package/skills/gsd-path-loop/scripts/loop_run.py +31 -29
- package/skills/gsd-path-loop/scripts/path_config.py +5 -5
- package/skills/gsd-path-loop/scripts/review_panel.py +1 -1
- package/skills/gsd-path-loop/scripts/worktree_paths.py +72 -10
- package/skills/gsd-path-migrate/SKILL.md +2 -1
- package/skills/gsd-path-migrate/references/operating-rules.md +214 -0
- package/skills/gsd-path-migrate/scripts/_common.py +619 -18
- package/skills/gsd-path-migrate/scripts/core_hook_gate.py +10 -1
- package/skills/gsd-path-migrate/scripts/core_hook_settings.py +11 -4
- package/skills/gsd-path-migrate/scripts/discussion_records.py +19 -13
- package/skills/gsd-path-migrate/scripts/loop_run.py +31 -29
- package/skills/gsd-path-migrate/scripts/migrate_core.py +2 -2
- package/skills/gsd-path-migrate/scripts/path_config.py +5 -5
- package/skills/gsd-path-migrate/scripts/review_panel.py +1 -1
- package/skills/gsd-path-plan/SKILL.md +18 -4
- package/skills/gsd-path-plan/references/dispatch.md +13 -0
- package/skills/gsd-path-plan/references/operating-rules.md +214 -0
- package/skills/gsd-path-plan/references/planner.md +22 -4
- package/skills/gsd-path-plan/references/spec-probes.md +150 -0
- package/skills/gsd-path-plan/scripts/_common.py +619 -18
- package/skills/gsd-path-plan/scripts/archive_milestone.py +160 -86
- package/skills/gsd-path-plan/scripts/build_recovery.py +100 -32
- package/skills/gsd-path-plan/scripts/check_handoffs.py +662 -50
- package/skills/gsd-path-plan/scripts/check_task_briefs.py +338 -30
- package/skills/gsd-path-plan/scripts/detect_project.py +587 -32
- package/skills/gsd-path-plan/scripts/discussion_records.py +19 -13
- package/skills/gsd-path-plan/scripts/discussion_validate.py +152 -50
- package/skills/gsd-path-plan/scripts/integration.py +580 -25
- package/skills/gsd-path-plan/scripts/isolation.py +1438 -57
- package/skills/gsd-path-plan/scripts/loop_run.py +31 -29
- package/skills/gsd-path-plan/scripts/members.py +706 -0
- package/skills/gsd-path-plan/scripts/path_config.py +5 -5
- package/skills/gsd-path-plan/scripts/pipeline_git.py +124 -34
- package/skills/gsd-path-plan/scripts/pipeline_state.py +287 -87
- package/skills/gsd-path-plan/scripts/promote_lookahead.py +1 -1
- package/skills/gsd-path-plan/scripts/review_panel.py +1 -1
- package/skills/gsd-path-plan/scripts/state_checkpoint.py +135 -44
- package/skills/gsd-path-plan/scripts/state_promote.py +2 -0
- package/skills/gsd-path-plan/scripts/workflow_run.py +107 -30
- package/skills/gsd-path-plan/scripts/worktree_paths.py +72 -10
- 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-plan/templates/task.md +1 -0
- package/skills/gsd-path-research/SKILL.md +13 -1
- package/skills/gsd-path-research/references/dispatch.md +13 -0
- package/skills/gsd-path-research/references/operating-rules.md +214 -0
- package/skills/gsd-path-research/scripts/_common.py +619 -18
- package/skills/gsd-path-research/scripts/archive_milestone.py +160 -86
- package/skills/gsd-path-research/scripts/build_recovery.py +100 -32
- package/skills/gsd-path-research/scripts/check_handoffs.py +662 -50
- package/skills/gsd-path-research/scripts/check_task_briefs.py +338 -30
- package/skills/gsd-path-research/scripts/detect_project.py +587 -32
- package/skills/gsd-path-research/scripts/discussion_records.py +19 -13
- package/skills/gsd-path-research/scripts/discussion_validate.py +152 -50
- package/skills/gsd-path-research/scripts/integration.py +580 -25
- package/skills/gsd-path-research/scripts/isolation.py +1438 -57
- package/skills/gsd-path-research/scripts/loop_run.py +31 -29
- package/skills/gsd-path-research/scripts/members.py +706 -0
- package/skills/gsd-path-research/scripts/path_config.py +5 -5
- package/skills/gsd-path-research/scripts/pipeline_git.py +124 -34
- package/skills/gsd-path-research/scripts/pipeline_state.py +287 -87
- package/skills/gsd-path-research/scripts/promote_lookahead.py +1 -1
- package/skills/gsd-path-research/scripts/review_panel.py +1 -1
- package/skills/gsd-path-research/scripts/state_checkpoint.py +135 -44
- package/skills/gsd-path-research/scripts/state_promote.py +2 -0
- package/skills/gsd-path-research/scripts/worktree_paths.py +72 -10
- package/skills/gsd-path-research/templates/research-handoff.md +14 -3
- package/skills/gsd-path-roadmap/SKILL.md +2 -1
- package/skills/gsd-path-roadmap/references/dispatch.md +13 -0
- package/skills/gsd-path-roadmap/references/operating-rules.md +214 -0
- package/skills/gsd-path-roadmap/scripts/_common.py +619 -18
- package/skills/gsd-path-roadmap/scripts/archive_milestone.py +160 -86
- package/skills/gsd-path-roadmap/scripts/build_recovery.py +100 -32
- package/skills/gsd-path-roadmap/scripts/check_handoffs.py +662 -50
- package/skills/gsd-path-roadmap/scripts/check_task_briefs.py +338 -30
- package/skills/gsd-path-roadmap/scripts/detect_project.py +587 -32
- package/skills/gsd-path-roadmap/scripts/discussion_records.py +19 -13
- package/skills/gsd-path-roadmap/scripts/discussion_validate.py +152 -50
- package/skills/gsd-path-roadmap/scripts/integration.py +580 -25
- package/skills/gsd-path-roadmap/scripts/isolation.py +1438 -57
- package/skills/gsd-path-roadmap/scripts/loop_run.py +31 -29
- package/skills/gsd-path-roadmap/scripts/members.py +706 -0
- package/skills/gsd-path-roadmap/scripts/path_config.py +5 -5
- package/skills/gsd-path-roadmap/scripts/pipeline_git.py +124 -34
- package/skills/gsd-path-roadmap/scripts/pipeline_state.py +287 -87
- package/skills/gsd-path-roadmap/scripts/promote_lookahead.py +1 -1
- package/skills/gsd-path-roadmap/scripts/review_panel.py +1 -1
- package/skills/gsd-path-roadmap/scripts/state_checkpoint.py +135 -44
- package/skills/gsd-path-roadmap/scripts/state_promote.py +2 -0
- package/skills/gsd-path-roadmap/scripts/worktree_paths.py +72 -10
- package/skills/gsd-path-ship/SKILL.md +33 -11
- package/skills/gsd-path-ship/references/dispatch.md +13 -0
- package/skills/gsd-path-ship/references/operating-rules.md +214 -0
- package/skills/gsd-path-ship/references/reviewer-final.md +20 -2
- package/skills/gsd-path-ship/references/reviewer-wave.md +18 -5
- 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 +619 -18
- package/skills/gsd-path-ship/scripts/archive_milestone.py +160 -86
- package/skills/gsd-path-ship/scripts/build_recovery.py +100 -32
- package/skills/gsd-path-ship/scripts/build_state.py +143 -24
- package/skills/gsd-path-ship/scripts/check_handoffs.py +662 -50
- package/skills/gsd-path-ship/scripts/check_task_briefs.py +338 -30
- package/skills/gsd-path-ship/scripts/detect_project.py +587 -32
- package/skills/gsd-path-ship/scripts/discussion_records.py +19 -13
- package/skills/gsd-path-ship/scripts/discussion_validate.py +152 -50
- package/skills/gsd-path-ship/scripts/integration.py +580 -25
- package/skills/gsd-path-ship/scripts/isolation.py +1438 -57
- package/skills/gsd-path-ship/scripts/lean_verification.py +99 -19
- package/skills/gsd-path-ship/scripts/loop_run.py +31 -29
- package/skills/gsd-path-ship/scripts/members.py +706 -0
- package/skills/gsd-path-ship/scripts/path_config.py +5 -5
- package/skills/gsd-path-ship/scripts/pipeline_git.py +124 -34
- package/skills/gsd-path-ship/scripts/pipeline_state.py +287 -87
- package/skills/gsd-path-ship/scripts/promote_lookahead.py +1 -1
- package/skills/gsd-path-ship/scripts/review_panel.py +1 -1
- package/skills/gsd-path-ship/scripts/state_checkpoint.py +135 -44
- package/skills/gsd-path-ship/scripts/state_promote.py +2 -0
- package/skills/gsd-path-ship/scripts/workflow_run.py +107 -30
- package/skills/gsd-path-ship/scripts/worktree_paths.py +72 -10
- package/skills/gsd-path-ship/templates/final-review.md +16 -4
- package/skills/gsd-path-ship/templates/gap-review.md +2 -1
- package/skills/gsd-path-ship/templates/patch-findings.md +3 -3
- package/skills/gsd-path-ship/templates/wave-review.md +16 -4
- package/skills/gsd-path-undo/SKILL.md +2 -1
- package/skills/gsd-path-undo/references/operating-rules.md +214 -0
- package/skills/gsd-path-undo/scripts/_common.py +619 -18
- package/skills/gsd-path-undo/scripts/discussion_records.py +19 -13
- package/skills/gsd-path-undo/scripts/loop_run.py +31 -29
- package/skills/gsd-path-undo/scripts/path_config.py +5 -5
- package/skills/gsd-path-undo/scripts/pipeline_undo.py +218 -30
- package/skills/gsd-path-undo/scripts/review_panel.py +1 -1
- package/skills/gsd-path-undo/scripts/worktree_paths.py +72 -10
- package/skills/path/BUILD.md +7 -1
- package/skills/path/DECIDE.md +11 -4
- package/skills/path/DEFINE.md +36 -4
- package/skills/path/DOCS-AUDIT.md +6 -4
- package/skills/path/INSPECT.md +5 -4
- package/skills/path/PLAN.md +18 -4
- package/skills/path/RESEARCH.md +13 -1
- package/skills/path/ROADMAP.md +2 -1
- package/skills/path/SHIP.md +33 -11
- package/skills/path/SKILL.md +45 -1
- package/skills/path/references/build-native.md +63 -15
- package/skills/path/references/codebase-mapper.md +2 -1
- package/skills/path/references/dispatch.md +13 -0
- package/skills/path/references/docs-auditor.md +7 -1
- package/skills/path/references/operating-rules.md +214 -0
- package/skills/path/references/planner.md +22 -4
- package/skills/path/references/reviewer-final.md +20 -2
- package/skills/path/references/reviewer-wave.md +18 -5
- package/skills/path/references/reviewer.md +3 -1
- package/skills/path/references/spec-probes.md +150 -0
- package/skills/path/scripts/_common.py +619 -18
- package/skills/path/scripts/archive_milestone.py +160 -86
- package/skills/path/scripts/bootstrap_repository.py +8 -5
- package/skills/path/scripts/build_recovery.py +100 -32
- package/skills/path/scripts/build_state.py +143 -24
- package/skills/path/scripts/check_docs_audit.py +91 -7
- package/skills/path/scripts/check_handoffs.py +662 -50
- package/skills/path/scripts/check_task_briefs.py +338 -30
- package/skills/path/scripts/check_update.py +2 -2
- package/skills/path/scripts/detect_project.py +587 -32
- package/skills/path/scripts/discussion_records.py +19 -13
- package/skills/path/scripts/discussion_validate.py +152 -50
- package/skills/path/scripts/dispatch_driver.py +501 -46
- package/skills/path/scripts/git_guard.py +118 -31
- package/skills/path/scripts/integration.py +580 -25
- package/skills/path/scripts/isolation.py +1438 -57
- package/skills/path/scripts/lean_verification.py +99 -19
- package/skills/path/scripts/loop_run.py +31 -29
- package/skills/path/scripts/members.py +706 -0
- package/skills/path/scripts/path_config.py +5 -5
- package/skills/path/scripts/pipeline_git.py +124 -34
- package/skills/path/scripts/pipeline_state.py +287 -87
- package/skills/path/scripts/promote_lookahead.py +1 -1
- package/skills/path/scripts/review_panel.py +1 -1
- package/skills/path/scripts/state_checkpoint.py +135 -44
- package/skills/path/scripts/state_promote.py +2 -0
- package/skills/path/scripts/status_runtime.py +58 -10
- package/skills/path/scripts/task_context.py +12 -3
- package/skills/path/scripts/token_budget.py +12 -8
- package/skills/path/scripts/workflow_run.py +107 -30
- package/skills/path/scripts/worktree_paths.py +72 -10
- package/skills/path/templates/codebase.md +1 -1
- package/skills/path/templates/docs-audit.md +6 -2
- package/skills/path/templates/final-review.md +16 -4
- package/skills/path/templates/gap-review.md +2 -1
- 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/task.md +1 -0
- package/skills/path/templates/wave-review.md +16 -4
- 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
|
@@ -13,12 +13,16 @@ sys.dont_write_bytecode = True
|
|
|
13
13
|
from pathlib import Path, PurePosixPath
|
|
14
14
|
from typing import Dict, List, Optional, Sequence, Set, Tuple
|
|
15
15
|
|
|
16
|
-
|
|
17
|
-
from pipeline_state import PipelineStateError, load_state
|
|
18
|
-
import _common
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
16
|
+
if __package__:
|
|
17
|
+
from .pipeline_state import PipelineStateError, load_state
|
|
18
|
+
from . import _common
|
|
19
|
+
else:
|
|
20
|
+
try:
|
|
21
|
+
from pipeline_state import PipelineStateError, load_state
|
|
22
|
+
import _common
|
|
23
|
+
except ImportError: # pragma: no cover - package imports used by tests
|
|
24
|
+
from scripts.pipeline_state import PipelineStateError, load_state
|
|
25
|
+
from scripts import _common
|
|
22
26
|
|
|
23
27
|
|
|
24
28
|
PIPELINE = "gsd-path/v2"
|
|
@@ -29,7 +33,11 @@ DISPATCH_PATTERN = re.compile(
|
|
|
29
33
|
r"^- `(?P<dimension>[^`]+)` — (?P<status>dispatched|skipped) "
|
|
30
34
|
r"→ (?P<output>[^—]+) — (?P<reason>.+)$"
|
|
31
35
|
)
|
|
32
|
-
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] "
|
|
33
41
|
FINDING_PATTERN = re.compile(r"^### (?P<id>P\d{3}) — (?P<title>.+)$")
|
|
34
42
|
CRITERION_LOCATOR_PATTERN = re.compile(r"^SC[1-9]\d*$")
|
|
35
43
|
COMMENT_PATTERN = re.compile(r"<!--.*?-->", re.DOTALL)
|
|
@@ -52,6 +60,21 @@ WAVE_REVIEW_NAME = re.compile(
|
|
|
52
60
|
r"wave-(?P<wave>[1-9]\d*)\.cycle(?P<cycle>[1-9]\d*)"
|
|
53
61
|
r"(?:\.(?P<lens>contract|adversarial))?\.md$"
|
|
54
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
|
+
|
|
55
78
|
WAVE_SC_HEADING = re.compile(r"^### (SC[1-9]\d*) — (.+)$")
|
|
56
79
|
WAVE_TASK_HEADING = re.compile(
|
|
57
80
|
r"^## (?P<task>T\d{3}) — (?P<title>.+): (?P<verdict>pass|fail)$"
|
|
@@ -69,6 +92,23 @@ SURFACE_HEADING = re.compile(
|
|
|
69
92
|
r"(?m)^### (?P<surface>\S.*?) — (?P<task>T\d{3})\s*$"
|
|
70
93
|
)
|
|
71
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"^:?-+:?$")
|
|
72
112
|
GAP_NAME_PATTERN = re.compile(r"^final-gap-(?P<number>[1-9]\d*)\.md$")
|
|
73
113
|
GAP_HEADING_PATTERN = re.compile(
|
|
74
114
|
r"^# Gap Review — (?P<number>[1-9]\d*): (?P<risk>\S.*)$"
|
|
@@ -177,7 +217,7 @@ def _read(root: Path, relative: str) -> str:
|
|
|
177
217
|
path = root / relative
|
|
178
218
|
if not path.is_file() or path.is_symlink():
|
|
179
219
|
raise HandoffError(f"missing real hand-off file: {relative}")
|
|
180
|
-
return
|
|
220
|
+
return _common.read_user_text(path)
|
|
181
221
|
|
|
182
222
|
|
|
183
223
|
def _require_pipeline(root: Path, project_dir: str) -> Dict[str, str]:
|
|
@@ -357,7 +397,7 @@ def _research_questions(intent: str) -> List[str]:
|
|
|
357
397
|
]
|
|
358
398
|
|
|
359
399
|
|
|
360
|
-
def _research_assignments(section: str) -> List[Tuple[str, str]]:
|
|
400
|
+
def _research_assignments(section: str) -> List[Tuple[str, List[str]]]:
|
|
361
401
|
assignments = []
|
|
362
402
|
lines = [line.strip() for line in section.splitlines() if line.strip()]
|
|
363
403
|
if lines == ["- none"]:
|
|
@@ -368,7 +408,25 @@ def _research_assignments(section: str) -> List[Tuple[str, str]]:
|
|
|
368
408
|
match = QUESTION_PATTERN.fullmatch(line)
|
|
369
409
|
if match is None:
|
|
370
410
|
raise HandoffError("RESEARCH.md has a malformed question assignment")
|
|
371
|
-
|
|
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))
|
|
372
430
|
return assignments
|
|
373
431
|
|
|
374
432
|
|
|
@@ -377,7 +435,10 @@ def validate_research(
|
|
|
377
435
|
) -> Dict[str, object]:
|
|
378
436
|
"""Validate the research-to-synthesis hand-off and return its summary."""
|
|
379
437
|
|
|
380
|
-
|
|
438
|
+
# Research runs this before closing; decide runs it after its entry transition.
|
|
439
|
+
_require_state_one_of(
|
|
440
|
+
root, {("research", "active"), ("decide", "active")}, project_dir
|
|
441
|
+
)
|
|
381
442
|
return validate_research_artifacts(root, project_dir)
|
|
382
443
|
|
|
383
444
|
|
|
@@ -439,19 +500,23 @@ def validate_research_artifacts(
|
|
|
439
500
|
|
|
440
501
|
assignments = _research_assignments(_section(handoff, "Question assignments"))
|
|
441
502
|
questions = _research_questions(intent)
|
|
442
|
-
assigned_questions = [question for question,
|
|
443
|
-
if sorted(assigned_questions) != sorted(questions):
|
|
444
|
-
raise HandoffError("RESEARCH.md question assignments do not match the intent source")
|
|
503
|
+
assigned_questions = [question for question, _dimensions in assignments]
|
|
445
504
|
if len(assigned_questions) != len(set(assigned_questions)):
|
|
446
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")
|
|
447
508
|
dispatched_set = set(dispatched)
|
|
448
|
-
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
|
+
):
|
|
449
514
|
raise HandoffError("every assigned question must target a dispatched dimension")
|
|
450
515
|
for dimension in dispatched:
|
|
451
516
|
dimension_questions = [
|
|
452
517
|
question
|
|
453
|
-
for question,
|
|
454
|
-
if
|
|
518
|
+
for question, assigned_dimensions in assignments
|
|
519
|
+
if dimension in assigned_dimensions
|
|
455
520
|
]
|
|
456
521
|
if not dimension_questions:
|
|
457
522
|
raise HandoffError(f"dispatched {dimension} has no assigned research question")
|
|
@@ -489,7 +554,7 @@ def _field(block: str, field: str) -> str:
|
|
|
489
554
|
|
|
490
555
|
|
|
491
556
|
def _raw_source_field(block: str, field: str, source: str) -> str:
|
|
492
|
-
matches =
|
|
557
|
+
matches = _common.find_review_bullet_field_values(block.splitlines(), field)
|
|
493
558
|
if not matches:
|
|
494
559
|
raise HandoffError(f"{source} is missing {field}")
|
|
495
560
|
if len(matches) != 1:
|
|
@@ -514,10 +579,16 @@ def _heading_block(text: str, heading: re.Match[str]) -> str:
|
|
|
514
579
|
def _source_repair_fields(source: str, text: str, locator: str) -> Tuple[str, str]:
|
|
515
580
|
if source.endswith("FINAL.md"):
|
|
516
581
|
if not CRITERION_LOCATOR_PATTERN.fullmatch(locator):
|
|
517
|
-
raise HandoffError(
|
|
582
|
+
raise HandoffError(
|
|
583
|
+
f"{source} locator must be an SC<n> id; copy the id from a "
|
|
584
|
+
"criterion heading"
|
|
585
|
+
)
|
|
518
586
|
heading = re.search(rf"(?m)^### {re.escape(locator)} — .+$", text)
|
|
519
587
|
if not heading:
|
|
520
|
-
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
|
+
)
|
|
521
592
|
block = _heading_block(text, heading)
|
|
522
593
|
verdict = _source_field(block, "Verdict", source)
|
|
523
594
|
if verdict not in {"not-met", "unverifiable"}:
|
|
@@ -525,8 +596,13 @@ def _source_repair_fields(source: str, text: str, locator: str) -> Tuple[str, st
|
|
|
525
596
|
evidence = _source_field(block, "Finding", source)
|
|
526
597
|
fix_direction = _source_field(block, "Fix direction", source)
|
|
527
598
|
else:
|
|
528
|
-
|
|
529
|
-
|
|
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
|
+
)
|
|
530
606
|
block = _section(text, "Finding")
|
|
531
607
|
evidence = _source_field(block, "Found", source)
|
|
532
608
|
fix_direction = _source_field(block, "Fix direction", source)
|
|
@@ -548,6 +624,45 @@ def _reviewed_head(text: str, source: str) -> str:
|
|
|
548
624
|
return value
|
|
549
625
|
|
|
550
626
|
|
|
627
|
+
MEMBER_HEAD_PATTERN = re.compile(r"(?m)^Member reviewed HEAD:(.*)$")
|
|
628
|
+
|
|
629
|
+
|
|
630
|
+
def member_reviewed_heads(text: str, source: str) -> Dict[str, str]:
|
|
631
|
+
"""`Member reviewed HEAD: <member> <sha>` lines, one per member of a multi-repo milestone."""
|
|
632
|
+
heads: Dict[str, str] = {}
|
|
633
|
+
# Only the header binds heads; command output quoted in later sections cannot add one.
|
|
634
|
+
for value in MEMBER_HEAD_PATTERN.findall(_strip_comments(text).split("\n## ", 1)[0]):
|
|
635
|
+
name, _, sha = value.strip().partition(" ")
|
|
636
|
+
if not name or not SHA_PATTERN.fullmatch(sha.strip()):
|
|
637
|
+
raise HandoffError(
|
|
638
|
+
f"{source} Member reviewed HEAD must be '<member> <full commit SHA>', not {value.strip() or 'empty'}"
|
|
639
|
+
)
|
|
640
|
+
if name in heads:
|
|
641
|
+
raise HandoffError(f"{source} repeats Member reviewed HEAD for {name}")
|
|
642
|
+
heads[name] = sha.strip()
|
|
643
|
+
return heads
|
|
644
|
+
|
|
645
|
+
|
|
646
|
+
def _member_tips(root: Path) -> Dict[str, str]:
|
|
647
|
+
"""Each locked member's bound branch tip; empty when the milestone locks no member."""
|
|
648
|
+
try:
|
|
649
|
+
import isolation
|
|
650
|
+
except ImportError: # pragma: no cover - package import used by tests
|
|
651
|
+
from scripts import isolation
|
|
652
|
+
lock = root / ".project/build/members.json"
|
|
653
|
+
if not lock.is_file():
|
|
654
|
+
return {}
|
|
655
|
+
tips: Dict[str, str] = {}
|
|
656
|
+
try:
|
|
657
|
+
for item in json.loads(lock.read_text(encoding="utf-8"))["members"]:
|
|
658
|
+
checkout, _, entry = isolation._member_context(root, item["name"])
|
|
659
|
+
tips[entry["name"]] = isolation.git_output(
|
|
660
|
+
checkout, "rev-parse", "--verify", f"refs/heads/{entry['branch']}^{{commit}}")
|
|
661
|
+
except isolation.IsolationError as error:
|
|
662
|
+
raise HandoffError(str(error)) from error
|
|
663
|
+
return tips
|
|
664
|
+
|
|
665
|
+
|
|
551
666
|
def _strip_comments(text: str) -> str:
|
|
552
667
|
return COMMENT_PATTERN.sub("", text)
|
|
553
668
|
|
|
@@ -581,6 +696,42 @@ def _success_criteria(intent: str) -> Dict[str, str]:
|
|
|
581
696
|
return {f"SC{number}": text for number, text in items.items()}
|
|
582
697
|
|
|
583
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
|
+
|
|
584
735
|
def _surfaces(text: str, label: str) -> List[str]:
|
|
585
736
|
"""Human-facing surfaces one milestone delivers; empty when `none`."""
|
|
586
737
|
|
|
@@ -597,7 +748,7 @@ def _surfaces(text: str, label: str) -> List[str]:
|
|
|
597
748
|
_non_placeholder(value, f"{label} Surfaces")
|
|
598
749
|
named: List[str] = []
|
|
599
750
|
seen = set()
|
|
600
|
-
for item in value
|
|
751
|
+
for item in _split_top_level_commas(value):
|
|
601
752
|
if not item.strip():
|
|
602
753
|
continue
|
|
603
754
|
surface = _unquoted(item)
|
|
@@ -744,12 +895,346 @@ def _coverage_rows(plan: str) -> List[Tuple[str, str, str]]:
|
|
|
744
895
|
return rows
|
|
745
896
|
|
|
746
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
|
+
|
|
747
1230
|
def _task_texts(root: Path, project_dir: str) -> Dict[str, str]:
|
|
748
1231
|
tasks_dir = root / project_dir / "tasks"
|
|
749
1232
|
if not tasks_dir.is_dir():
|
|
750
1233
|
raise HandoffError(f"missing {project_dir}/tasks")
|
|
751
1234
|
tasks: Dict[str, str] = {}
|
|
752
1235
|
for path in sorted(tasks_dir.iterdir()):
|
|
1236
|
+
if _common.is_ignored_junk(path):
|
|
1237
|
+
continue
|
|
753
1238
|
named = TASK_FILE_NAME.fullmatch(path.name)
|
|
754
1239
|
if named is None or not path.is_file() or path.is_symlink():
|
|
755
1240
|
raise HandoffError(f"non-canonical task artifact: {path.name}")
|
|
@@ -917,6 +1402,20 @@ def _task_deps(task_text: str, task_id: str) -> List[str]:
|
|
|
917
1402
|
return _inline_ids(rendered, "T", f"{task_id} deps")
|
|
918
1403
|
|
|
919
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
|
+
|
|
920
1419
|
def _canonical_repo_path(value: str, label: str) -> str:
|
|
921
1420
|
cleaned = value.strip().strip("`\"'")
|
|
922
1421
|
path = PurePosixPath(cleaned)
|
|
@@ -1008,7 +1507,10 @@ def _validate_task_graph(
|
|
|
1008
1507
|
graph: Dict[str, List[str]] = {}
|
|
1009
1508
|
task_waves: Dict[str, int] = {}
|
|
1010
1509
|
task_files: Dict[str, List[str]] = {}
|
|
1510
|
+
# `repo:` names the member a task changes; absent means the coordinator.
|
|
1511
|
+
task_repos: Dict[str, str] = {}
|
|
1011
1512
|
for task_id, text in tasks.items():
|
|
1513
|
+
task_repos[task_id] = str(_strict_frontmatter(text, task_id).get("repo") or "")
|
|
1012
1514
|
# Patch mode reopens planning beside landed tasks; only new tasks start clean.
|
|
1013
1515
|
landed = _task_scalar(text, task_id, "status") == "done"
|
|
1014
1516
|
_require_task_structure(task_id, text, initial=initial and not landed)
|
|
@@ -1028,10 +1530,21 @@ def _validate_task_graph(
|
|
|
1028
1530
|
|
|
1029
1531
|
for index, left in enumerate(task_ids):
|
|
1030
1532
|
for right in task_ids[index + 1 :]:
|
|
1031
|
-
if task_waves[left] != task_waves[right]:
|
|
1533
|
+
if task_waves[left] != task_waves[right] or task_repos[left] != task_repos[right]:
|
|
1032
1534
|
continue
|
|
1033
1535
|
overlap = sorted(set(task_files[left]) & set(task_files[right]))
|
|
1034
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
|
|
1035
1548
|
raise HandoffError(
|
|
1036
1549
|
f"same-wave file overlap between {left} and {right}: {', '.join(overlap)}"
|
|
1037
1550
|
)
|
|
@@ -1039,6 +1552,7 @@ def _validate_task_graph(
|
|
|
1039
1552
|
visiting: Set[str] = set()
|
|
1040
1553
|
visited: Set[str] = set()
|
|
1041
1554
|
dependency_files: Dict[str, Set[str]] = {}
|
|
1555
|
+
dependency_pairs: Dict[str, Set[Tuple[str, str]]] = {}
|
|
1042
1556
|
|
|
1043
1557
|
def visit(task_id: str) -> None:
|
|
1044
1558
|
if task_id in visiting:
|
|
@@ -1046,11 +1560,15 @@ def _validate_task_graph(
|
|
|
1046
1560
|
if task_id in visited:
|
|
1047
1561
|
return
|
|
1048
1562
|
visiting.add(task_id)
|
|
1049
|
-
|
|
1563
|
+
dependency_pairs[task_id] = set()
|
|
1050
1564
|
for dependency in graph[task_id]:
|
|
1051
1565
|
visit(dependency)
|
|
1052
|
-
|
|
1053
|
-
|
|
1566
|
+
dependency_pairs[task_id].update((task_repos[dependency], name) for name in task_files[dependency])
|
|
1567
|
+
dependency_pairs[task_id].update(dependency_pairs[dependency])
|
|
1568
|
+
# A dependency's files are supplied only within the same repo.
|
|
1569
|
+
dependency_files[task_id] = {
|
|
1570
|
+
name for repo, name in dependency_pairs[task_id] if repo == task_repos[task_id]
|
|
1571
|
+
}
|
|
1054
1572
|
visiting.remove(task_id)
|
|
1055
1573
|
visited.add(task_id)
|
|
1056
1574
|
|
|
@@ -1060,11 +1578,12 @@ def _validate_task_graph(
|
|
|
1060
1578
|
|
|
1061
1579
|
|
|
1062
1580
|
def plan_brief_inputs(
|
|
1063
|
-
root: Path, project_dir: str = DEFAULT_PROJECT_DIR
|
|
1581
|
+
root: Path, project_dir: str = DEFAULT_PROJECT_DIR, *, initial: bool = True
|
|
1064
1582
|
) -> Tuple[Dict[str, str], Dict[str, Set[str]]]:
|
|
1583
|
+
"""Validate the plan graph, requiring clean unlanded tasks only at approval."""
|
|
1065
1584
|
tasks = _task_texts(root, project_dir)
|
|
1066
1585
|
wave_depths, _ = _plan_waves(_read(root, f"{project_dir}/plan/PLAN.md"))
|
|
1067
|
-
return tasks, _validate_task_graph(wave_depths, tasks, initial=
|
|
1586
|
+
return tasks, _validate_task_graph(wave_depths, tasks, initial=initial)
|
|
1068
1587
|
|
|
1069
1588
|
|
|
1070
1589
|
def _intent_path(project_dir: str) -> str:
|
|
@@ -1212,7 +1731,7 @@ def _head_file(root: Path, relative: str) -> Optional[str]:
|
|
|
1212
1731
|
["git", "show", f"HEAD:{relative}"],
|
|
1213
1732
|
cwd=root,
|
|
1214
1733
|
capture_output=True,
|
|
1215
|
-
|
|
1734
|
+
encoding="utf-8", errors="replace",
|
|
1216
1735
|
check=False,
|
|
1217
1736
|
)
|
|
1218
1737
|
return result.stdout if result.returncode == 0 else None
|
|
@@ -1313,6 +1832,21 @@ def validate_roadmap(
|
|
|
1313
1832
|
return {"phase": "roadmap", "milestones": ids, "dependencies": dependencies}
|
|
1314
1833
|
|
|
1315
1834
|
|
|
1835
|
+
def _integrated_members(root: Path, state: Dict[str, str]) -> Set[str]:
|
|
1836
|
+
"""Members this milestone already closed; a new task for one would need a second merge."""
|
|
1837
|
+
match = re.fullmatch(r"gsd-path/M(\d{3,})", state.get("branch") or "")
|
|
1838
|
+
common = subprocess.run(["git", "-C", str(root), "rev-parse", "--path-format=absolute", "--git-common-dir"],
|
|
1839
|
+
capture_output=True, text=True, check=False)
|
|
1840
|
+
if match is None or common.returncode != 0:
|
|
1841
|
+
return set()
|
|
1842
|
+
if __package__:
|
|
1843
|
+
from . import members
|
|
1844
|
+
else:
|
|
1845
|
+
import members
|
|
1846
|
+
rows = members.milestone_member_close(Path(common.stdout.strip()), int(match.group(1)))
|
|
1847
|
+
return {row["name"] for row in rows if row.get("status") == "integrated"}
|
|
1848
|
+
|
|
1849
|
+
|
|
1316
1850
|
def validate_plan(
|
|
1317
1851
|
root: Path, project_dir: str = DEFAULT_PROJECT_DIR
|
|
1318
1852
|
) -> Dict[str, object]:
|
|
@@ -1332,6 +1866,14 @@ def validate_plan(
|
|
|
1332
1866
|
# Build readiness owns task lifecycle validation; coverage must survive landings.
|
|
1333
1867
|
state = _require_pipeline(root, project_dir)
|
|
1334
1868
|
_validate_task_graph(wave_depths, tasks, initial=state["phase"] == "plan")
|
|
1869
|
+
integrated = _integrated_members(root, state)
|
|
1870
|
+
for task_id, text in tasks.items():
|
|
1871
|
+
repo = str(_strict_frontmatter(text, task_id).get("repo") or "")
|
|
1872
|
+
if repo in integrated and _task_scalar(text, task_id, "status") != "done":
|
|
1873
|
+
raise HandoffError(
|
|
1874
|
+
f"{task_id} changes member {repo}, which is already integrated in this milestone; "
|
|
1875
|
+
"plan that change in a new milestone"
|
|
1876
|
+
)
|
|
1335
1877
|
assigned: Dict[str, Set[str]] = {task_id: set() for task_id in tasks}
|
|
1336
1878
|
covered = set()
|
|
1337
1879
|
for criterion, task_id, acceptance in rows:
|
|
@@ -1396,6 +1938,9 @@ def validate_plan(
|
|
|
1396
1938
|
continue
|
|
1397
1939
|
if not _verify_mentions_a_file(command, _frontmatter_files(text)):
|
|
1398
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)
|
|
1399
1944
|
return {
|
|
1400
1945
|
"phase": "plan",
|
|
1401
1946
|
"criteria": sorted(criteria),
|
|
@@ -1403,6 +1948,7 @@ def validate_plan(
|
|
|
1403
1948
|
"tasks": len(tasks),
|
|
1404
1949
|
"waves": waves,
|
|
1405
1950
|
"surfaces": {surface: task for surface, (task, _) in owners.items()},
|
|
1951
|
+
"held_out": held_out,
|
|
1406
1952
|
}
|
|
1407
1953
|
|
|
1408
1954
|
|
|
@@ -1464,17 +2010,27 @@ def validate_wave_evidence(
|
|
|
1464
2010
|
assigned[task_id].add(criterion)
|
|
1465
2011
|
owned = _owned_by_wave(tasks, assigned, wave)
|
|
1466
2012
|
text = _read(root, review_path.as_posix())
|
|
2013
|
+
is_last_cycle = cycle >= _max_wave_cycle(root, project_dir, wave)
|
|
1467
2014
|
if _line_value(text, "Cycle:") != str(cycle):
|
|
1468
2015
|
raise HandoffError(f"{name} Cycle field does not match its filename")
|
|
1469
|
-
|
|
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:
|
|
1470
2020
|
raise HandoffError(f"{name} Depth does not match PLAN.md")
|
|
1471
2021
|
lens_fields = re.findall(r"(?m)^Lens:[ \t]*(\S.*?)[ \t]*$", text)
|
|
1472
2022
|
if lens is None and lens_fields:
|
|
1473
2023
|
raise HandoffError(f"{name} must not declare a review lens")
|
|
1474
2024
|
if lens is not None and lens_fields != [lens]:
|
|
1475
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")
|
|
1476
2029
|
reviewed = _line_value(text, "Tasks reviewed:")
|
|
1477
|
-
|
|
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):
|
|
1478
2034
|
raise HandoffError(f"{name} Tasks reviewed does not match Wave {wave}")
|
|
1479
2035
|
|
|
1480
2036
|
lines = text.splitlines()
|
|
@@ -1483,11 +2039,25 @@ def validate_wave_evidence(
|
|
|
1483
2039
|
match = WAVE_TASK_HEADING.fullmatch(line)
|
|
1484
2040
|
if match:
|
|
1485
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}")
|
|
1486
2044
|
reviewed_tasks = [match.group("task") for _, match in task_headings]
|
|
1487
|
-
if
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
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
|
+
)
|
|
1491
2061
|
task_verdicts = []
|
|
1492
2062
|
for heading_index, heading in task_headings:
|
|
1493
2063
|
task_id = heading.group("task")
|
|
@@ -1516,9 +2086,16 @@ def validate_wave_evidence(
|
|
|
1516
2086
|
_non_placeholder(item, f"{name} task {task_id} {verdict} evidence")
|
|
1517
2087
|
task_verdicts.append(verdict)
|
|
1518
2088
|
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
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
|
+
|
|
1522
2099
|
if overall == "pass" and "fail" in task_verdicts:
|
|
1523
2100
|
raise HandoffError(f"{name} Wave verdict is pass while a task failed")
|
|
1524
2101
|
try:
|
|
@@ -1591,6 +2168,19 @@ def validate_wave_evidence(
|
|
|
1591
2168
|
}
|
|
1592
2169
|
|
|
1593
2170
|
|
|
2171
|
+
def criterion_surfaces(root: Path, project_dir: str = DEFAULT_PROJECT_DIR) -> Dict[str, str]:
|
|
2172
|
+
"""Map each surface-owned INTENT SC to the surface PLAN.md's contract lists it under."""
|
|
2173
|
+
|
|
2174
|
+
surface_of: Dict[str, str] = {}
|
|
2175
|
+
surfaces = _surfaces(_read(root, _intent_path(project_dir)), "INTENT.md")
|
|
2176
|
+
if surfaces:
|
|
2177
|
+
plan = _read(root, f"{project_dir}/plan/PLAN.md")
|
|
2178
|
+
for surface, (_task, owned) in _surface_contract(plan, surfaces).items():
|
|
2179
|
+
for criterion in owned:
|
|
2180
|
+
surface_of[criterion] = surface
|
|
2181
|
+
return surface_of
|
|
2182
|
+
|
|
2183
|
+
|
|
1594
2184
|
def validate_final(
|
|
1595
2185
|
root: Path, project_dir: str = DEFAULT_PROJECT_DIR, *, final_text: Optional[str] = None
|
|
1596
2186
|
) -> Dict[str, object]:
|
|
@@ -1599,20 +2189,15 @@ def validate_final(
|
|
|
1599
2189
|
_require_state(root, "ship", "active", project_dir)
|
|
1600
2190
|
intent = _read(root, _intent_path(project_dir))
|
|
1601
2191
|
criteria = _success_criteria(intent)
|
|
1602
|
-
surface_of
|
|
1603
|
-
|
|
1604
|
-
if surfaces:
|
|
1605
|
-
plan = _read(root, f"{project_dir}/plan/PLAN.md")
|
|
1606
|
-
for surface, (_task, owned) in _surface_contract(plan, surfaces).items():
|
|
1607
|
-
for criterion in owned:
|
|
1608
|
-
surface_of[criterion] = surface
|
|
2192
|
+
surface_of = criterion_surfaces(root, project_dir)
|
|
2193
|
+
tags = evidence_tags(intent, criteria)
|
|
1609
2194
|
relative = f"{project_dir}/review/FINAL.md"
|
|
1610
2195
|
text = _read(root, relative) if final_text is None else final_text
|
|
1611
2196
|
reviewed_head = _reviewed_head(text, relative)
|
|
1612
2197
|
head_result = subprocess.run(
|
|
1613
2198
|
("git", "-C", str(root), "rev-parse", "--verify", "HEAD"),
|
|
1614
2199
|
capture_output=True,
|
|
1615
|
-
|
|
2200
|
+
encoding="utf-8", errors="replace",
|
|
1616
2201
|
check=False,
|
|
1617
2202
|
)
|
|
1618
2203
|
if head_result.returncode != 0:
|
|
@@ -1623,6 +2208,14 @@ def validate_final(
|
|
|
1623
2208
|
f"FINAL.md Reviewed HEAD {reviewed_head} is not the current HEAD "
|
|
1624
2209
|
f"{current_head}; re-run the final review on the current commit"
|
|
1625
2210
|
)
|
|
2211
|
+
member_heads = member_reviewed_heads(text, relative)
|
|
2212
|
+
tips = _member_tips(root)
|
|
2213
|
+
if member_heads != tips:
|
|
2214
|
+
expected = ", ".join(f"{name} {tip}" for name, tip in tips.items()) or "none"
|
|
2215
|
+
raise HandoffError(
|
|
2216
|
+
f"FINAL.md Member reviewed HEAD lines must name each locked member head ({expected}); "
|
|
2217
|
+
"re-run the final review on the current member heads"
|
|
2218
|
+
)
|
|
1626
2219
|
overall = _line_value(text, "Overall verdict:")
|
|
1627
2220
|
if overall not in {"pass", "blocked"}:
|
|
1628
2221
|
raise HandoffError("FINAL.md Overall verdict is invalid")
|
|
@@ -1675,6 +2268,16 @@ def validate_final(
|
|
|
1675
2268
|
raise HandoffError(
|
|
1676
2269
|
f"FINAL.md {sc_id} met verdict must have no Finding or Fix direction"
|
|
1677
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
|
+
)
|
|
1678
2281
|
elif finding.casefold() == "none" or fix_direction.casefold() == "none":
|
|
1679
2282
|
raise HandoffError(
|
|
1680
2283
|
f"FINAL.md {sc_id} {verdict} verdict requires a Finding and Fix direction"
|
|
@@ -1697,6 +2300,8 @@ def validate_final(
|
|
|
1697
2300
|
raise HandoffError(f"{gap_relative} heading does not match its file number")
|
|
1698
2301
|
if _reviewed_head(gap, gap_relative) != reviewed_head:
|
|
1699
2302
|
raise HandoffError(f"{gap_relative} Reviewed HEAD differs from FINAL.md")
|
|
2303
|
+
if member_reviewed_heads(gap, gap_relative) != member_heads:
|
|
2304
|
+
raise HandoffError(f"{gap_relative} Member reviewed HEAD lines differ from FINAL.md")
|
|
1700
2305
|
verdict = _line_value(gap, "Gap verdict:")
|
|
1701
2306
|
if verdict not in {"pass", "blocked"}:
|
|
1702
2307
|
raise HandoffError(f"{gap_relative} Gap verdict is invalid")
|
|
@@ -1793,10 +2398,15 @@ def validate_patch_findings(
|
|
|
1793
2398
|
source, source_text, locator
|
|
1794
2399
|
)
|
|
1795
2400
|
if evidence != source_evidence:
|
|
1796
|
-
|
|
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
|
+
)
|
|
1797
2406
|
if fix_direction != source_fix_direction:
|
|
1798
2407
|
raise HandoffError(
|
|
1799
|
-
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}"
|
|
1800
2410
|
)
|
|
1801
2411
|
sources.append(source)
|
|
1802
2412
|
|
|
@@ -1814,7 +2424,8 @@ def validate_patch_findings(
|
|
|
1814
2424
|
def parser() -> argparse.ArgumentParser:
|
|
1815
2425
|
argument_parser = argparse.ArgumentParser(description=__doc__)
|
|
1816
2426
|
argument_parser.add_argument(
|
|
1817
|
-
"phase",
|
|
2427
|
+
"phase",
|
|
2428
|
+
choices=("intent", "research", "decide", "roadmap", "patch", "plan", "wave", "final"),
|
|
1818
2429
|
)
|
|
1819
2430
|
argument_parser.add_argument("--repo", type=Path, required=True)
|
|
1820
2431
|
argument_parser.add_argument(
|
|
@@ -1842,6 +2453,7 @@ def main(argv: Optional[Sequence[str]] = None) -> int:
|
|
|
1842
2453
|
result = validate_wave(repo, arguments.project_dir, arguments.review)
|
|
1843
2454
|
else:
|
|
1844
2455
|
validators = {
|
|
2456
|
+
"intent": validate_intent,
|
|
1845
2457
|
"research": validate_research,
|
|
1846
2458
|
"decide": validate_decide,
|
|
1847
2459
|
"roadmap": validate_roadmap,
|