@opengsd/gsd-path 1.4.0 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +325 -344
- package/DOCS.md +12 -2
- package/FULL.md +3 -2
- package/HOOKS.md +95 -8
- package/QUICK.md +1 -1
- package/README.md +78 -110
- package/RUNTIME.md +1 -1
- package/UPDATE.md +4 -3
- package/WORKFLOW.md +22 -1
- package/package.json +5 -2
- package/platforms/shared-agents/dispatch.md +13 -0
- package/scripts/_common.py +149 -1
- package/scripts/archive_milestone.py +14 -13
- package/scripts/build_recovery.py +96 -31
- package/scripts/build_state.py +79 -3
- package/scripts/check_docs_audit.py +77 -4
- package/scripts/check_handoffs.py +546 -30
- package/scripts/check_task_briefs.py +279 -23
- package/scripts/detect_project.py +243 -18
- package/scripts/discussion_validate.py +86 -29
- package/scripts/dispatch_driver.py +392 -22
- package/scripts/git_guard.py +3 -3
- package/scripts/guard_hook.py +1416 -165
- package/scripts/install.mjs +23 -8
- package/scripts/install.py +49 -9
- package/scripts/integration.py +146 -1
- package/scripts/isolation.py +392 -60
- package/scripts/loop_run.py +16 -4
- package/scripts/members.py +11 -9
- package/scripts/path_config.py +3 -3
- package/scripts/pipeline_git.py +63 -12
- package/scripts/pipeline_state.py +111 -33
- package/scripts/pipeline_undo.py +101 -8
- package/scripts/runtime_store.py +20 -2
- package/scripts/skill-resources.json +9 -2
- package/scripts/state_checkpoint.py +9 -27
- package/scripts/status_runtime.py +22 -4
- package/scripts/task_context.py +12 -3
- package/scripts/wizard.mjs +1 -0
- package/scripts/workflow_run.py +5 -2
- package/scripts/worktree_paths.py +54 -2
- package/skills/gsd-path/DEFINE.md +28 -3
- package/skills/gsd-path/PLAN.md +11 -3
- package/skills/gsd-path/RESEARCH.md +11 -0
- package/skills/gsd-path/SHIP.md +6 -1
- package/skills/gsd-path/references/build-native.md +16 -1
- package/skills/gsd-path/references/dispatch.md +13 -0
- package/skills/gsd-path/references/docs-auditor.md +5 -0
- package/skills/gsd-path/references/planner.md +14 -4
- package/skills/gsd-path/references/reviewer-final.md +20 -2
- package/skills/gsd-path/references/reviewer-wave.md +15 -4
- package/skills/gsd-path/references/reviewer.md +3 -1
- package/skills/gsd-path/references/spec-probes.md +150 -0
- package/skills/gsd-path/scripts/_common.py +149 -1
- package/skills/gsd-path/scripts/archive_milestone.py +14 -13
- package/skills/gsd-path/scripts/build_recovery.py +96 -31
- package/skills/gsd-path/scripts/build_state.py +79 -3
- package/skills/gsd-path/scripts/check_docs_audit.py +77 -4
- package/skills/gsd-path/scripts/check_handoffs.py +546 -30
- package/skills/gsd-path/scripts/check_task_briefs.py +279 -23
- package/skills/gsd-path/scripts/detect_project.py +243 -18
- package/skills/gsd-path/scripts/discussion_validate.py +86 -29
- package/skills/gsd-path/scripts/dispatch_driver.py +392 -22
- package/skills/gsd-path/scripts/git_guard.py +3 -3
- package/skills/gsd-path/scripts/integration.py +146 -1
- package/skills/gsd-path/scripts/isolation.py +392 -60
- package/skills/gsd-path/scripts/loop_run.py +16 -4
- package/skills/gsd-path/scripts/members.py +11 -9
- package/skills/gsd-path/scripts/path_config.py +3 -3
- package/skills/gsd-path/scripts/pipeline_git.py +63 -12
- package/skills/gsd-path/scripts/pipeline_state.py +111 -33
- package/skills/gsd-path/scripts/state_checkpoint.py +9 -27
- package/skills/gsd-path/scripts/status_runtime.py +22 -4
- package/skills/gsd-path/scripts/task_context.py +12 -3
- package/skills/gsd-path/scripts/workflow_run.py +5 -2
- package/skills/gsd-path/scripts/worktree_paths.py +54 -2
- package/skills/gsd-path/templates/docs-audit.md +5 -1
- package/skills/gsd-path/templates/final-review.md +11 -2
- package/skills/gsd-path/templates/intent.md +30 -0
- package/skills/gsd-path/templates/patch-findings.md +3 -3
- package/skills/gsd-path/templates/plan.md +17 -2
- package/skills/gsd-path/templates/research-handoff.md +14 -3
- package/skills/gsd-path/templates/state.md +1 -1
- package/skills/gsd-path/templates/wave-review.md +16 -6
- package/skills/gsd-path-build/references/build-native.md +16 -1
- package/skills/gsd-path-build/references/dispatch.md +13 -0
- package/skills/gsd-path-build/references/reviewer-final.md +20 -2
- package/skills/gsd-path-build/references/reviewer-wave.md +15 -4
- package/skills/gsd-path-build/references/reviewer.md +3 -1
- package/skills/gsd-path-build/references/spec-probes.md +150 -0
- package/skills/gsd-path-build/scripts/_common.py +149 -1
- package/skills/gsd-path-build/scripts/archive_milestone.py +14 -13
- package/skills/gsd-path-build/scripts/build_recovery.py +96 -31
- package/skills/gsd-path-build/scripts/build_state.py +79 -3
- package/skills/gsd-path-build/scripts/check_handoffs.py +546 -30
- package/skills/gsd-path-build/scripts/check_task_briefs.py +279 -23
- package/skills/gsd-path-build/scripts/detect_project.py +243 -18
- package/skills/gsd-path-build/scripts/discussion_validate.py +86 -29
- package/skills/gsd-path-build/scripts/dispatch_driver.py +392 -22
- package/skills/gsd-path-build/scripts/integration.py +146 -1
- package/skills/gsd-path-build/scripts/isolation.py +392 -60
- package/skills/gsd-path-build/scripts/loop_run.py +16 -4
- package/skills/gsd-path-build/scripts/members.py +11 -9
- package/skills/gsd-path-build/scripts/path_config.py +3 -3
- package/skills/gsd-path-build/scripts/pipeline_git.py +63 -12
- package/skills/gsd-path-build/scripts/pipeline_state.py +111 -33
- package/skills/gsd-path-build/scripts/state_checkpoint.py +9 -27
- package/skills/gsd-path-build/scripts/task_context.py +12 -3
- package/skills/gsd-path-build/scripts/workflow_run.py +5 -2
- package/skills/gsd-path-build/scripts/worktree_paths.py +54 -2
- package/skills/gsd-path-build/templates/wave-review.md +16 -6
- package/skills/gsd-path-decide/references/dispatch.md +13 -0
- package/skills/gsd-path-decide/scripts/_common.py +149 -1
- package/skills/gsd-path-decide/scripts/archive_milestone.py +14 -13
- package/skills/gsd-path-decide/scripts/build_recovery.py +96 -31
- package/skills/gsd-path-decide/scripts/check_handoffs.py +546 -30
- package/skills/gsd-path-decide/scripts/check_task_briefs.py +279 -23
- package/skills/gsd-path-decide/scripts/detect_project.py +243 -18
- package/skills/gsd-path-decide/scripts/discussion_validate.py +86 -29
- package/skills/gsd-path-decide/scripts/integration.py +146 -1
- package/skills/gsd-path-decide/scripts/isolation.py +392 -60
- package/skills/gsd-path-decide/scripts/loop_run.py +16 -4
- package/skills/gsd-path-decide/scripts/members.py +11 -9
- package/skills/gsd-path-decide/scripts/path_config.py +3 -3
- package/skills/gsd-path-decide/scripts/pipeline_git.py +63 -12
- package/skills/gsd-path-decide/scripts/pipeline_state.py +111 -33
- package/skills/gsd-path-decide/scripts/state_checkpoint.py +9 -27
- package/skills/gsd-path-decide/scripts/worktree_paths.py +54 -2
- package/skills/gsd-path-define/SKILL.md +28 -3
- package/skills/gsd-path-define/references/spec-probes.md +150 -0
- package/skills/gsd-path-define/scripts/_common.py +149 -1
- package/skills/gsd-path-define/scripts/archive_milestone.py +14 -13
- package/skills/gsd-path-define/scripts/build_recovery.py +96 -31
- package/skills/gsd-path-define/scripts/check_handoffs.py +546 -30
- package/skills/gsd-path-define/scripts/check_task_briefs.py +279 -23
- package/skills/gsd-path-define/scripts/detect_project.py +243 -18
- package/skills/gsd-path-define/scripts/discussion_validate.py +86 -29
- package/skills/gsd-path-define/scripts/integration.py +146 -1
- package/skills/gsd-path-define/scripts/isolation.py +392 -60
- package/skills/gsd-path-define/scripts/loop_run.py +16 -4
- package/skills/gsd-path-define/scripts/members.py +11 -9
- package/skills/gsd-path-define/scripts/path_config.py +3 -3
- package/skills/gsd-path-define/scripts/pipeline_git.py +63 -12
- package/skills/gsd-path-define/scripts/pipeline_state.py +111 -33
- package/skills/gsd-path-define/scripts/state_checkpoint.py +9 -27
- package/skills/gsd-path-define/scripts/worktree_paths.py +54 -2
- package/skills/gsd-path-define/templates/intent.md +30 -0
- package/skills/gsd-path-define/templates/state.md +1 -1
- package/skills/gsd-path-discuss/scripts/_common.py +149 -1
- package/skills/gsd-path-discuss/scripts/loop_run.py +16 -4
- package/skills/gsd-path-discuss/scripts/path_config.py +3 -3
- package/skills/gsd-path-discuss/scripts/worktree_paths.py +54 -2
- package/skills/gsd-path-docs-audit/references/dispatch.md +13 -0
- package/skills/gsd-path-docs-audit/references/docs-auditor.md +5 -0
- package/skills/gsd-path-docs-audit/scripts/_common.py +149 -1
- package/skills/gsd-path-docs-audit/scripts/archive_milestone.py +14 -13
- package/skills/gsd-path-docs-audit/scripts/build_recovery.py +96 -31
- package/skills/gsd-path-docs-audit/scripts/check_docs_audit.py +77 -4
- package/skills/gsd-path-docs-audit/scripts/check_handoffs.py +546 -30
- package/skills/gsd-path-docs-audit/scripts/check_task_briefs.py +279 -23
- package/skills/gsd-path-docs-audit/scripts/detect_project.py +243 -18
- package/skills/gsd-path-docs-audit/scripts/discussion_validate.py +86 -29
- package/skills/gsd-path-docs-audit/scripts/integration.py +146 -1
- package/skills/gsd-path-docs-audit/scripts/isolation.py +392 -60
- package/skills/gsd-path-docs-audit/scripts/loop_run.py +16 -4
- package/skills/gsd-path-docs-audit/scripts/members.py +11 -9
- package/skills/gsd-path-docs-audit/scripts/path_config.py +3 -3
- package/skills/gsd-path-docs-audit/scripts/pipeline_git.py +63 -12
- package/skills/gsd-path-docs-audit/scripts/pipeline_state.py +111 -33
- package/skills/gsd-path-docs-audit/scripts/state_checkpoint.py +9 -27
- package/skills/gsd-path-docs-audit/scripts/worktree_paths.py +54 -2
- package/skills/gsd-path-docs-audit/templates/docs-audit.md +5 -1
- package/skills/gsd-path-forensics/scripts/_common.py +149 -1
- package/skills/gsd-path-forensics/scripts/loop_run.py +16 -4
- package/skills/gsd-path-forensics/scripts/path_config.py +3 -3
- package/skills/gsd-path-forensics/scripts/pipeline_undo.py +101 -8
- package/skills/gsd-path-forensics/scripts/worktree_paths.py +54 -2
- package/skills/gsd-path-inspect/references/dispatch.md +13 -0
- package/skills/gsd-path-inspect/references/docs-auditor.md +5 -0
- package/skills/gsd-path-inspect/scripts/_common.py +149 -1
- package/skills/gsd-path-inspect/scripts/archive_milestone.py +14 -13
- package/skills/gsd-path-inspect/scripts/build_recovery.py +96 -31
- package/skills/gsd-path-inspect/scripts/check_docs_audit.py +77 -4
- package/skills/gsd-path-inspect/scripts/check_handoffs.py +546 -30
- package/skills/gsd-path-inspect/scripts/check_task_briefs.py +279 -23
- package/skills/gsd-path-inspect/scripts/detect_project.py +243 -18
- package/skills/gsd-path-inspect/scripts/discussion_validate.py +86 -29
- package/skills/gsd-path-inspect/scripts/integration.py +146 -1
- package/skills/gsd-path-inspect/scripts/isolation.py +392 -60
- package/skills/gsd-path-inspect/scripts/loop_run.py +16 -4
- package/skills/gsd-path-inspect/scripts/members.py +11 -9
- package/skills/gsd-path-inspect/scripts/path_config.py +3 -3
- package/skills/gsd-path-inspect/scripts/pipeline_git.py +63 -12
- package/skills/gsd-path-inspect/scripts/pipeline_state.py +111 -33
- package/skills/gsd-path-inspect/scripts/state_checkpoint.py +9 -27
- package/skills/gsd-path-inspect/scripts/workflow_run.py +5 -2
- package/skills/gsd-path-inspect/scripts/worktree_paths.py +54 -2
- package/skills/gsd-path-inspect/templates/docs-audit.md +5 -1
- package/skills/gsd-path-inspect/templates/state.md +1 -1
- package/skills/gsd-path-loop/scripts/_common.py +149 -1
- package/skills/gsd-path-loop/scripts/loop_run.py +16 -4
- package/skills/gsd-path-loop/scripts/path_config.py +3 -3
- package/skills/gsd-path-loop/scripts/worktree_paths.py +54 -2
- package/skills/gsd-path-migrate/scripts/_common.py +149 -1
- package/skills/gsd-path-migrate/scripts/loop_run.py +16 -4
- package/skills/gsd-path-migrate/scripts/path_config.py +3 -3
- package/skills/gsd-path-plan/SKILL.md +11 -3
- package/skills/gsd-path-plan/references/dispatch.md +13 -0
- package/skills/gsd-path-plan/references/planner.md +14 -4
- package/skills/gsd-path-plan/references/spec-probes.md +150 -0
- package/skills/gsd-path-plan/scripts/_common.py +149 -1
- package/skills/gsd-path-plan/scripts/archive_milestone.py +14 -13
- package/skills/gsd-path-plan/scripts/build_recovery.py +96 -31
- package/skills/gsd-path-plan/scripts/check_handoffs.py +546 -30
- package/skills/gsd-path-plan/scripts/check_task_briefs.py +279 -23
- package/skills/gsd-path-plan/scripts/detect_project.py +243 -18
- package/skills/gsd-path-plan/scripts/discussion_validate.py +86 -29
- package/skills/gsd-path-plan/scripts/integration.py +146 -1
- package/skills/gsd-path-plan/scripts/isolation.py +392 -60
- package/skills/gsd-path-plan/scripts/loop_run.py +16 -4
- package/skills/gsd-path-plan/scripts/members.py +11 -9
- package/skills/gsd-path-plan/scripts/path_config.py +3 -3
- package/skills/gsd-path-plan/scripts/pipeline_git.py +63 -12
- package/skills/gsd-path-plan/scripts/pipeline_state.py +111 -33
- package/skills/gsd-path-plan/scripts/state_checkpoint.py +9 -27
- package/skills/gsd-path-plan/scripts/workflow_run.py +5 -2
- package/skills/gsd-path-plan/scripts/worktree_paths.py +54 -2
- package/skills/gsd-path-plan/templates/patch-findings.md +3 -3
- package/skills/gsd-path-plan/templates/plan.md +17 -2
- package/skills/gsd-path-research/SKILL.md +11 -0
- package/skills/gsd-path-research/references/dispatch.md +13 -0
- package/skills/gsd-path-research/scripts/_common.py +149 -1
- package/skills/gsd-path-research/scripts/archive_milestone.py +14 -13
- package/skills/gsd-path-research/scripts/build_recovery.py +96 -31
- package/skills/gsd-path-research/scripts/check_handoffs.py +546 -30
- package/skills/gsd-path-research/scripts/check_task_briefs.py +279 -23
- package/skills/gsd-path-research/scripts/detect_project.py +243 -18
- package/skills/gsd-path-research/scripts/discussion_validate.py +86 -29
- package/skills/gsd-path-research/scripts/integration.py +146 -1
- package/skills/gsd-path-research/scripts/isolation.py +392 -60
- package/skills/gsd-path-research/scripts/loop_run.py +16 -4
- package/skills/gsd-path-research/scripts/members.py +11 -9
- package/skills/gsd-path-research/scripts/path_config.py +3 -3
- package/skills/gsd-path-research/scripts/pipeline_git.py +63 -12
- package/skills/gsd-path-research/scripts/pipeline_state.py +111 -33
- package/skills/gsd-path-research/scripts/state_checkpoint.py +9 -27
- package/skills/gsd-path-research/scripts/worktree_paths.py +54 -2
- package/skills/gsd-path-research/templates/research-handoff.md +14 -3
- package/skills/gsd-path-roadmap/references/dispatch.md +13 -0
- package/skills/gsd-path-roadmap/scripts/_common.py +149 -1
- package/skills/gsd-path-roadmap/scripts/archive_milestone.py +14 -13
- package/skills/gsd-path-roadmap/scripts/build_recovery.py +96 -31
- package/skills/gsd-path-roadmap/scripts/check_handoffs.py +546 -30
- package/skills/gsd-path-roadmap/scripts/check_task_briefs.py +279 -23
- package/skills/gsd-path-roadmap/scripts/detect_project.py +243 -18
- package/skills/gsd-path-roadmap/scripts/discussion_validate.py +86 -29
- package/skills/gsd-path-roadmap/scripts/integration.py +146 -1
- package/skills/gsd-path-roadmap/scripts/isolation.py +392 -60
- package/skills/gsd-path-roadmap/scripts/loop_run.py +16 -4
- package/skills/gsd-path-roadmap/scripts/members.py +11 -9
- package/skills/gsd-path-roadmap/scripts/path_config.py +3 -3
- package/skills/gsd-path-roadmap/scripts/pipeline_git.py +63 -12
- package/skills/gsd-path-roadmap/scripts/pipeline_state.py +111 -33
- package/skills/gsd-path-roadmap/scripts/state_checkpoint.py +9 -27
- package/skills/gsd-path-roadmap/scripts/worktree_paths.py +54 -2
- package/skills/gsd-path-ship/SKILL.md +6 -1
- package/skills/gsd-path-ship/references/dispatch.md +13 -0
- package/skills/gsd-path-ship/references/reviewer-final.md +20 -2
- package/skills/gsd-path-ship/references/reviewer-wave.md +15 -4
- package/skills/gsd-path-ship/references/reviewer.md +3 -1
- package/skills/gsd-path-ship/references/spec-probes.md +150 -0
- package/skills/gsd-path-ship/scripts/_common.py +149 -1
- package/skills/gsd-path-ship/scripts/archive_milestone.py +14 -13
- package/skills/gsd-path-ship/scripts/build_recovery.py +96 -31
- package/skills/gsd-path-ship/scripts/build_state.py +79 -3
- package/skills/gsd-path-ship/scripts/check_handoffs.py +546 -30
- package/skills/gsd-path-ship/scripts/check_task_briefs.py +279 -23
- package/skills/gsd-path-ship/scripts/detect_project.py +243 -18
- package/skills/gsd-path-ship/scripts/discussion_validate.py +86 -29
- package/skills/gsd-path-ship/scripts/integration.py +146 -1
- package/skills/gsd-path-ship/scripts/isolation.py +392 -60
- package/skills/gsd-path-ship/scripts/loop_run.py +16 -4
- package/skills/gsd-path-ship/scripts/members.py +11 -9
- package/skills/gsd-path-ship/scripts/path_config.py +3 -3
- package/skills/gsd-path-ship/scripts/pipeline_git.py +63 -12
- package/skills/gsd-path-ship/scripts/pipeline_state.py +111 -33
- package/skills/gsd-path-ship/scripts/state_checkpoint.py +9 -27
- package/skills/gsd-path-ship/scripts/workflow_run.py +5 -2
- package/skills/gsd-path-ship/scripts/worktree_paths.py +54 -2
- package/skills/gsd-path-ship/templates/final-review.md +11 -2
- package/skills/gsd-path-ship/templates/patch-findings.md +3 -3
- package/skills/gsd-path-ship/templates/wave-review.md +16 -6
- package/skills/gsd-path-undo/scripts/_common.py +149 -1
- package/skills/gsd-path-undo/scripts/loop_run.py +16 -4
- package/skills/gsd-path-undo/scripts/path_config.py +3 -3
- package/skills/gsd-path-undo/scripts/pipeline_undo.py +101 -8
- package/skills/gsd-path-undo/scripts/worktree_paths.py +54 -2
- package/skills/path/DEFINE.md +28 -3
- package/skills/path/PLAN.md +11 -3
- package/skills/path/RESEARCH.md +11 -0
- package/skills/path/SHIP.md +6 -1
- package/skills/path/references/build-native.md +16 -1
- package/skills/path/references/dispatch.md +13 -0
- package/skills/path/references/docs-auditor.md +5 -0
- package/skills/path/references/planner.md +14 -4
- package/skills/path/references/reviewer-final.md +20 -2
- package/skills/path/references/reviewer-wave.md +15 -4
- package/skills/path/references/reviewer.md +3 -1
- package/skills/path/references/spec-probes.md +150 -0
- package/skills/path/scripts/_common.py +149 -1
- package/skills/path/scripts/archive_milestone.py +14 -13
- package/skills/path/scripts/build_recovery.py +96 -31
- package/skills/path/scripts/build_state.py +79 -3
- package/skills/path/scripts/check_docs_audit.py +77 -4
- package/skills/path/scripts/check_handoffs.py +546 -30
- package/skills/path/scripts/check_task_briefs.py +279 -23
- package/skills/path/scripts/detect_project.py +243 -18
- package/skills/path/scripts/discussion_validate.py +86 -29
- package/skills/path/scripts/dispatch_driver.py +392 -22
- package/skills/path/scripts/git_guard.py +3 -3
- package/skills/path/scripts/integration.py +146 -1
- package/skills/path/scripts/isolation.py +392 -60
- package/skills/path/scripts/loop_run.py +16 -4
- package/skills/path/scripts/members.py +11 -9
- package/skills/path/scripts/path_config.py +3 -3
- package/skills/path/scripts/pipeline_git.py +63 -12
- package/skills/path/scripts/pipeline_state.py +111 -33
- package/skills/path/scripts/state_checkpoint.py +9 -27
- package/skills/path/scripts/status_runtime.py +22 -4
- package/skills/path/scripts/task_context.py +12 -3
- package/skills/path/scripts/workflow_run.py +5 -2
- package/skills/path/scripts/worktree_paths.py +54 -2
- package/skills/path/templates/docs-audit.md +5 -1
- package/skills/path/templates/final-review.md +11 -2
- package/skills/path/templates/intent.md +30 -0
- package/skills/path/templates/patch-findings.md +3 -3
- package/skills/path/templates/plan.md +17 -2
- package/skills/path/templates/research-handoff.md +14 -3
- package/skills/path/templates/state.md +1 -1
- package/skills/path/templates/wave-review.md +16 -6
- package/skills/gsd-path/scripts/__pycache__/_common.cpython-312.pyc +0 -0
- package/skills/gsd-path/scripts/__pycache__/check_handoffs.cpython-312.pyc +0 -0
- package/skills/gsd-path-build/scripts/__pycache__/_common.cpython-312.pyc +0 -0
- package/skills/gsd-path-build/scripts/__pycache__/check_handoffs.cpython-312.pyc +0 -0
- package/skills/gsd-path-inspect/scripts/__pycache__/_common.cpython-312.pyc +0 -0
- package/skills/gsd-path-plan/scripts/__pycache__/_common.cpython-312.pyc +0 -0
- package/skills/gsd-path-ship/scripts/__pycache__/_common.cpython-312.pyc +0 -0
|
@@ -65,6 +65,49 @@ VERIFY_RESULTS = ("pass", "fail")
|
|
|
65
65
|
VERIFY_LEDGER_SCHEMA = "gsd-path/verify-ledger/v2"
|
|
66
66
|
VERIFY_BLOCK_PATTERN = re.compile(r"```bash[ \t]*\n(?P<block>.*?)```", re.DOTALL)
|
|
67
67
|
|
|
68
|
+
REVIEW_BULLET_HEADING_PATTERN = re.compile(r"^#{2,3} ")
|
|
69
|
+
REVIEW_BULLET_FIELD_LABEL_PATTERN = re.compile(r"^- \*\*[^*]+\*\*:")
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def review_bullet_field_label_pattern(field: str) -> re.Pattern[str]:
|
|
73
|
+
return re.compile(rf"^- \*\*{re.escape(field)}\*\*:[ \t]*(.*)$")
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _review_bullet_field_value(lines: Sequence[str], index: int, inline: str) -> str:
|
|
77
|
+
if inline:
|
|
78
|
+
return inline
|
|
79
|
+
collected: list[str] = []
|
|
80
|
+
for line in lines[index + 1 :]:
|
|
81
|
+
if REVIEW_BULLET_FIELD_LABEL_PATTERN.match(line) or REVIEW_BULLET_HEADING_PATTERN.match(
|
|
82
|
+
line
|
|
83
|
+
):
|
|
84
|
+
break
|
|
85
|
+
if line and not line[0].isspace():
|
|
86
|
+
break
|
|
87
|
+
stripped = line.strip()
|
|
88
|
+
if stripped:
|
|
89
|
+
collected.append(stripped)
|
|
90
|
+
return "\n".join(collected)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def find_review_bullet_field_values(lines: Sequence[str], field: str) -> list[str]:
|
|
94
|
+
pattern = review_bullet_field_label_pattern(field)
|
|
95
|
+
values: list[str] = []
|
|
96
|
+
for index, line in enumerate(lines):
|
|
97
|
+
match = pattern.fullmatch(line)
|
|
98
|
+
if match is None:
|
|
99
|
+
continue
|
|
100
|
+
values.append(_review_bullet_field_value(lines, index, match.group(1)))
|
|
101
|
+
return values
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def parse_review_bullet_field(lines: Sequence[str], field: str) -> str:
|
|
105
|
+
"""Parse one `- **Field**:` review bullet, including indented continuations."""
|
|
106
|
+
values = find_review_bullet_field_values(lines, field)
|
|
107
|
+
if len(values) != 1:
|
|
108
|
+
raise ValueError(field)
|
|
109
|
+
return values[0]
|
|
110
|
+
|
|
68
111
|
|
|
69
112
|
# Sample paths the pipeline must commit. A product rule such as an unanchored
|
|
70
113
|
# `build/` would otherwise drop them silently from every commit.
|
|
@@ -150,6 +193,11 @@ def verify_ledger_entries(path: Path) -> list:
|
|
|
150
193
|
return parse_verify_ledger(path.read_text(encoding="utf-8"))
|
|
151
194
|
|
|
152
195
|
|
|
196
|
+
def read_user_text(path: Path) -> str:
|
|
197
|
+
"""Read a user-edited text file with CRLF normalized to LF."""
|
|
198
|
+
return path.read_text(encoding="utf-8").replace("\r\n", "\n").replace("\r", "\n")
|
|
199
|
+
|
|
200
|
+
|
|
153
201
|
def parse_verify_ledger(text: str) -> list:
|
|
154
202
|
entries = []
|
|
155
203
|
for number, line in enumerate(text.splitlines(), start=1):
|
|
@@ -204,7 +252,43 @@ def run_command(
|
|
|
204
252
|
def run_git(
|
|
205
253
|
repo: Path, *arguments: str, input: Optional[str] = None
|
|
206
254
|
) -> subprocess.CompletedProcess[str]:
|
|
207
|
-
return _run_exact((
|
|
255
|
+
return _run_exact(git_command(repo, *arguments), input=input)
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def git_command(repo: Path, *arguments: str) -> tuple[str, ...]:
|
|
259
|
+
"""Git argv for repo, including WSL drvfs workarounds when needed."""
|
|
260
|
+
return ("git", "-C", str(repo), *git_drvfs_config_flags(repo), *arguments)
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
def git_drvfs_config_flags(repo: Path) -> tuple[str, ...]:
|
|
264
|
+
"""Disable index preload on 9p/drvfs mounts where git commit races on index.lock (#207)."""
|
|
265
|
+
if os.name != "posix":
|
|
266
|
+
return ()
|
|
267
|
+
try:
|
|
268
|
+
root = repo.resolve()
|
|
269
|
+
mounts = Path("/proc/mounts").read_text(encoding="utf-8", errors="replace").splitlines()
|
|
270
|
+
except OSError:
|
|
271
|
+
return ()
|
|
272
|
+
best = ""
|
|
273
|
+
fstype = ""
|
|
274
|
+
for line in mounts:
|
|
275
|
+
parts = line.split()
|
|
276
|
+
if len(parts) < 3:
|
|
277
|
+
continue
|
|
278
|
+
mount_point, mount_type = parts[1], parts[2]
|
|
279
|
+
if mount_type not in {"9p", "drvfs"}:
|
|
280
|
+
continue
|
|
281
|
+
if root == Path(mount_point) or root.is_relative_to(mount_point):
|
|
282
|
+
if len(mount_point) >= len(best):
|
|
283
|
+
best, fstype = mount_point, mount_type
|
|
284
|
+
if not fstype:
|
|
285
|
+
return ()
|
|
286
|
+
return ("-c", "core.preloadindex=false", "-c", "index.threads=1")
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
def git_index_lock_retryable(stderr: str) -> bool:
|
|
290
|
+
text = stderr.casefold()
|
|
291
|
+
return "index.lock" in text and "file exists" in text
|
|
208
292
|
|
|
209
293
|
|
|
210
294
|
# OS files that are never pipeline artifacts when git ignores them.
|
|
@@ -227,6 +311,70 @@ def git_visible_entries(directory: Path) -> set:
|
|
|
227
311
|
return {path.split("/", 1)[0] for path in result.stdout.split("\0") if path}
|
|
228
312
|
|
|
229
313
|
|
|
314
|
+
RUNTIME_PIN_PATH = ".gsd-path/runtime.json"
|
|
315
|
+
RUNTIME_PIN_ALLOWANCE = (
|
|
316
|
+
"only runtime-pin commits (.gsd-path/runtime.json) may follow the reviewed HEAD"
|
|
317
|
+
)
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
def reviewed_head_covers(repo: Path, reviewed: str, head: str) -> bool:
|
|
321
|
+
"""True when a review of `reviewed` still covers `head`.
|
|
322
|
+
|
|
323
|
+
That is the same commit, or `reviewed` is an ancestor of `head` and every
|
|
324
|
+
commit on the first-parent path in ``reviewed..head`` is a non-merge commit
|
|
325
|
+
whose only change is ``.gsd-path/runtime.json``. An empty commit does not
|
|
326
|
+
qualify. Any other path, any merge commit, an unresolvable SHA, or a
|
|
327
|
+
non-ancestor returns False.
|
|
328
|
+
"""
|
|
329
|
+
if not reviewed or not head:
|
|
330
|
+
return False
|
|
331
|
+
resolved_repo = Path(repo)
|
|
332
|
+
resolved: List[str] = []
|
|
333
|
+
for value in (reviewed, head):
|
|
334
|
+
result = run_git(
|
|
335
|
+
resolved_repo, "rev-parse", "--verify", "--quiet", f"{value}^{{commit}}"
|
|
336
|
+
)
|
|
337
|
+
if result.returncode != 0 or not result.stdout.strip():
|
|
338
|
+
return False
|
|
339
|
+
resolved.append(result.stdout.strip())
|
|
340
|
+
reviewed_sha, head_sha = resolved
|
|
341
|
+
if reviewed_sha == head_sha:
|
|
342
|
+
return True
|
|
343
|
+
if (
|
|
344
|
+
run_git(resolved_repo, "merge-base", "--is-ancestor", reviewed_sha, head_sha).returncode
|
|
345
|
+
!= 0
|
|
346
|
+
):
|
|
347
|
+
return False
|
|
348
|
+
listing = run_git(
|
|
349
|
+
resolved_repo,
|
|
350
|
+
"rev-list",
|
|
351
|
+
"--first-parent",
|
|
352
|
+
"--parents",
|
|
353
|
+
f"{reviewed_sha}..{head_sha}",
|
|
354
|
+
)
|
|
355
|
+
if listing.returncode != 0:
|
|
356
|
+
return False
|
|
357
|
+
for line in listing.stdout.splitlines():
|
|
358
|
+
fields = line.split()
|
|
359
|
+
if len(fields) != 2:
|
|
360
|
+
return False
|
|
361
|
+
commit_sha, parent_sha = fields[0], fields[1]
|
|
362
|
+
changed = run_git(
|
|
363
|
+
resolved_repo,
|
|
364
|
+
"diff",
|
|
365
|
+
"--name-only",
|
|
366
|
+
"--no-renames",
|
|
367
|
+
"--no-ext-diff",
|
|
368
|
+
parent_sha,
|
|
369
|
+
commit_sha,
|
|
370
|
+
)
|
|
371
|
+
if changed.returncode != 0:
|
|
372
|
+
return False
|
|
373
|
+
if {path for path in changed.stdout.splitlines() if path} != {RUNTIME_PIN_PATH}:
|
|
374
|
+
return False
|
|
375
|
+
return True
|
|
376
|
+
|
|
377
|
+
|
|
230
378
|
def atomic_replace(path: Path, temporary_path: Path, content: str) -> None:
|
|
231
379
|
temporary_path.parent.mkdir(parents=True, exist_ok=True)
|
|
232
380
|
descriptor = None
|
|
@@ -514,16 +514,28 @@ def run_shell(command: str, timeout: float) -> tuple[Optional[int], str]:
|
|
|
514
514
|
"""Run one spec command; a hung command counts as failed (exit None).
|
|
515
515
|
|
|
516
516
|
POSIX runs /bin/sh as shell=True did; Windows runs Git for Windows bash, not
|
|
517
|
-
cmd.exe. A timeout kills the whole process group
|
|
518
|
-
|
|
517
|
+
cmd.exe. A timeout kills the whole process group when possible; if output
|
|
518
|
+
pipes stay open, drain them with a short grace timeout and then close them.
|
|
519
519
|
"""
|
|
520
520
|
argv = _common.bash_argv(command) if os.name == "nt" else ["/bin/sh", "-c", command]
|
|
521
521
|
process = _common.popen_group(argv, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
|
522
|
+
stdout = b""
|
|
523
|
+
stderr = b""
|
|
522
524
|
try:
|
|
523
525
|
stdout, stderr = process.communicate(timeout=timeout)
|
|
524
|
-
except subprocess.TimeoutExpired:
|
|
526
|
+
except subprocess.TimeoutExpired as expired:
|
|
525
527
|
_common.kill_tree(process)
|
|
526
|
-
|
|
528
|
+
try:
|
|
529
|
+
stdout, stderr = process.communicate(timeout=5)
|
|
530
|
+
except subprocess.TimeoutExpired as drained:
|
|
531
|
+
stdout = drained.stdout or expired.stdout or b""
|
|
532
|
+
stderr = drained.stderr or expired.stderr or b""
|
|
533
|
+
if process.stdout:
|
|
534
|
+
process.stdout.close()
|
|
535
|
+
if process.stderr:
|
|
536
|
+
process.stderr.close()
|
|
537
|
+
process.kill()
|
|
538
|
+
process.wait(timeout=5)
|
|
527
539
|
output = stdout.decode("utf-8", errors="replace") + stderr.decode("utf-8", errors="replace")
|
|
528
540
|
return None, f"{output}\ntimed out at the admitted wall-clock deadline"
|
|
529
541
|
return process.returncode, (stdout + stderr).decode("utf-8", errors="replace")
|
|
@@ -13,7 +13,7 @@ sys.dont_write_bytecode = True
|
|
|
13
13
|
ROLES = ('coder', 'reviewer', 'review_panel', 'skeptic', 'inspect_codebase',
|
|
14
14
|
'inspect_docs', 'docs_audit', 'research', 'decide', 'roadmap', 'plan', 'plan_patch')
|
|
15
15
|
HOSTS = ('codex', 'claude', 'grok', 'opencode', 'copilot', 'qwen', 'antigravity',
|
|
16
|
-
'cursor', 'zed', 'kiro', 'kimi')
|
|
16
|
+
'cursor', 'zed', 'kiro', 'kimi', 'muse')
|
|
17
17
|
|
|
18
18
|
|
|
19
19
|
def sibling(name):
|
|
@@ -73,8 +73,8 @@ def validate_value(key, value):
|
|
|
73
73
|
if not isinstance(value, str) or not value or value != value.strip():
|
|
74
74
|
raise ValueError(f'{key} requires a nonempty string')
|
|
75
75
|
if key == 'integration':
|
|
76
|
-
if value not in ('direct', 'pull-request'):
|
|
77
|
-
raise ValueError('integration must be direct or
|
|
76
|
+
if value not in ('direct', 'pull-request', 'external-landing'):
|
|
77
|
+
raise ValueError('integration must be direct, pull-request, or external-landing')
|
|
78
78
|
elif key == 'review_panel':
|
|
79
79
|
if any(char.isspace() for char in value):
|
|
80
80
|
raise ValueError('use off, detected, or comma-separated families without spaces')
|
|
@@ -22,6 +22,27 @@ else:
|
|
|
22
22
|
from scripts import _common
|
|
23
23
|
|
|
24
24
|
|
|
25
|
+
def _worktree_list_porcelain(primary: Path) -> tuple[str, bool]:
|
|
26
|
+
try:
|
|
27
|
+
if __package__:
|
|
28
|
+
from . import pipeline_git as pg
|
|
29
|
+
else:
|
|
30
|
+
from scripts import pipeline_git as pg
|
|
31
|
+
except ImportError:
|
|
32
|
+
try:
|
|
33
|
+
import pipeline_git as pg
|
|
34
|
+
except ImportError:
|
|
35
|
+
pg = None
|
|
36
|
+
if pg is not None:
|
|
37
|
+
return pg.worktree_list_porcelain(primary)
|
|
38
|
+
try:
|
|
39
|
+
output = _git(primary, "worktree", "list", "--porcelain", "-z")
|
|
40
|
+
return output, True
|
|
41
|
+
except ValueError:
|
|
42
|
+
output = _git(primary, "worktree", "list", "--porcelain")
|
|
43
|
+
return output, False
|
|
44
|
+
|
|
45
|
+
|
|
25
46
|
def _git(primary: Path, *args: str) -> str:
|
|
26
47
|
result = _common.run_git(primary, *args)
|
|
27
48
|
if result.returncode:
|
|
@@ -82,6 +103,33 @@ def _workspace(primary: Path, pin: bool) -> Path:
|
|
|
82
103
|
return workspace
|
|
83
104
|
|
|
84
105
|
|
|
106
|
+
def release_workspace(primary: Path) -> bool:
|
|
107
|
+
"""Forget the pinned placement receipt once no linked worktrees remain.
|
|
108
|
+
|
|
109
|
+
Lets GSD_PATH_WORKTREE_ROOT (or a new default) apply again at the next
|
|
110
|
+
milestone's first allocation. Returns True when a receipt was removed;
|
|
111
|
+
any remaining linked worktree (including one only still registered) keeps
|
|
112
|
+
the pin.
|
|
113
|
+
"""
|
|
114
|
+
primary = primary.resolve()
|
|
115
|
+
common = Path(_git(primary, "rev-parse", "--git-common-dir"))
|
|
116
|
+
common = (primary / common).resolve()
|
|
117
|
+
receipt = common / "gsd-path" / "workspaces" / f"{_identity(primary)}.json"
|
|
118
|
+
if not receipt.is_file():
|
|
119
|
+
return False
|
|
120
|
+
output, nul_separated = _worktree_list_porcelain(primary)
|
|
121
|
+
records = output.split("\0\0") if nul_separated else [
|
|
122
|
+
block for block in output.strip().split("\n\n") if block.strip()
|
|
123
|
+
]
|
|
124
|
+
for record in records:
|
|
125
|
+
fields = record.split("\0") if nul_separated else record.splitlines()
|
|
126
|
+
for field in fields:
|
|
127
|
+
if field.startswith("worktree ") and Path(field[len("worktree "):]).resolve() != primary:
|
|
128
|
+
return False
|
|
129
|
+
receipt.unlink()
|
|
130
|
+
return True
|
|
131
|
+
|
|
132
|
+
|
|
85
133
|
def worktree_path(primary: Path, kind: str, name: str, *, pin: bool = False) -> Path:
|
|
86
134
|
primary = primary.resolve()
|
|
87
135
|
if kind not in {"task", "verify", "integrate", "bound"} or not name or Path(name).name != name or name in {".", ".."}:
|
|
@@ -91,9 +139,13 @@ def worktree_path(primary: Path, kind: str, name: str, *, pin: bool = False) ->
|
|
|
91
139
|
legacy = (primary.parent / f".{primary.name}-gsd-path-integrate-{name}"
|
|
92
140
|
if kind == "integrate" else primary.parent / f"{primary.name}.gsd-path" / kind / name)
|
|
93
141
|
branch = f"refs/heads/gsd-path-{kind}/{name}"
|
|
94
|
-
|
|
142
|
+
output, nul_separated = _worktree_list_porcelain(primary)
|
|
143
|
+
if nul_separated:
|
|
144
|
+
records = output.split("\0\0")
|
|
145
|
+
else:
|
|
146
|
+
records = [block for block in output.strip().split("\n\n") if block.strip()]
|
|
95
147
|
for record in records:
|
|
96
|
-
fields = record.split("\0")
|
|
148
|
+
fields = record.split("\0") if nul_separated else record.splitlines()
|
|
97
149
|
# Compare as paths: git prints forward slashes on Windows.
|
|
98
150
|
worktrees = [Path(field[len("worktree "):]) for field in fields if field.startswith("worktree ")]
|
|
99
151
|
if legacy in worktrees and f"branch {branch}" in fields:
|
|
@@ -44,6 +44,19 @@ use exactly one matching branch:
|
|
|
44
44
|
create another worktree. Encode task dependencies explicitly; launch
|
|
45
45
|
independent children concurrently up to the host's advertised
|
|
46
46
|
concurrent-subagent capacity.
|
|
47
|
+
- A `muse.subagent_spawn` tool (with `muse.subagent_wait`,
|
|
48
|
+
`muse.subagent_read_result`, and `muse.subagent_send_message` siblings) is
|
|
49
|
+
Muse Code. Omit `subagent_type` (general-purpose, full capability), pass the
|
|
50
|
+
logical task name as `task_name` and `role`, and the complete brief as
|
|
51
|
+
`objective`. Never set `worktree_isolation`. Keep the returned
|
|
52
|
+
`subagent_id`; send a correction to a still-live child with
|
|
53
|
+
`muse.subagent_send_message` mode `followup` and the complete new prompt,
|
|
54
|
+
otherwise start a fresh child. Collect with `muse.subagent_wait`
|
|
55
|
+
(`wait_for: task_terminal`) then `muse.subagent_read_result`; on
|
|
56
|
+
`root_capacity_exhausted`, wait for a child to finish and retry. Spawn has no
|
|
57
|
+
model or effort argument, so record no such capability. If only
|
|
58
|
+
`muse.workflow` is exposed (untrusted workspace), do not script or inline
|
|
59
|
+
the work; stop and report that the workspace must be trusted.
|
|
47
60
|
|
|
48
61
|
For every branch:
|
|
49
62
|
|
|
@@ -59,6 +59,11 @@ actually does. Read-only: change nothing outside your single output file.
|
|
|
59
59
|
5. **Classify remediation** for every non-verified claim: `fix-doc`,
|
|
60
60
|
`fix-code`, or `NEEDS-USER` when the right side of the conflict is not
|
|
61
61
|
yours to decide.
|
|
62
|
+
Number queue rows with unique, strictly increasing positive integers; they
|
|
63
|
+
need not be contiguous. A row that persists from the prior audit keeps its
|
|
64
|
+
number. A new row takes a number greater than every prior queue number and
|
|
65
|
+
every `## User rulings` Queue #. Never reuse a retired number: a ruling
|
|
66
|
+
stays bound to the item it was made for.
|
|
62
67
|
|
|
63
68
|
## Rules
|
|
64
69
|
|
|
@@ -65,6 +65,49 @@ VERIFY_RESULTS = ("pass", "fail")
|
|
|
65
65
|
VERIFY_LEDGER_SCHEMA = "gsd-path/verify-ledger/v2"
|
|
66
66
|
VERIFY_BLOCK_PATTERN = re.compile(r"```bash[ \t]*\n(?P<block>.*?)```", re.DOTALL)
|
|
67
67
|
|
|
68
|
+
REVIEW_BULLET_HEADING_PATTERN = re.compile(r"^#{2,3} ")
|
|
69
|
+
REVIEW_BULLET_FIELD_LABEL_PATTERN = re.compile(r"^- \*\*[^*]+\*\*:")
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def review_bullet_field_label_pattern(field: str) -> re.Pattern[str]:
|
|
73
|
+
return re.compile(rf"^- \*\*{re.escape(field)}\*\*:[ \t]*(.*)$")
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _review_bullet_field_value(lines: Sequence[str], index: int, inline: str) -> str:
|
|
77
|
+
if inline:
|
|
78
|
+
return inline
|
|
79
|
+
collected: list[str] = []
|
|
80
|
+
for line in lines[index + 1 :]:
|
|
81
|
+
if REVIEW_BULLET_FIELD_LABEL_PATTERN.match(line) or REVIEW_BULLET_HEADING_PATTERN.match(
|
|
82
|
+
line
|
|
83
|
+
):
|
|
84
|
+
break
|
|
85
|
+
if line and not line[0].isspace():
|
|
86
|
+
break
|
|
87
|
+
stripped = line.strip()
|
|
88
|
+
if stripped:
|
|
89
|
+
collected.append(stripped)
|
|
90
|
+
return "\n".join(collected)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def find_review_bullet_field_values(lines: Sequence[str], field: str) -> list[str]:
|
|
94
|
+
pattern = review_bullet_field_label_pattern(field)
|
|
95
|
+
values: list[str] = []
|
|
96
|
+
for index, line in enumerate(lines):
|
|
97
|
+
match = pattern.fullmatch(line)
|
|
98
|
+
if match is None:
|
|
99
|
+
continue
|
|
100
|
+
values.append(_review_bullet_field_value(lines, index, match.group(1)))
|
|
101
|
+
return values
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def parse_review_bullet_field(lines: Sequence[str], field: str) -> str:
|
|
105
|
+
"""Parse one `- **Field**:` review bullet, including indented continuations."""
|
|
106
|
+
values = find_review_bullet_field_values(lines, field)
|
|
107
|
+
if len(values) != 1:
|
|
108
|
+
raise ValueError(field)
|
|
109
|
+
return values[0]
|
|
110
|
+
|
|
68
111
|
|
|
69
112
|
# Sample paths the pipeline must commit. A product rule such as an unanchored
|
|
70
113
|
# `build/` would otherwise drop them silently from every commit.
|
|
@@ -150,6 +193,11 @@ def verify_ledger_entries(path: Path) -> list:
|
|
|
150
193
|
return parse_verify_ledger(path.read_text(encoding="utf-8"))
|
|
151
194
|
|
|
152
195
|
|
|
196
|
+
def read_user_text(path: Path) -> str:
|
|
197
|
+
"""Read a user-edited text file with CRLF normalized to LF."""
|
|
198
|
+
return path.read_text(encoding="utf-8").replace("\r\n", "\n").replace("\r", "\n")
|
|
199
|
+
|
|
200
|
+
|
|
153
201
|
def parse_verify_ledger(text: str) -> list:
|
|
154
202
|
entries = []
|
|
155
203
|
for number, line in enumerate(text.splitlines(), start=1):
|
|
@@ -204,7 +252,43 @@ def run_command(
|
|
|
204
252
|
def run_git(
|
|
205
253
|
repo: Path, *arguments: str, input: Optional[str] = None
|
|
206
254
|
) -> subprocess.CompletedProcess[str]:
|
|
207
|
-
return _run_exact((
|
|
255
|
+
return _run_exact(git_command(repo, *arguments), input=input)
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def git_command(repo: Path, *arguments: str) -> tuple[str, ...]:
|
|
259
|
+
"""Git argv for repo, including WSL drvfs workarounds when needed."""
|
|
260
|
+
return ("git", "-C", str(repo), *git_drvfs_config_flags(repo), *arguments)
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
def git_drvfs_config_flags(repo: Path) -> tuple[str, ...]:
|
|
264
|
+
"""Disable index preload on 9p/drvfs mounts where git commit races on index.lock (#207)."""
|
|
265
|
+
if os.name != "posix":
|
|
266
|
+
return ()
|
|
267
|
+
try:
|
|
268
|
+
root = repo.resolve()
|
|
269
|
+
mounts = Path("/proc/mounts").read_text(encoding="utf-8", errors="replace").splitlines()
|
|
270
|
+
except OSError:
|
|
271
|
+
return ()
|
|
272
|
+
best = ""
|
|
273
|
+
fstype = ""
|
|
274
|
+
for line in mounts:
|
|
275
|
+
parts = line.split()
|
|
276
|
+
if len(parts) < 3:
|
|
277
|
+
continue
|
|
278
|
+
mount_point, mount_type = parts[1], parts[2]
|
|
279
|
+
if mount_type not in {"9p", "drvfs"}:
|
|
280
|
+
continue
|
|
281
|
+
if root == Path(mount_point) or root.is_relative_to(mount_point):
|
|
282
|
+
if len(mount_point) >= len(best):
|
|
283
|
+
best, fstype = mount_point, mount_type
|
|
284
|
+
if not fstype:
|
|
285
|
+
return ()
|
|
286
|
+
return ("-c", "core.preloadindex=false", "-c", "index.threads=1")
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
def git_index_lock_retryable(stderr: str) -> bool:
|
|
290
|
+
text = stderr.casefold()
|
|
291
|
+
return "index.lock" in text and "file exists" in text
|
|
208
292
|
|
|
209
293
|
|
|
210
294
|
# OS files that are never pipeline artifacts when git ignores them.
|
|
@@ -227,6 +311,70 @@ def git_visible_entries(directory: Path) -> set:
|
|
|
227
311
|
return {path.split("/", 1)[0] for path in result.stdout.split("\0") if path}
|
|
228
312
|
|
|
229
313
|
|
|
314
|
+
RUNTIME_PIN_PATH = ".gsd-path/runtime.json"
|
|
315
|
+
RUNTIME_PIN_ALLOWANCE = (
|
|
316
|
+
"only runtime-pin commits (.gsd-path/runtime.json) may follow the reviewed HEAD"
|
|
317
|
+
)
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
def reviewed_head_covers(repo: Path, reviewed: str, head: str) -> bool:
|
|
321
|
+
"""True when a review of `reviewed` still covers `head`.
|
|
322
|
+
|
|
323
|
+
That is the same commit, or `reviewed` is an ancestor of `head` and every
|
|
324
|
+
commit on the first-parent path in ``reviewed..head`` is a non-merge commit
|
|
325
|
+
whose only change is ``.gsd-path/runtime.json``. An empty commit does not
|
|
326
|
+
qualify. Any other path, any merge commit, an unresolvable SHA, or a
|
|
327
|
+
non-ancestor returns False.
|
|
328
|
+
"""
|
|
329
|
+
if not reviewed or not head:
|
|
330
|
+
return False
|
|
331
|
+
resolved_repo = Path(repo)
|
|
332
|
+
resolved: List[str] = []
|
|
333
|
+
for value in (reviewed, head):
|
|
334
|
+
result = run_git(
|
|
335
|
+
resolved_repo, "rev-parse", "--verify", "--quiet", f"{value}^{{commit}}"
|
|
336
|
+
)
|
|
337
|
+
if result.returncode != 0 or not result.stdout.strip():
|
|
338
|
+
return False
|
|
339
|
+
resolved.append(result.stdout.strip())
|
|
340
|
+
reviewed_sha, head_sha = resolved
|
|
341
|
+
if reviewed_sha == head_sha:
|
|
342
|
+
return True
|
|
343
|
+
if (
|
|
344
|
+
run_git(resolved_repo, "merge-base", "--is-ancestor", reviewed_sha, head_sha).returncode
|
|
345
|
+
!= 0
|
|
346
|
+
):
|
|
347
|
+
return False
|
|
348
|
+
listing = run_git(
|
|
349
|
+
resolved_repo,
|
|
350
|
+
"rev-list",
|
|
351
|
+
"--first-parent",
|
|
352
|
+
"--parents",
|
|
353
|
+
f"{reviewed_sha}..{head_sha}",
|
|
354
|
+
)
|
|
355
|
+
if listing.returncode != 0:
|
|
356
|
+
return False
|
|
357
|
+
for line in listing.stdout.splitlines():
|
|
358
|
+
fields = line.split()
|
|
359
|
+
if len(fields) != 2:
|
|
360
|
+
return False
|
|
361
|
+
commit_sha, parent_sha = fields[0], fields[1]
|
|
362
|
+
changed = run_git(
|
|
363
|
+
resolved_repo,
|
|
364
|
+
"diff",
|
|
365
|
+
"--name-only",
|
|
366
|
+
"--no-renames",
|
|
367
|
+
"--no-ext-diff",
|
|
368
|
+
parent_sha,
|
|
369
|
+
commit_sha,
|
|
370
|
+
)
|
|
371
|
+
if changed.returncode != 0:
|
|
372
|
+
return False
|
|
373
|
+
if {path for path in changed.stdout.splitlines() if path} != {RUNTIME_PIN_PATH}:
|
|
374
|
+
return False
|
|
375
|
+
return True
|
|
376
|
+
|
|
377
|
+
|
|
230
378
|
def atomic_replace(path: Path, temporary_path: Path, content: str) -> None:
|
|
231
379
|
temporary_path.parent.mkdir(parents=True, exist_ok=True)
|
|
232
380
|
descriptor = None
|
|
@@ -426,10 +426,10 @@ def frontmatter_value(content: str, key: str) -> Optional[str]:
|
|
|
426
426
|
for line in lines[1:]:
|
|
427
427
|
if line == "---":
|
|
428
428
|
break
|
|
429
|
-
match = re.match(rf"^{re.escape(key)}
|
|
429
|
+
match = re.match(rf"^{re.escape(key)}:(.*)$", line)
|
|
430
430
|
if match:
|
|
431
|
-
value = match.group(1)
|
|
432
|
-
return value
|
|
431
|
+
value = _common.strip_yaml_comment(match.group(1))
|
|
432
|
+
return value.strip().strip('"').strip("'")
|
|
433
433
|
return None
|
|
434
434
|
|
|
435
435
|
|
|
@@ -586,14 +586,11 @@ def resolved_archive_target(
|
|
|
586
586
|
expected_number = milestone_number(branch)
|
|
587
587
|
configured = parsed_state.archive
|
|
588
588
|
entries = archive_sequence_entries(project)
|
|
589
|
-
missing_prior = [
|
|
590
|
-
number for number in range(1, expected_number) if number not in entries
|
|
591
|
-
]
|
|
592
589
|
later = sorted(number for number in entries if number > expected_number)
|
|
593
|
-
if
|
|
590
|
+
if later:
|
|
594
591
|
raise ArchiveError(
|
|
595
592
|
f"archive sequence does not match bound branch {branch}: "
|
|
596
|
-
f"
|
|
593
|
+
f"later={later}"
|
|
597
594
|
)
|
|
598
595
|
|
|
599
596
|
if configured is not None:
|
|
@@ -1065,9 +1062,10 @@ def prepare_locked(project: Path, active_root: Path, slug: str) -> dict:
|
|
|
1065
1062
|
final = active_root / "review" / "FINAL.md"
|
|
1066
1063
|
reviewed = re.search(r"(?m)^Reviewed HEAD:\s*(\S+)", final.read_text(encoding="utf-8")) if final.is_file() else None
|
|
1067
1064
|
head = require_git_success(run_git(project, "rev-parse", "HEAD"), "resolve HEAD").strip()
|
|
1068
|
-
if reviewed and reviewed.group(1)
|
|
1065
|
+
if reviewed and not _common.reviewed_head_covers(project, reviewed.group(1), head):
|
|
1069
1066
|
raise ArchiveError(
|
|
1070
1067
|
f"FINAL.md Reviewed HEAD {reviewed.group(1)} does not match current HEAD {head}; "
|
|
1068
|
+
f"{_common.RUNTIME_PIN_ALLOWANCE}; "
|
|
1071
1069
|
"re-run the final review on the current commit before archiving"
|
|
1072
1070
|
)
|
|
1073
1071
|
target = resolved_archive_target(project, slug, parsed_state)
|
|
@@ -1680,6 +1678,7 @@ def abandon_locked(
|
|
|
1680
1678
|
|
|
1681
1679
|
|
|
1682
1680
|
run_git = _common.run_git
|
|
1681
|
+
reviewed_head_covers = _common.reviewed_head_covers
|
|
1683
1682
|
run_command = _common.run_command
|
|
1684
1683
|
|
|
1685
1684
|
|
|
@@ -1899,9 +1898,10 @@ def preflight(repo: Path) -> dict:
|
|
|
1899
1898
|
require_stageable_carry_forward(project, archive)
|
|
1900
1899
|
require_clean_older_archives(project, configured)
|
|
1901
1900
|
head = require_git_success(run_git(project, "rev-parse", "HEAD"), "resolve preflight HEAD")
|
|
1902
|
-
if reviewed_head
|
|
1901
|
+
if not reviewed_head_covers(project, reviewed_head, head):
|
|
1903
1902
|
raise ArchiveError(
|
|
1904
|
-
f"FINAL.md Reviewed HEAD {reviewed_head} does not match current HEAD {head}"
|
|
1903
|
+
f"FINAL.md Reviewed HEAD {reviewed_head} does not match current HEAD {head}; "
|
|
1904
|
+
f"{_common.RUNTIME_PIN_ALLOWANCE}"
|
|
1905
1905
|
)
|
|
1906
1906
|
return {
|
|
1907
1907
|
"archive": configured,
|
|
@@ -2093,9 +2093,10 @@ def validate(repo: Path, *, historical: bool = False) -> dict:
|
|
|
2093
2093
|
|
|
2094
2094
|
members = member_ship_rows(project, archive, ship_commit)
|
|
2095
2095
|
ship_parent = require_git_success(run_git(project, "rev-parse", f"{ship_commit}^"), "resolve ship parent")
|
|
2096
|
-
if reviewed_head
|
|
2096
|
+
if not reviewed_head_covers(project, reviewed_head, ship_parent):
|
|
2097
2097
|
raise ArchiveError(
|
|
2098
|
-
f"FINAL.md Reviewed HEAD {reviewed_head} does not match ship parent {ship_parent}"
|
|
2098
|
+
f"FINAL.md Reviewed HEAD {reviewed_head} does not match ship parent {ship_parent}; "
|
|
2099
|
+
f"{_common.RUNTIME_PIN_ALLOWANCE}"
|
|
2099
2100
|
)
|
|
2100
2101
|
require_canonical_commit_body(
|
|
2101
2102
|
project,
|