@opengsd/gsd-path 1.5.0 → 1.6.1
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 +1 -1
- package/CHANGELOG.md +169 -0
- package/DOCS.md +4 -3
- package/FULL.md +68 -7
- package/HOOKS.md +81 -4
- package/MIGRATE.md +12 -0
- package/QUICK.md +4 -2
- package/README.md +69 -16
- package/RUNTIME.md +23 -5
- package/UPDATE.md +35 -0
- package/WORKFLOW.md +66 -7
- package/package.json +1 -1
- package/scripts/_common.py +150 -1
- package/scripts/build_recovery.py +62 -30
- package/scripts/build_state.py +195 -5
- package/scripts/check_handoffs.py +92 -30
- package/scripts/check_task_briefs.py +27 -19
- package/scripts/detect_project.py +17 -2
- package/scripts/discussion_records.py +9 -1
- package/scripts/discussion_validate.py +7 -6
- package/scripts/dispatch_driver.py +174 -36
- package/scripts/git_guard.py +42 -7
- package/scripts/guard_hook.py +1216 -47
- package/scripts/install.mjs +76 -3
- package/scripts/install.py +182 -37
- package/scripts/integration.py +115 -68
- package/scripts/isolation.py +192 -23
- package/scripts/members.py +68 -25
- package/scripts/migrate_core.py +2 -0
- package/scripts/model_policy.py +13 -2
- package/scripts/pipeline_diagnose.py +17 -2
- package/scripts/pipeline_git.py +87 -35
- package/scripts/pipeline_state.py +31 -7
- package/scripts/pipeline_undo.py +17 -8
- package/scripts/promote_lookahead.py +3 -3
- package/scripts/review_findings.py +180 -44
- package/scripts/roadmap.py +9 -1
- package/scripts/runtime_store.py +1 -0
- package/scripts/state_checkpoint.py +30 -9
- package/scripts/state_promote.py +92 -5
- package/scripts/task_context.py +2 -0
- package/scripts/workflow_run.py +9 -2
- package/skills/gsd-path/DEFINE.md +6 -2
- package/skills/gsd-path/PLAN.md +6 -2
- package/skills/gsd-path/ROADMAP.md +16 -6
- package/skills/gsd-path/SHIP.md +38 -11
- package/skills/gsd-path/SKILL.md +52 -24
- package/skills/gsd-path/references/build-native.md +96 -9
- package/skills/gsd-path/references/build-recovery.md +13 -0
- package/skills/gsd-path/references/coder.md +7 -3
- package/skills/gsd-path/references/model-policy.md +3 -1
- package/skills/gsd-path/references/operating-rules.md +14 -5
- package/skills/gsd-path/references/planner.md +13 -1
- package/skills/gsd-path/references/reviewer-final.md +5 -5
- package/skills/gsd-path/references/reviewer-wave.md +46 -13
- package/skills/gsd-path/references/runtime-selection.md +10 -0
- package/skills/gsd-path/scripts/_common.py +150 -1
- package/skills/gsd-path/scripts/build_recovery.py +62 -30
- package/skills/gsd-path/scripts/build_state.py +195 -5
- package/skills/gsd-path/scripts/check_handoffs.py +92 -30
- package/skills/gsd-path/scripts/check_task_briefs.py +27 -19
- package/skills/gsd-path/scripts/detect_project.py +17 -2
- package/skills/gsd-path/scripts/discussion_records.py +9 -1
- package/skills/gsd-path/scripts/discussion_validate.py +7 -6
- package/skills/gsd-path/scripts/dispatch_driver.py +174 -36
- package/skills/gsd-path/scripts/git_guard.py +42 -7
- package/skills/gsd-path/scripts/integration.py +115 -68
- package/skills/gsd-path/scripts/isolation.py +192 -23
- package/skills/gsd-path/scripts/members.py +68 -25
- package/skills/gsd-path/scripts/model_policy.py +13 -2
- package/skills/gsd-path/scripts/pipeline_git.py +87 -35
- package/skills/gsd-path/scripts/pipeline_state.py +31 -7
- package/skills/gsd-path/scripts/promote_lookahead.py +3 -3
- package/skills/gsd-path/scripts/review_findings.py +180 -44
- package/skills/gsd-path/scripts/roadmap.py +9 -1
- package/skills/gsd-path/scripts/state_checkpoint.py +30 -9
- package/skills/gsd-path/scripts/state_promote.py +92 -5
- package/skills/gsd-path/scripts/task_context.py +2 -0
- package/skills/gsd-path/scripts/workflow_run.py +9 -2
- package/skills/gsd-path/templates/final-review.md +5 -5
- package/skills/gsd-path/templates/state.md +1 -0
- package/skills/gsd-path/templates/task.md +1 -0
- package/skills/gsd-path/templates/wave-review.md +18 -9
- package/skills/gsd-path-build/references/build-native.md +96 -9
- package/skills/gsd-path-build/references/build-recovery.md +13 -0
- package/skills/gsd-path-build/references/coder.md +7 -3
- package/skills/gsd-path-build/references/model-policy.md +3 -1
- package/skills/gsd-path-build/references/operating-rules.md +14 -5
- package/skills/gsd-path-build/references/reviewer-final.md +5 -5
- package/skills/gsd-path-build/references/reviewer-wave.md +46 -13
- package/skills/gsd-path-build/references/runtime-selection.md +10 -0
- package/skills/gsd-path-build/scripts/_common.py +150 -1
- package/skills/gsd-path-build/scripts/build_recovery.py +62 -30
- package/skills/gsd-path-build/scripts/build_state.py +195 -5
- package/skills/gsd-path-build/scripts/check_handoffs.py +92 -30
- package/skills/gsd-path-build/scripts/check_task_briefs.py +27 -19
- package/skills/gsd-path-build/scripts/detect_project.py +17 -2
- package/skills/gsd-path-build/scripts/discussion_records.py +9 -1
- package/skills/gsd-path-build/scripts/discussion_validate.py +7 -6
- package/skills/gsd-path-build/scripts/dispatch_driver.py +174 -36
- package/skills/gsd-path-build/scripts/integration.py +115 -68
- package/skills/gsd-path-build/scripts/isolation.py +192 -23
- package/skills/gsd-path-build/scripts/members.py +68 -25
- package/skills/gsd-path-build/scripts/model_policy.py +13 -2
- package/skills/gsd-path-build/scripts/pipeline_git.py +87 -35
- package/skills/gsd-path-build/scripts/pipeline_state.py +31 -7
- package/skills/gsd-path-build/scripts/promote_lookahead.py +3 -3
- package/skills/gsd-path-build/scripts/review_findings.py +180 -44
- package/skills/gsd-path-build/scripts/roadmap.py +9 -1
- package/skills/gsd-path-build/scripts/state_checkpoint.py +30 -9
- package/skills/gsd-path-build/scripts/state_promote.py +92 -5
- package/skills/gsd-path-build/scripts/task_context.py +2 -0
- package/skills/gsd-path-build/scripts/workflow_run.py +9 -2
- package/skills/gsd-path-build/templates/task.md +1 -0
- package/skills/gsd-path-build/templates/wave-review.md +18 -9
- package/skills/gsd-path-decide/references/model-policy.md +3 -1
- package/skills/gsd-path-decide/references/operating-rules.md +14 -5
- package/skills/gsd-path-decide/references/runtime-selection.md +10 -0
- package/skills/gsd-path-decide/scripts/_common.py +150 -1
- package/skills/gsd-path-decide/scripts/build_recovery.py +62 -30
- package/skills/gsd-path-decide/scripts/check_handoffs.py +92 -30
- package/skills/gsd-path-decide/scripts/check_task_briefs.py +27 -19
- package/skills/gsd-path-decide/scripts/detect_project.py +17 -2
- package/skills/gsd-path-decide/scripts/discussion_records.py +9 -1
- package/skills/gsd-path-decide/scripts/discussion_validate.py +7 -6
- package/skills/gsd-path-decide/scripts/integration.py +115 -68
- package/skills/gsd-path-decide/scripts/isolation.py +192 -23
- package/skills/gsd-path-decide/scripts/members.py +68 -25
- package/skills/gsd-path-decide/scripts/model_policy.py +13 -2
- package/skills/gsd-path-decide/scripts/pipeline_git.py +87 -35
- package/skills/gsd-path-decide/scripts/pipeline_state.py +31 -7
- package/skills/gsd-path-decide/scripts/promote_lookahead.py +3 -3
- package/skills/gsd-path-decide/scripts/roadmap.py +9 -1
- package/skills/gsd-path-decide/scripts/state_checkpoint.py +30 -9
- package/skills/gsd-path-decide/scripts/state_promote.py +92 -5
- package/skills/gsd-path-define/SKILL.md +6 -2
- package/skills/gsd-path-define/references/build-recovery.md +13 -0
- package/skills/gsd-path-define/references/operating-rules.md +14 -5
- package/skills/gsd-path-define/references/runtime-selection.md +10 -0
- package/skills/gsd-path-define/scripts/_common.py +150 -1
- package/skills/gsd-path-define/scripts/build_recovery.py +62 -30
- package/skills/gsd-path-define/scripts/check_handoffs.py +92 -30
- package/skills/gsd-path-define/scripts/check_task_briefs.py +27 -19
- package/skills/gsd-path-define/scripts/detect_project.py +17 -2
- package/skills/gsd-path-define/scripts/discussion_records.py +9 -1
- package/skills/gsd-path-define/scripts/discussion_validate.py +7 -6
- package/skills/gsd-path-define/scripts/integration.py +115 -68
- package/skills/gsd-path-define/scripts/isolation.py +192 -23
- package/skills/gsd-path-define/scripts/members.py +68 -25
- package/skills/gsd-path-define/scripts/model_policy.py +13 -2
- package/skills/gsd-path-define/scripts/pipeline_git.py +87 -35
- package/skills/gsd-path-define/scripts/pipeline_state.py +31 -7
- package/skills/gsd-path-define/scripts/promote_lookahead.py +3 -3
- package/skills/gsd-path-define/scripts/roadmap.py +9 -1
- package/skills/gsd-path-define/scripts/state_checkpoint.py +30 -9
- package/skills/gsd-path-define/scripts/state_promote.py +92 -5
- package/skills/gsd-path-define/templates/state.md +1 -0
- package/skills/gsd-path-discuss/references/operating-rules.md +14 -5
- package/skills/gsd-path-discuss/references/runtime-selection.md +10 -0
- package/skills/gsd-path-discuss/scripts/_common.py +150 -1
- package/skills/gsd-path-discuss/scripts/discussion_records.py +9 -1
- package/skills/gsd-path-discuss/scripts/model_policy.py +13 -2
- package/skills/gsd-path-docs-audit/references/model-policy.md +3 -1
- package/skills/gsd-path-docs-audit/references/operating-rules.md +14 -5
- package/skills/gsd-path-docs-audit/references/runtime-selection.md +10 -0
- package/skills/gsd-path-docs-audit/scripts/_common.py +150 -1
- package/skills/gsd-path-docs-audit/scripts/build_recovery.py +62 -30
- package/skills/gsd-path-docs-audit/scripts/check_handoffs.py +92 -30
- package/skills/gsd-path-docs-audit/scripts/check_task_briefs.py +27 -19
- package/skills/gsd-path-docs-audit/scripts/detect_project.py +17 -2
- package/skills/gsd-path-docs-audit/scripts/discussion_records.py +9 -1
- package/skills/gsd-path-docs-audit/scripts/discussion_validate.py +7 -6
- package/skills/gsd-path-docs-audit/scripts/integration.py +115 -68
- package/skills/gsd-path-docs-audit/scripts/isolation.py +192 -23
- package/skills/gsd-path-docs-audit/scripts/members.py +68 -25
- package/skills/gsd-path-docs-audit/scripts/model_policy.py +13 -2
- package/skills/gsd-path-docs-audit/scripts/pipeline_git.py +87 -35
- package/skills/gsd-path-docs-audit/scripts/pipeline_state.py +31 -7
- package/skills/gsd-path-docs-audit/scripts/promote_lookahead.py +3 -3
- package/skills/gsd-path-docs-audit/scripts/roadmap.py +9 -1
- package/skills/gsd-path-docs-audit/scripts/state_checkpoint.py +30 -9
- package/skills/gsd-path-docs-audit/scripts/state_promote.py +92 -5
- package/skills/gsd-path-forensics/references/operating-rules.md +14 -5
- package/skills/gsd-path-forensics/references/runtime-selection.md +10 -0
- package/skills/gsd-path-forensics/scripts/_common.py +150 -1
- package/skills/gsd-path-forensics/scripts/discussion_records.py +9 -1
- package/skills/gsd-path-forensics/scripts/model_policy.py +13 -2
- package/skills/gsd-path-forensics/scripts/pipeline_diagnose.py +17 -2
- package/skills/gsd-path-forensics/scripts/pipeline_undo.py +17 -8
- package/skills/gsd-path-inspect/references/model-policy.md +3 -1
- package/skills/gsd-path-inspect/references/operating-rules.md +14 -5
- package/skills/gsd-path-inspect/references/runtime-selection.md +10 -0
- package/skills/gsd-path-inspect/scripts/_common.py +150 -1
- package/skills/gsd-path-inspect/scripts/build_recovery.py +62 -30
- package/skills/gsd-path-inspect/scripts/check_handoffs.py +92 -30
- package/skills/gsd-path-inspect/scripts/check_task_briefs.py +27 -19
- package/skills/gsd-path-inspect/scripts/detect_project.py +17 -2
- package/skills/gsd-path-inspect/scripts/discussion_records.py +9 -1
- package/skills/gsd-path-inspect/scripts/discussion_validate.py +7 -6
- package/skills/gsd-path-inspect/scripts/integration.py +115 -68
- package/skills/gsd-path-inspect/scripts/isolation.py +192 -23
- package/skills/gsd-path-inspect/scripts/members.py +68 -25
- package/skills/gsd-path-inspect/scripts/model_policy.py +13 -2
- package/skills/gsd-path-inspect/scripts/pipeline_git.py +87 -35
- package/skills/gsd-path-inspect/scripts/pipeline_state.py +31 -7
- package/skills/gsd-path-inspect/scripts/promote_lookahead.py +3 -3
- package/skills/gsd-path-inspect/scripts/roadmap.py +9 -1
- package/skills/gsd-path-inspect/scripts/state_checkpoint.py +30 -9
- package/skills/gsd-path-inspect/scripts/state_promote.py +92 -5
- package/skills/gsd-path-inspect/scripts/workflow_run.py +9 -2
- package/skills/gsd-path-inspect/templates/state.md +1 -0
- package/skills/gsd-path-loop/references/operating-rules.md +14 -5
- package/skills/gsd-path-loop/references/runtime-selection.md +10 -0
- package/skills/gsd-path-loop/scripts/_common.py +150 -1
- package/skills/gsd-path-loop/scripts/discussion_records.py +9 -1
- package/skills/gsd-path-loop/scripts/model_policy.py +13 -2
- package/skills/gsd-path-migrate/SKILL.md +8 -0
- package/skills/gsd-path-migrate/references/operating-rules.md +14 -5
- package/skills/gsd-path-migrate/references/runtime-selection.md +10 -0
- package/skills/gsd-path-migrate/scripts/_common.py +150 -1
- package/skills/gsd-path-migrate/scripts/discussion_records.py +9 -1
- package/skills/gsd-path-migrate/scripts/migrate_core.py +2 -0
- package/skills/gsd-path-migrate/scripts/model_policy.py +13 -2
- package/skills/gsd-path-plan/SKILL.md +6 -2
- package/skills/gsd-path-plan/references/build-recovery.md +13 -0
- package/skills/gsd-path-plan/references/model-policy.md +3 -1
- package/skills/gsd-path-plan/references/operating-rules.md +14 -5
- package/skills/gsd-path-plan/references/planner.md +13 -1
- package/skills/gsd-path-plan/references/runtime-selection.md +10 -0
- package/skills/gsd-path-plan/scripts/_common.py +150 -1
- package/skills/gsd-path-plan/scripts/build_recovery.py +62 -30
- package/skills/gsd-path-plan/scripts/check_handoffs.py +92 -30
- package/skills/gsd-path-plan/scripts/check_task_briefs.py +27 -19
- package/skills/gsd-path-plan/scripts/detect_project.py +17 -2
- package/skills/gsd-path-plan/scripts/discussion_records.py +9 -1
- package/skills/gsd-path-plan/scripts/discussion_validate.py +7 -6
- package/skills/gsd-path-plan/scripts/integration.py +115 -68
- package/skills/gsd-path-plan/scripts/isolation.py +192 -23
- package/skills/gsd-path-plan/scripts/members.py +68 -25
- package/skills/gsd-path-plan/scripts/model_policy.py +13 -2
- package/skills/gsd-path-plan/scripts/pipeline_git.py +87 -35
- package/skills/gsd-path-plan/scripts/pipeline_state.py +31 -7
- package/skills/gsd-path-plan/scripts/promote_lookahead.py +3 -3
- package/skills/gsd-path-plan/scripts/roadmap.py +9 -1
- package/skills/gsd-path-plan/scripts/state_checkpoint.py +30 -9
- package/skills/gsd-path-plan/scripts/state_promote.py +92 -5
- package/skills/gsd-path-plan/scripts/workflow_run.py +9 -2
- package/skills/gsd-path-plan/templates/task.md +1 -0
- package/skills/gsd-path-research/references/model-policy.md +3 -1
- package/skills/gsd-path-research/references/operating-rules.md +14 -5
- package/skills/gsd-path-research/references/runtime-selection.md +10 -0
- package/skills/gsd-path-research/scripts/_common.py +150 -1
- package/skills/gsd-path-research/scripts/build_recovery.py +62 -30
- package/skills/gsd-path-research/scripts/check_handoffs.py +92 -30
- package/skills/gsd-path-research/scripts/check_task_briefs.py +27 -19
- package/skills/gsd-path-research/scripts/detect_project.py +17 -2
- package/skills/gsd-path-research/scripts/discussion_records.py +9 -1
- package/skills/gsd-path-research/scripts/discussion_validate.py +7 -6
- package/skills/gsd-path-research/scripts/integration.py +115 -68
- package/skills/gsd-path-research/scripts/isolation.py +192 -23
- package/skills/gsd-path-research/scripts/members.py +68 -25
- package/skills/gsd-path-research/scripts/model_policy.py +13 -2
- package/skills/gsd-path-research/scripts/pipeline_git.py +87 -35
- package/skills/gsd-path-research/scripts/pipeline_state.py +31 -7
- package/skills/gsd-path-research/scripts/promote_lookahead.py +3 -3
- package/skills/gsd-path-research/scripts/roadmap.py +9 -1
- package/skills/gsd-path-research/scripts/state_checkpoint.py +30 -9
- package/skills/gsd-path-research/scripts/state_promote.py +92 -5
- package/skills/gsd-path-roadmap/SKILL.md +16 -6
- package/skills/gsd-path-roadmap/references/model-policy.md +3 -1
- package/skills/gsd-path-roadmap/references/operating-rules.md +14 -5
- package/skills/gsd-path-roadmap/references/runtime-selection.md +10 -0
- package/skills/gsd-path-roadmap/scripts/_common.py +150 -1
- package/skills/gsd-path-roadmap/scripts/build_recovery.py +62 -30
- package/skills/gsd-path-roadmap/scripts/check_handoffs.py +92 -30
- package/skills/gsd-path-roadmap/scripts/check_task_briefs.py +27 -19
- package/skills/gsd-path-roadmap/scripts/detect_project.py +17 -2
- package/skills/gsd-path-roadmap/scripts/discussion_records.py +9 -1
- package/skills/gsd-path-roadmap/scripts/discussion_validate.py +7 -6
- package/skills/gsd-path-roadmap/scripts/integration.py +115 -68
- package/skills/gsd-path-roadmap/scripts/isolation.py +192 -23
- package/skills/gsd-path-roadmap/scripts/members.py +68 -25
- package/skills/gsd-path-roadmap/scripts/model_policy.py +13 -2
- package/skills/gsd-path-roadmap/scripts/pipeline_git.py +87 -35
- package/skills/gsd-path-roadmap/scripts/pipeline_state.py +31 -7
- package/skills/gsd-path-roadmap/scripts/promote_lookahead.py +3 -3
- package/skills/gsd-path-roadmap/scripts/roadmap.py +9 -1
- package/skills/gsd-path-roadmap/scripts/state_checkpoint.py +30 -9
- package/skills/gsd-path-roadmap/scripts/state_promote.py +92 -5
- package/skills/gsd-path-ship/SKILL.md +38 -11
- package/skills/gsd-path-ship/references/model-policy.md +3 -1
- package/skills/gsd-path-ship/references/operating-rules.md +14 -5
- package/skills/gsd-path-ship/references/reviewer-final.md +5 -5
- package/skills/gsd-path-ship/references/reviewer-wave.md +46 -13
- package/skills/gsd-path-ship/references/runtime-selection.md +10 -0
- package/skills/gsd-path-ship/scripts/_common.py +150 -1
- package/skills/gsd-path-ship/scripts/build_recovery.py +62 -30
- package/skills/gsd-path-ship/scripts/build_state.py +195 -5
- package/skills/gsd-path-ship/scripts/check_handoffs.py +92 -30
- package/skills/gsd-path-ship/scripts/check_task_briefs.py +27 -19
- package/skills/gsd-path-ship/scripts/detect_project.py +17 -2
- package/skills/gsd-path-ship/scripts/discussion_records.py +9 -1
- package/skills/gsd-path-ship/scripts/discussion_validate.py +7 -6
- package/skills/gsd-path-ship/scripts/integration.py +115 -68
- package/skills/gsd-path-ship/scripts/isolation.py +192 -23
- package/skills/gsd-path-ship/scripts/members.py +68 -25
- package/skills/gsd-path-ship/scripts/model_policy.py +13 -2
- package/skills/gsd-path-ship/scripts/pipeline_git.py +87 -35
- package/skills/gsd-path-ship/scripts/pipeline_state.py +31 -7
- package/skills/gsd-path-ship/scripts/promote_lookahead.py +3 -3
- package/skills/gsd-path-ship/scripts/roadmap.py +9 -1
- package/skills/gsd-path-ship/scripts/state_checkpoint.py +30 -9
- package/skills/gsd-path-ship/scripts/state_promote.py +92 -5
- package/skills/gsd-path-ship/scripts/workflow_run.py +9 -2
- package/skills/gsd-path-ship/templates/final-review.md +5 -5
- package/skills/gsd-path-ship/templates/wave-review.md +18 -9
- package/skills/gsd-path-undo/references/operating-rules.md +14 -5
- package/skills/gsd-path-undo/references/runtime-selection.md +10 -0
- package/skills/gsd-path-undo/scripts/_common.py +150 -1
- package/skills/gsd-path-undo/scripts/discussion_records.py +9 -1
- package/skills/gsd-path-undo/scripts/model_policy.py +13 -2
- package/skills/gsd-path-undo/scripts/pipeline_undo.py +17 -8
- package/skills/path/DEFINE.md +6 -2
- package/skills/path/PLAN.md +6 -2
- package/skills/path/ROADMAP.md +16 -6
- package/skills/path/SHIP.md +38 -11
- package/skills/path/SKILL.md +52 -24
- package/skills/path/references/build-native.md +96 -9
- package/skills/path/references/build-recovery.md +13 -0
- package/skills/path/references/coder.md +7 -3
- package/skills/path/references/model-policy.md +3 -1
- package/skills/path/references/operating-rules.md +14 -5
- package/skills/path/references/planner.md +13 -1
- package/skills/path/references/reviewer-final.md +5 -5
- package/skills/path/references/reviewer-wave.md +46 -13
- package/skills/path/references/runtime-selection.md +10 -0
- package/skills/path/scripts/_common.py +150 -1
- package/skills/path/scripts/build_recovery.py +62 -30
- package/skills/path/scripts/build_state.py +195 -5
- package/skills/path/scripts/check_handoffs.py +92 -30
- package/skills/path/scripts/check_task_briefs.py +27 -19
- package/skills/path/scripts/detect_project.py +17 -2
- package/skills/path/scripts/discussion_records.py +9 -1
- package/skills/path/scripts/discussion_validate.py +7 -6
- package/skills/path/scripts/dispatch_driver.py +174 -36
- package/skills/path/scripts/git_guard.py +42 -7
- package/skills/path/scripts/integration.py +115 -68
- package/skills/path/scripts/isolation.py +192 -23
- package/skills/path/scripts/members.py +68 -25
- package/skills/path/scripts/model_policy.py +13 -2
- package/skills/path/scripts/pipeline_git.py +87 -35
- package/skills/path/scripts/pipeline_state.py +31 -7
- package/skills/path/scripts/promote_lookahead.py +3 -3
- package/skills/path/scripts/review_findings.py +180 -44
- package/skills/path/scripts/roadmap.py +9 -1
- package/skills/path/scripts/state_checkpoint.py +30 -9
- package/skills/path/scripts/state_promote.py +92 -5
- package/skills/path/scripts/task_context.py +2 -0
- package/skills/path/scripts/workflow_run.py +9 -2
- package/skills/path/templates/final-review.md +5 -5
- package/skills/path/templates/state.md +1 -0
- package/skills/path/templates/task.md +1 -0
- package/skills/path/templates/wave-review.md +18 -9
|
@@ -94,6 +94,8 @@ class Task:
|
|
|
94
94
|
verify_heavy: bool
|
|
95
95
|
repo: str = ""
|
|
96
96
|
review_findings: bool = False
|
|
97
|
+
verify: str = ""
|
|
98
|
+
supersedes: Tuple[str, ...] = ()
|
|
97
99
|
|
|
98
100
|
|
|
99
101
|
@dataclass(frozen=True)
|
|
@@ -248,6 +250,8 @@ def _parse_task(path: Path, relative_path: str, task_file: Optional[str] = None)
|
|
|
248
250
|
verify_heavy=_verify_heavy(text, label),
|
|
249
251
|
repo=str(fields.get("repo") or ""),
|
|
250
252
|
review_findings=REVIEW_FINDINGS_HEADING in _sections(text),
|
|
253
|
+
verify=_common.task_verify_command(text),
|
|
254
|
+
supersedes=_normalize_list(fields.get("supersedes", []), f"{label} supersedes", task_ids=True),
|
|
251
255
|
)
|
|
252
256
|
|
|
253
257
|
|
|
@@ -420,7 +424,8 @@ def _recovery_reports(project: Project) -> Dict[str, Mapping[str, object]]:
|
|
|
420
424
|
return reports
|
|
421
425
|
|
|
422
426
|
|
|
423
|
-
def _validate_ready_metadata(project: Project) ->
|
|
427
|
+
def _validate_ready_metadata(project: Project) -> Dict[str, Mapping[str, object]]:
|
|
428
|
+
"""Validate every task against its recovery report; return the reports by task id."""
|
|
424
429
|
by_id = {task.task_id: task for task in project.tasks}
|
|
425
430
|
recovery = _recovery_reports(project)
|
|
426
431
|
seen_worktrees: Dict[str, str] = {}
|
|
@@ -484,12 +489,155 @@ def _validate_ready_metadata(project: Project) -> None:
|
|
|
484
489
|
if report.get("verdict") not in LANDED_VERDICTS:
|
|
485
490
|
reason = report.get("reason", "landing commit is not proven")
|
|
486
491
|
_invalid_state(task, str(reason))
|
|
492
|
+
return recovery
|
|
487
493
|
|
|
488
494
|
|
|
489
495
|
def _overlap(left: Task, right: Task) -> List[str]:
|
|
490
496
|
if left.repo != right.repo:
|
|
491
497
|
return []
|
|
492
|
-
return
|
|
498
|
+
return _common.overlapping_paths(left.files, right.files)
|
|
499
|
+
|
|
500
|
+
|
|
501
|
+
def _superseded(tasks: Sequence[Task], task: Task) -> Dict[str, str]:
|
|
502
|
+
"""Earlier task id -> the task that replaces its behavior: this task, or a `done` task."""
|
|
503
|
+
|
|
504
|
+
return {
|
|
505
|
+
earlier: other.task_id
|
|
506
|
+
for other in tasks
|
|
507
|
+
if other.status == "done" or other.task_id == task.task_id
|
|
508
|
+
for earlier in other.supersedes
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
|
|
512
|
+
def _held(by_id: Mapping[str, Task], task: Task) -> List[Task]:
|
|
513
|
+
"""The tasks whose protective role this task inherits: each task it supersedes, through a chain."""
|
|
514
|
+
|
|
515
|
+
held: Dict[str, Task] = {}
|
|
516
|
+
pending = list(task.supersedes)
|
|
517
|
+
while pending:
|
|
518
|
+
other = by_id.get(pending.pop())
|
|
519
|
+
if other is not None and other.task_id not in held: # a cycle ends here
|
|
520
|
+
held[other.task_id] = other
|
|
521
|
+
pending.extend(other.supersedes)
|
|
522
|
+
return [by_id[name] for name in sorted(held)]
|
|
523
|
+
|
|
524
|
+
|
|
525
|
+
def _live_verify(
|
|
526
|
+
by_id: Mapping[str, Task], superseded: Mapping[str, str], source: Task
|
|
527
|
+
) -> Tuple[str, Optional[str], List[Tuple[str, str]]]:
|
|
528
|
+
"""The Verify of a source that still runs, or the task that retired all of it, and the drops.
|
|
529
|
+
|
|
530
|
+
The Verify of a fix task is exactly the composition that fix-tasks generates from the
|
|
531
|
+
Verify of each task in its deps, so it carries a copy of each: only the copies that are
|
|
532
|
+
not retired stay in the command. Any other Verify is one command. Each drop is
|
|
533
|
+
(superseded task, superseding task); a copy whose command a live dependency still runs
|
|
534
|
+
is not a drop.
|
|
535
|
+
"""
|
|
536
|
+
|
|
537
|
+
if source.task_id in superseded:
|
|
538
|
+
return "", superseded[source.task_id], [(source.task_id, superseded[source.task_id])]
|
|
539
|
+
deps = [by_id[dependency] for dependency in source.deps if dependency in by_id]
|
|
540
|
+
if not deps or source.verify != _common.fix_verify(dep.verify for dep in deps):
|
|
541
|
+
return source.verify, None, []
|
|
542
|
+
parts = [_live_verify(by_id, superseded, dep) for dep in deps]
|
|
543
|
+
live = [command for command, retired_by, _ in parts if not retired_by]
|
|
544
|
+
dropped = list(dict.fromkeys(drop for _, _, drops in parts for drop in drops
|
|
545
|
+
if by_id[drop[0]].verify not in live))
|
|
546
|
+
if not live:
|
|
547
|
+
return "", parts[0][1], dropped
|
|
548
|
+
return _common.fix_verify(live), None, dropped
|
|
549
|
+
|
|
550
|
+
|
|
551
|
+
def _regression_sources(
|
|
552
|
+
tasks: Sequence[Task], task: Task
|
|
553
|
+
) -> List[Tuple[Task, List[str], Optional[str], str, List[Tuple[str, str]]]]:
|
|
554
|
+
"""Each `done` task whose Verify protects a file of this task:
|
|
555
|
+
(source, replaced, retired_by, command, dropped).
|
|
556
|
+
|
|
557
|
+
A source is a task of an earlier wave that shares a declared file with this task. A task
|
|
558
|
+
that supersedes others inherits their role: it is also a source, in any wave, for a task
|
|
559
|
+
that shares a file with a task it holds, and `replaced` names those tasks.
|
|
560
|
+
`retired_by` names the task that supersedes the source: this task, or a `done` task. A
|
|
561
|
+
retired Verify does not run, also not as a copy in the Verify of a fix task: `command`
|
|
562
|
+
is the Verify that runs and `dropped` names each retired copy (`_live_verify`). The source still counts for the one-at-a-time
|
|
563
|
+
rule, so the superseding task never runs beside a task that shares a file of the
|
|
564
|
+
superseded task.
|
|
565
|
+
"""
|
|
566
|
+
|
|
567
|
+
by_id = {other.task_id: other for other in tasks}
|
|
568
|
+
superseded = _superseded(tasks, task)
|
|
569
|
+
sources = []
|
|
570
|
+
for other in tasks:
|
|
571
|
+
if other.status != "done" or other.task_id == task.task_id:
|
|
572
|
+
continue
|
|
573
|
+
replaced = [held.task_id for held in _held(by_id, other) if _overlap(task, held)]
|
|
574
|
+
if replaced or (other.wave < task.wave and _overlap(task, other)):
|
|
575
|
+
command, retired_by, dropped = _live_verify(by_id, superseded, other)
|
|
576
|
+
sources.append((other, replaced, retired_by, command, dropped))
|
|
577
|
+
return sources
|
|
578
|
+
|
|
579
|
+
|
|
580
|
+
def _landing_tasks(repo: Path, task_id: str, project_dir: str) -> Tuple[List[Task], Task]:
|
|
581
|
+
tasks = [
|
|
582
|
+
_parse_task(path, path.relative_to(repo).as_posix())
|
|
583
|
+
for path in sorted((repo / project_dir / "tasks").glob("*.md"))
|
|
584
|
+
]
|
|
585
|
+
task = next((item for item in tasks if item.task_id == task_id), None)
|
|
586
|
+
if task is None:
|
|
587
|
+
raise BuildStateError("unknown-task", f"task file is missing: {task_id}")
|
|
588
|
+
return tasks, task
|
|
589
|
+
|
|
590
|
+
|
|
591
|
+
def retired_verifies(
|
|
592
|
+
repo: Path, task_id: str, project_dir: str = DEFAULT_PROJECT_DIR
|
|
593
|
+
) -> List[Dict[str, str]]:
|
|
594
|
+
"""Return the earlier-wave tasks whose Verify a landing of this task does not run again.
|
|
595
|
+
|
|
596
|
+
A task's plan-approved `supersedes` names them. The Verify is retired for that task, and
|
|
597
|
+
for every task that lands after that task is `done`: the Verify of the superseding task
|
|
598
|
+
then runs in its place (`regression_verifies`, key `supersedes`). A fix task whose
|
|
599
|
+
generated Verify holds only retired copies is here too. A fix task whose generated Verify
|
|
600
|
+
loses some of its copies has one entry for each, with the superseded task under `part`.
|
|
601
|
+
"""
|
|
602
|
+
|
|
603
|
+
tasks, task = _landing_tasks(repo, task_id, project_dir)
|
|
604
|
+
retired: List[Dict[str, str]] = []
|
|
605
|
+
for other, _, retired_by, _, dropped in _regression_sources(tasks, task):
|
|
606
|
+
if retired_by:
|
|
607
|
+
if other.verify:
|
|
608
|
+
retired.append({"task": other.task_id, "superseded_by": retired_by})
|
|
609
|
+
else:
|
|
610
|
+
retired.extend({"task": other.task_id, "part": part, "superseded_by": by}
|
|
611
|
+
for part, by in dropped)
|
|
612
|
+
return retired
|
|
613
|
+
|
|
614
|
+
|
|
615
|
+
def regression_verifies(
|
|
616
|
+
repo: Path, task_id: str, own: str, project_dir: str = DEFAULT_PROJECT_DIR
|
|
617
|
+
) -> List[Dict[str, object]]:
|
|
618
|
+
"""Return the Verify commands of landed tasks that a landing of this task must also pass.
|
|
619
|
+
|
|
620
|
+
One entry per distinct command, with every source task that has it, and under
|
|
621
|
+
`supersedes` the tasks whose retired Verify it replaces for this landing. A command
|
|
622
|
+
that the task's own Verify already runs is left out: an identical command, or
|
|
623
|
+
a dependency's command when the own Verify is exactly the fix-task Verify of
|
|
624
|
+
the task's dependencies. Text that only contains a command proves nothing.
|
|
625
|
+
A retired Verify is not here, also not as a copy in the Verify of a fix task: see
|
|
626
|
+
`retired_verifies`.
|
|
627
|
+
"""
|
|
628
|
+
|
|
629
|
+
tasks, task = _landing_tasks(repo, task_id, project_dir)
|
|
630
|
+
by_id = {item.task_id: item for item in tasks}
|
|
631
|
+
composed = [by_id[dependency].verify for dependency in task.deps if dependency in by_id]
|
|
632
|
+
covered = {own, *(composed if own == _common.fix_verify(composed) else ())}
|
|
633
|
+
sources: Dict[str, Dict[str, object]] = {}
|
|
634
|
+
for other, replaced, _, command, _ in _regression_sources(tasks, task):
|
|
635
|
+
if command and command not in covered:
|
|
636
|
+
entry = sources.setdefault(command, {"tasks": [], "command": command})
|
|
637
|
+
entry["tasks"].append(other.task_id)
|
|
638
|
+
if replaced: # this Verify runs in the place of the Verify of each task it replaced
|
|
639
|
+
entry["supersedes"] = sorted({*entry.get("supersedes", ()), *replaced})
|
|
640
|
+
return list(sources.values())
|
|
493
641
|
|
|
494
642
|
|
|
495
643
|
def _blocked_wave_reviews(project: Project) -> Dict[int, int]:
|
|
@@ -522,12 +670,31 @@ def _blocked_wave_reviews(project: Project) -> Dict[int, int]:
|
|
|
522
670
|
}
|
|
523
671
|
|
|
524
672
|
|
|
673
|
+
def _unreviewed_waves(project: Project, waves: Sequence[int]) -> List[int]:
|
|
674
|
+
"""Return the given waves that still need a canonical review file of their own.
|
|
675
|
+
|
|
676
|
+
A repair wave is left out: the next review cycle of its source wave judges it.
|
|
677
|
+
"""
|
|
678
|
+
|
|
679
|
+
review_dir = project.tasks_dir.parent / "review"
|
|
680
|
+
reviewed = {
|
|
681
|
+
wave for wave in waves
|
|
682
|
+
if all(task.review_findings for task in project.tasks if task.wave == wave)
|
|
683
|
+
}
|
|
684
|
+
if review_dir.is_dir() and not review_dir.is_symlink():
|
|
685
|
+
for path in review_dir.iterdir():
|
|
686
|
+
named = check_handoffs.WAVE_REVIEW_NAME.fullmatch(path.name)
|
|
687
|
+
if named is not None and path.is_file() and not path.is_symlink():
|
|
688
|
+
reviewed.add(int(named.group("wave")))
|
|
689
|
+
return [wave for wave in waves if wave not in reviewed]
|
|
690
|
+
|
|
691
|
+
|
|
525
692
|
def ready(repo: str, project_dir: str = DEFAULT_PROJECT_DIR, *,
|
|
526
693
|
allow_done: bool = False) -> Dict[str, object]:
|
|
527
694
|
"""Return task readiness, optionally accepting build/done for completion checks."""
|
|
528
695
|
|
|
529
696
|
project = _load_project(repo, project_dir, ("active", "done") if allow_done else ("active",))
|
|
530
|
-
_validate_ready_metadata(project)
|
|
697
|
+
reports = _validate_ready_metadata(project)
|
|
531
698
|
by_id = {task.task_id: task for task in project.tasks}
|
|
532
699
|
unfinished_waves = sorted({task.wave for task in project.tasks if task.status != "done"})
|
|
533
700
|
current_wave = unfinished_waves[0] if unfinished_waves else None
|
|
@@ -626,11 +793,31 @@ def ready(repo: str, project_dir: str = DEFAULT_PROJECT_DIR, *,
|
|
|
626
793
|
{"wave": current_wave, "tasks": stalled},
|
|
627
794
|
)
|
|
628
795
|
|
|
796
|
+
# Tasks that share a regression source land one at a time, each from the landing of the one
|
|
797
|
+
# before, so the source's Verify runs on their combined edits. A parallel round leaves the
|
|
798
|
+
# primary task file pending: a retained isolate shows that the task is already dispatched.
|
|
799
|
+
# A retired source still counts: its superseding task holds its role.
|
|
800
|
+
sources = {task.task_id: {other.task_id for other, _, _, _, _ in _regression_sources(project.tasks, task)}
|
|
801
|
+
for task in concurrent}
|
|
802
|
+
started = [task for task in selectable if reports[task.task_id].get("verdict") != "none"]
|
|
803
|
+
claimed = {source for task in active + started for source in sources[task.task_id]}
|
|
804
|
+
dispatchable = []
|
|
805
|
+
for task in selectable:
|
|
806
|
+
if task in started or not sources[task.task_id] & claimed:
|
|
807
|
+
dispatchable.append(task)
|
|
808
|
+
claimed |= sources[task.task_id]
|
|
809
|
+
|
|
629
810
|
typed: Dict[str, object] = {}
|
|
630
811
|
if repair_for_wave is not None:
|
|
631
812
|
typed["repair_for_wave"] = repair_for_wave
|
|
632
813
|
if blocked_review is not None:
|
|
633
814
|
typed["blocked_review"] = blocked_review
|
|
815
|
+
# Informational: every wave below the lowest unfinished wave is all done.
|
|
816
|
+
unreviewed = _unreviewed_waves(project, sorted(
|
|
817
|
+
{task.wave for task in project.tasks if unfinished_waves and task.wave < unfinished_waves[0]}
|
|
818
|
+
))
|
|
819
|
+
if unreviewed:
|
|
820
|
+
typed["unreviewed_waves"] = unreviewed
|
|
634
821
|
return {
|
|
635
822
|
"command": "ready",
|
|
636
823
|
"branch": project.branch,
|
|
@@ -646,10 +833,13 @@ def ready(repo: str, project_dir: str = DEFAULT_PROJECT_DIR, *,
|
|
|
646
833
|
"deps": list(task.deps),
|
|
647
834
|
"files": list(task.files),
|
|
648
835
|
"task_file": task.task_file,
|
|
649
|
-
|
|
836
|
+
# The landing also runs each regression Verify, so a heavy one makes the task heavy.
|
|
837
|
+
"verify_heavy": task.verify_heavy
|
|
838
|
+
or any(other.verify_heavy and not retired_by
|
|
839
|
+
for other, _, retired_by, _, _ in _regression_sources(project.tasks, task)),
|
|
650
840
|
**({"repo": task.repo} if task.repo else {}),
|
|
651
841
|
}
|
|
652
|
-
for task in
|
|
842
|
+
for task in dispatchable
|
|
653
843
|
],
|
|
654
844
|
}
|
|
655
845
|
|
|
@@ -161,7 +161,8 @@ def _reject_placeholder(value: str, label: str) -> None:
|
|
|
161
161
|
if placeholder is not None:
|
|
162
162
|
raise HandoffError(
|
|
163
163
|
f"{label} still contains the placeholder {placeholder.group(0)}; "
|
|
164
|
-
"replace it with the real value"
|
|
164
|
+
"replace it with the real value. A value cannot be only a placeholder; "
|
|
165
|
+
"literal text such as an HTML tag must be inside a sentence, in backticks"
|
|
165
166
|
)
|
|
166
167
|
|
|
167
168
|
|
|
@@ -667,22 +668,30 @@ def _strip_comments(text: str) -> str:
|
|
|
667
668
|
return COMMENT_PATTERN.sub("", text)
|
|
668
669
|
|
|
669
670
|
|
|
670
|
-
def
|
|
671
|
-
|
|
672
|
-
|
|
671
|
+
def _numbered_entries(section: str, *, continuations: bool = False) -> List[Tuple[int, str]]:
|
|
672
|
+
"""Each numbered item in document order; a repeated number stays a separate entry."""
|
|
673
|
+
entries: List[Tuple[int, str]] = []
|
|
674
|
+
open_item = False
|
|
673
675
|
for line in _strip_comments(section).splitlines():
|
|
674
676
|
match = NUMBERED_ITEM_PATTERN.fullmatch(line.strip())
|
|
675
677
|
if not match:
|
|
676
|
-
if continuations and
|
|
677
|
-
|
|
678
|
+
if continuations and open_item and line[:1].isspace() and line.strip():
|
|
679
|
+
number, text = entries[-1]
|
|
680
|
+
entries[-1] = (number, text + " " + line.strip())
|
|
678
681
|
elif line.strip():
|
|
679
|
-
|
|
682
|
+
open_item = False
|
|
680
683
|
continue
|
|
681
|
-
|
|
684
|
+
entries.append((int(match.group(1)), match.group(2).strip()))
|
|
685
|
+
open_item = True
|
|
686
|
+
return entries
|
|
687
|
+
|
|
688
|
+
|
|
689
|
+
def _numbered_items(section: str, *, continuations: bool = False) -> Dict[int, str]:
|
|
690
|
+
items: Dict[int, str] = {}
|
|
691
|
+
for number, text in _numbered_entries(section, continuations=continuations):
|
|
682
692
|
if number in items:
|
|
683
693
|
raise HandoffError(f"repeated numbered item {number}")
|
|
684
|
-
items[number] =
|
|
685
|
-
current = number
|
|
694
|
+
items[number] = text
|
|
686
695
|
return items
|
|
687
696
|
|
|
688
697
|
|
|
@@ -693,7 +702,11 @@ def _success_criteria(intent: str) -> Dict[str, str]:
|
|
|
693
702
|
expected = list(range(1, max(items) + 1))
|
|
694
703
|
if sorted(items) != expected:
|
|
695
704
|
raise HandoffError("INTENT.md success criteria must be contiguous from 1")
|
|
696
|
-
|
|
705
|
+
# Issue #392: a review can quote a criterion without the markers of its sub-bullet lines.
|
|
706
|
+
bare = _numbered_items(
|
|
707
|
+
_section(_common.without_sub_bullet_markers(intent), "Success criteria"), continuations=True
|
|
708
|
+
)
|
|
709
|
+
return {f"SC{number}": _common.Criterion(text, bare[number]) for number, text in items.items()}
|
|
697
710
|
|
|
698
711
|
|
|
699
712
|
def _split_top_level_commas(value: str) -> List[str]:
|
|
@@ -1297,18 +1310,22 @@ def _normalize_ws(value: str) -> str:
|
|
|
1297
1310
|
|
|
1298
1311
|
|
|
1299
1312
|
def task_review_observation(item: str, task_text: str) -> str:
|
|
1300
|
-
"""Separate
|
|
1313
|
+
"""Separate a quoted acceptance criterion from its observed evidence."""
|
|
1301
1314
|
item = _normalize_ws(item)
|
|
1302
1315
|
section = _common.section_body(task_text, "Acceptance criteria")
|
|
1303
1316
|
if section is not None:
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1317
|
+
# Issue #374: frozen landed bytes may repeat a criterion number. Only the
|
|
1318
|
+
# criterion text matters here, so each repeated entry is still split
|
|
1319
|
+
# from its evidence and the evidence gets the same checks.
|
|
1320
|
+
criteria = [criterion for _number, criterion in _numbered_entries(section, continuations=True)]
|
|
1321
|
+
quoted = _common.quoted_criterion(item, criteria)
|
|
1322
|
+
if quoted is not None:
|
|
1323
|
+
return item[quoted[1] + len(" — ") :]
|
|
1324
|
+
# Issue #392: a quote can leave out the `- ` markers of the criterion's
|
|
1325
|
+
# sub-bullet lines, so read the criteria again without them.
|
|
1326
|
+
bare = _common.without_sub_bullet_markers(task_text)
|
|
1327
|
+
return item if bare == task_text else task_review_observation(item, bare)
|
|
1328
|
+
|
|
1312
1329
|
|
|
1313
1330
|
|
|
1314
1331
|
def _verify_command(task_text: str) -> str:
|
|
@@ -1528,11 +1545,30 @@ def _validate_task_graph(
|
|
|
1528
1545
|
if _task_wave(tasks[dependency], dependency) > wave:
|
|
1529
1546
|
raise HandoffError(f"{task_id} depends on later-wave {dependency}")
|
|
1530
1547
|
|
|
1548
|
+
# `supersedes` retires the regression Verify of an earlier task: the same overlap rule selects both.
|
|
1549
|
+
for task_id, text in tasks.items():
|
|
1550
|
+
superseded = _strict_frontmatter(text, task_id).get("supersedes", [])
|
|
1551
|
+
if (
|
|
1552
|
+
not isinstance(superseded, list)
|
|
1553
|
+
or any(not re.fullmatch(r"T\d{3}", item) for item in superseded)
|
|
1554
|
+
or len(superseded) != len(set(superseded))
|
|
1555
|
+
):
|
|
1556
|
+
raise HandoffError(f"{task_id} supersedes must be a list of different task ids, for example [T001]")
|
|
1557
|
+
for earlier in superseded:
|
|
1558
|
+
if earlier not in tasks:
|
|
1559
|
+
raise HandoffError(f"{task_id} supersedes unknown task {earlier}")
|
|
1560
|
+
if task_waves[earlier] >= task_waves[task_id]:
|
|
1561
|
+
raise HandoffError(f"{task_id} supersedes {earlier}, which is not in an earlier wave")
|
|
1562
|
+
if task_repos[earlier] != task_repos[task_id]:
|
|
1563
|
+
raise HandoffError(f"{task_id} supersedes {earlier}, which is in another repo")
|
|
1564
|
+
if not _common.overlapping_paths(task_files[earlier], task_files[task_id]):
|
|
1565
|
+
raise HandoffError(f"{task_id} supersedes {earlier}, which shares no declared file with it")
|
|
1566
|
+
|
|
1531
1567
|
for index, left in enumerate(task_ids):
|
|
1532
1568
|
for right in task_ids[index + 1 :]:
|
|
1533
1569
|
if task_waves[left] != task_waves[right] or task_repos[left] != task_repos[right]:
|
|
1534
1570
|
continue
|
|
1535
|
-
overlap =
|
|
1571
|
+
overlap = _common.overlapping_paths(task_files[left], task_files[right])
|
|
1536
1572
|
if overlap:
|
|
1537
1573
|
if (
|
|
1538
1574
|
_task_scalar(tasks[left], left, "status") == "done"
|
|
@@ -1746,7 +1782,24 @@ def validate_roadmap(
|
|
|
1746
1782
|
) -> Dict[str, object]:
|
|
1747
1783
|
"""Validate roadmap structure and dependency ordering."""
|
|
1748
1784
|
|
|
1749
|
-
|
|
1785
|
+
# A milestone-boundary re-slice keeps its entering inspect or define state.
|
|
1786
|
+
_require_state_one_of(
|
|
1787
|
+
root,
|
|
1788
|
+
{("roadmap", "active"), ("inspect", "active"), ("define", "active")},
|
|
1789
|
+
project_dir,
|
|
1790
|
+
)
|
|
1791
|
+
if _require_pipeline(root, project_dir)["phase"] == "define":
|
|
1792
|
+
if __package__:
|
|
1793
|
+
from . import build_recovery
|
|
1794
|
+
else:
|
|
1795
|
+
import build_recovery
|
|
1796
|
+
try:
|
|
1797
|
+
# Define entered again after intent approval is not a boundary.
|
|
1798
|
+
reopened = (build_recovery.context(root.resolve()) or {}).get("active")
|
|
1799
|
+
except PipelineStateError as error:
|
|
1800
|
+
raise HandoffError(str(error)) from error
|
|
1801
|
+
if reopened:
|
|
1802
|
+
raise HandoffError("roadmap re-slice is not legal after intent approval")
|
|
1750
1803
|
relative = f"{project_dir}/ROADMAP.md"
|
|
1751
1804
|
text = _read(root, relative)
|
|
1752
1805
|
if re.search(r"(?m)^## Wave |^\|\s*Task\s*\||^Files:\s*", text):
|
|
@@ -2045,25 +2098,28 @@ def validate_wave_evidence(
|
|
|
2045
2098
|
if enforce_plan_tasks:
|
|
2046
2099
|
if reviewed_tasks != expected_tasks:
|
|
2047
2100
|
raise HandoffError(
|
|
2048
|
-
f"{name} must review exactly {', '.join(expected_tasks)} in
|
|
2101
|
+
f"{name} must review exactly {', '.join(expected_tasks)} in task id order"
|
|
2049
2102
|
)
|
|
2050
2103
|
else:
|
|
2051
2104
|
unknown = [task_id for task_id in reviewed_tasks if task_id not in expected_tasks]
|
|
2052
2105
|
if unknown:
|
|
2053
2106
|
raise HandoffError(
|
|
2054
|
-
f"{name} must review exactly {', '.join(expected_tasks)} in
|
|
2107
|
+
f"{name} must review exactly {', '.join(expected_tasks)} in task id order"
|
|
2055
2108
|
)
|
|
2056
2109
|
plan_order = [task_id for task_id in expected_tasks if task_id in reviewed_tasks]
|
|
2057
2110
|
if reviewed_tasks != plan_order:
|
|
2058
2111
|
raise HandoffError(
|
|
2059
|
-
f"{name} must review exactly {', '.join(expected_tasks)} in
|
|
2112
|
+
f"{name} must review exactly {', '.join(expected_tasks)} in task id order"
|
|
2060
2113
|
)
|
|
2061
2114
|
task_verdicts = []
|
|
2062
2115
|
for heading_index, heading in task_headings:
|
|
2063
2116
|
task_id = heading.group("task")
|
|
2064
2117
|
expected_title = _normalize_ws(_task_scalar(tasks[task_id], task_id, "title"))
|
|
2065
2118
|
if _normalize_ws(heading.group("title")) != expected_title:
|
|
2066
|
-
raise HandoffError(
|
|
2119
|
+
raise HandoffError(
|
|
2120
|
+
f"{name} title for {task_id} differs from task file; "
|
|
2121
|
+
f"expected the exact `title:` value: {expected_title}"
|
|
2122
|
+
)
|
|
2067
2123
|
end = next(
|
|
2068
2124
|
(
|
|
2069
2125
|
index
|
|
@@ -2080,7 +2136,10 @@ def validate_wave_evidence(
|
|
|
2080
2136
|
if line.strip().startswith(f"- {marker} ")
|
|
2081
2137
|
]
|
|
2082
2138
|
if not evidence:
|
|
2083
|
-
raise HandoffError(
|
|
2139
|
+
raise HandoffError(
|
|
2140
|
+
f"{name} task {task_id} lacks {verdict} evidence; "
|
|
2141
|
+
f"add a line that starts with `- {marker} ` under its heading"
|
|
2142
|
+
)
|
|
2084
2143
|
for item in evidence:
|
|
2085
2144
|
item = task_review_observation(item, tasks[task_id])
|
|
2086
2145
|
_non_placeholder(item, f"{name} task {task_id} {verdict} evidence")
|
|
@@ -2132,7 +2191,7 @@ def validate_wave_evidence(
|
|
|
2132
2191
|
sc_id in criteria
|
|
2133
2192
|
and (
|
|
2134
2193
|
not separator
|
|
2135
|
-
or
|
|
2194
|
+
or not _common.quotes_criterion(heading_text, criteria[sc_id])
|
|
2136
2195
|
)
|
|
2137
2196
|
):
|
|
2138
2197
|
raise HandoffError(f"{name} {sc_id} heading text differs from INTENT.md")
|
|
@@ -2150,7 +2209,10 @@ def validate_wave_evidence(
|
|
|
2150
2209
|
if line.strip().startswith(f"- {marker} ")
|
|
2151
2210
|
]
|
|
2152
2211
|
if not evidence:
|
|
2153
|
-
raise HandoffError(
|
|
2212
|
+
raise HandoffError(
|
|
2213
|
+
f"{name} {sc_id} lacks {verdict} evidence; "
|
|
2214
|
+
f"add a line that starts with `- {marker} ` under its heading"
|
|
2215
|
+
)
|
|
2154
2216
|
for item in evidence:
|
|
2155
2217
|
_non_placeholder(item, f"{name} {sc_id} {verdict} evidence")
|
|
2156
2218
|
verdicts[sc_id] = verdict
|
|
@@ -2230,7 +2292,7 @@ def validate_final(
|
|
|
2230
2292
|
verdicts: Dict[str, str] = {}
|
|
2231
2293
|
for index, heading in enumerate(blocks):
|
|
2232
2294
|
sc_id = heading.group("id")
|
|
2233
|
-
if
|
|
2295
|
+
if not _common.quotes_criterion(heading.group("title"), criteria[sc_id]):
|
|
2234
2296
|
raise HandoffError(f"FINAL.md {sc_id} heading text differs from INTENT.md")
|
|
2235
2297
|
end = blocks[index + 1].start() if index + 1 < len(blocks) else len(text)
|
|
2236
2298
|
block = text[heading.end() : end]
|
|
@@ -100,13 +100,15 @@ def _supplied_contract(repo: Path, task: Path, token: str) -> bool:
|
|
|
100
100
|
return path.is_file() and path.resolve() == repo.resolve() / token
|
|
101
101
|
|
|
102
102
|
|
|
103
|
+
def _pipeline_workspace_path(repo: Path, task: Path, token: str) -> bool:
|
|
104
|
+
track = _project_track(task, repo)
|
|
105
|
+
return track is not None and token.startswith(
|
|
106
|
+
tuple(f"{track}/{name}/" for name in PIPELINE_WORKSPACE_DIRS))
|
|
107
|
+
|
|
108
|
+
|
|
103
109
|
def _pipeline_workspace_file(repo: Path, task: Path, token: str) -> bool:
|
|
104
110
|
"""Pipeline-owned paths may exist only in the working tree before checkpoint."""
|
|
105
|
-
|
|
106
|
-
if track is None:
|
|
107
|
-
return False
|
|
108
|
-
prefixes = tuple(f"{track}/{name}/" for name in PIPELINE_WORKSPACE_DIRS)
|
|
109
|
-
if not any(token.startswith(prefix) for prefix in prefixes):
|
|
111
|
+
if not _pipeline_workspace_path(repo, task, token):
|
|
110
112
|
return False
|
|
111
113
|
path = repo / token
|
|
112
114
|
return path.is_file() and path.resolve() == repo.resolve() / token
|
|
@@ -277,7 +279,7 @@ def _verify_tokens(
|
|
|
277
279
|
|
|
278
280
|
|
|
279
281
|
def _member_bases(root: Path):
|
|
280
|
-
"""Resolve a member name to (checkout,
|
|
282
|
+
"""Resolve a member name to (checkout, base SHA), reading MEMBERS.md once."""
|
|
281
283
|
cache: Dict[str, Optional[Tuple[Path, str]]] = {}
|
|
282
284
|
|
|
283
285
|
def locate(name: str) -> Optional[Tuple[Path, str]]:
|
|
@@ -290,21 +292,22 @@ def _member_bases(root: Path):
|
|
|
290
292
|
except ImportError: # pragma: no cover - package import used by tests
|
|
291
293
|
from scripts import members
|
|
292
294
|
try:
|
|
293
|
-
listed = {member["name"]:
|
|
294
|
-
|
|
295
|
-
if checkout is None:
|
|
295
|
+
listed = {member["name"]: member for member in members.read_members(root)}
|
|
296
|
+
if name not in listed:
|
|
296
297
|
cache[name] = None
|
|
297
298
|
else:
|
|
299
|
+
checkout = Path(listed[name]["checkout"])
|
|
298
300
|
# A build lock selects the bound tip, including during Plan recovery;
|
|
299
|
-
# without the lock, use
|
|
301
|
+
# without the lock, use the member's recorded default branch on origin
|
|
302
|
+
# before the first build start.
|
|
300
303
|
lock = root / members.LOCK_PATH
|
|
301
304
|
locked = [entry for entry in (json.loads(lock.read_text(encoding="utf-8"))["members"]
|
|
302
305
|
if lock.is_file() else []) if entry["name"] == name]
|
|
303
306
|
if locked:
|
|
304
307
|
cache[name] = (checkout, _resolve_base(checkout, f"refs/heads/{locked[0]['branch']}"))
|
|
305
308
|
else:
|
|
306
|
-
members.
|
|
307
|
-
|
|
309
|
+
cache[name] = (checkout, members.require_origin_branch(
|
|
310
|
+
checkout, members.default_branch(listed[name])))
|
|
308
311
|
except (members.MembersError, BriefError) as error:
|
|
309
312
|
raise BriefError(f"member {name} is unavailable: {error}") from error
|
|
310
313
|
return cache[name]
|
|
@@ -328,8 +331,8 @@ def _lint_task(
|
|
|
328
331
|
checked = 0
|
|
329
332
|
if fields is None:
|
|
330
333
|
return task_id, [error or "invalid frontmatter"], None, checked
|
|
331
|
-
# A member task's paths resolve in the member at its origin
|
|
332
|
-
# member bound branch starts; `repo:` absent means the coordinator.
|
|
334
|
+
# A member task's paths resolve in the member at its default branch on origin,
|
|
335
|
+
# where the member bound branch starts; `repo:` absent means the coordinator.
|
|
333
336
|
git_root = repo
|
|
334
337
|
member = fields.get("repo")
|
|
335
338
|
if member is not None:
|
|
@@ -384,7 +387,9 @@ def _lint_task(
|
|
|
384
387
|
)
|
|
385
388
|
break
|
|
386
389
|
|
|
387
|
-
|
|
390
|
+
# A landed brief is history that nobody can edit: only an unlanded brief must name present paths.
|
|
391
|
+
landed = task_id in (landed_bases or {})
|
|
392
|
+
for name in () if landed else PROSE_SECTIONS:
|
|
388
393
|
body = sections.get(name)
|
|
389
394
|
if body is None or not _clean(body):
|
|
390
395
|
continue
|
|
@@ -406,7 +411,7 @@ def _lint_task(
|
|
|
406
411
|
block = VERIFY_BLOCK_PATTERN.search(verify_body)
|
|
407
412
|
if block is None or not block.group("block").strip():
|
|
408
413
|
problems.append("## Verify must contain a non-empty fenced bash block")
|
|
409
|
-
|
|
414
|
+
elif not landed:
|
|
410
415
|
for token in _verify_tokens(
|
|
411
416
|
block.group("block"),
|
|
412
417
|
repo=repo,
|
|
@@ -443,9 +448,10 @@ def validate_task_briefs(
|
|
|
443
448
|
|
|
444
449
|
Plain lint uses the layer base without overrides. Plan approval and dispatch
|
|
445
450
|
supply transitive dependency files for unlanded tasks and resolved historical
|
|
446
|
-
bases for landed tasks
|
|
451
|
+
bases for landed tasks. A landed brief is immutable: its `files` scope is
|
|
452
|
+
checked at that base, and the paths its prose and Verify name are not checked.
|
|
447
453
|
The caller validates the task graph and landed metadata before supplying
|
|
448
|
-
these mappings
|
|
454
|
+
these mappings.
|
|
449
455
|
"""
|
|
450
456
|
|
|
451
457
|
resolved_base = _resolve_base(root, base)
|
|
@@ -476,6 +482,9 @@ def validate_task_briefs(
|
|
|
476
482
|
for shape, task_ids in contracts.items():
|
|
477
483
|
if len(task_ids) == 1:
|
|
478
484
|
task_id = next(iter(task_ids))
|
|
485
|
+
# A landed brief is history: a later repair may reword the bullet in its partner.
|
|
486
|
+
if task_id in (landed_bases or {}):
|
|
487
|
+
continue
|
|
479
488
|
problems.append(
|
|
480
489
|
f"{task_id}: interface contract is not shared by any other task: {shape}"
|
|
481
490
|
)
|
|
@@ -587,7 +596,6 @@ def plan_validation_bases(
|
|
|
587
596
|
)
|
|
588
597
|
except IsolationError as error:
|
|
589
598
|
raise BriefError(f"{task_id} landed task has invalid historical base: {error}") from error
|
|
590
|
-
dependency_files[task_id] = set()
|
|
591
599
|
except check_handoffs.HandoffError as error:
|
|
592
600
|
raise BriefError(str(error)) from error
|
|
593
601
|
return tasks, dependency_files, landed_bases
|
|
@@ -1291,10 +1291,17 @@ def project_slug(root: Path) -> str:
|
|
|
1291
1291
|
return slug or "project"
|
|
1292
1292
|
|
|
1293
1293
|
|
|
1294
|
-
def filled_state_template(
|
|
1294
|
+
def filled_state_template(
|
|
1295
|
+
template: str, slug: str, phase: str, default_branch: str = "main"
|
|
1296
|
+
) -> str:
|
|
1295
1297
|
if "<slug>" not in template:
|
|
1296
1298
|
raise DetectError("state template missing slug placeholder")
|
|
1297
1299
|
text = template.replace("<slug>", slug)
|
|
1300
|
+
text, count = re.subn(
|
|
1301
|
+
r"(?m)^default_branch: main\b", lambda _: f"default_branch: {default_branch}", text, count=1
|
|
1302
|
+
)
|
|
1303
|
+
if count != 1:
|
|
1304
|
+
raise DetectError("state template missing default_branch: main line")
|
|
1298
1305
|
text, count = re.subn(r"(?m)^phase: define\b", f"phase: {phase}", text, count=1)
|
|
1299
1306
|
if count != 1:
|
|
1300
1307
|
raise DetectError("state template missing phase: define line")
|
|
@@ -2030,6 +2037,14 @@ def initialize(
|
|
|
2030
2037
|
)
|
|
2031
2038
|
if git.returncode != 0 or git.stdout.strip() != "true":
|
|
2032
2039
|
return {**payload, "route": "setup-repository", "wrote_state": False}
|
|
2040
|
+
default_branch = _common.origin_default_branch(root)
|
|
2041
|
+
if not _common.valid_default_branch(default_branch):
|
|
2042
|
+
return {**payload, "wrote_state": False, "error": (
|
|
2043
|
+
f"origin default branch {default_branch!r} cannot be recorded in STATE.md: "
|
|
2044
|
+
"it is a GSD Path reserved name or it has a character that STATE frontmatter "
|
|
2045
|
+
"cannot hold. Rename the default branch, then run "
|
|
2046
|
+
"`git remote set-head origin --auto`."
|
|
2047
|
+
)}
|
|
2033
2048
|
expected = "inspect" if payload["verdict"] == "brownfield" else "define"
|
|
2034
2049
|
if phase is None:
|
|
2035
2050
|
phase = expected
|
|
@@ -2043,7 +2058,7 @@ def initialize(
|
|
|
2043
2058
|
return payload
|
|
2044
2059
|
write_state_anchored(
|
|
2045
2060
|
root,
|
|
2046
|
-
filled_state_template(raw, project_slug(root), phase),
|
|
2061
|
+
filled_state_template(raw, project_slug(root), phase, default_branch),
|
|
2047
2062
|
root_status,
|
|
2048
2063
|
project_status,
|
|
2049
2064
|
)
|