@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')
|
|
@@ -17,6 +17,7 @@ import os
|
|
|
17
17
|
import re
|
|
18
18
|
import shutil
|
|
19
19
|
import sys
|
|
20
|
+
from datetime import date
|
|
20
21
|
|
|
21
22
|
# Runtime helpers must not modify their immutable installation.
|
|
22
23
|
sys.dont_write_bytecode = True
|
|
@@ -106,6 +107,16 @@ KINDS = ("checkpoint", "task", "member-task", "uncommitted-archive", "lookahead"
|
|
|
106
107
|
PROMOTION_PREFIX = "router: promote lookahead milestone "
|
|
107
108
|
ABANDON_PREFIX = "build: abandon milestone "
|
|
108
109
|
INTEGRATE_PREFIX = "integrate: "
|
|
110
|
+
SHIP_LOG_ONLY_LINE = re.compile(
|
|
111
|
+
r"- (?P<date>\d{4}-\d{2}-\d{2}) — ship — (?P<event>[^\r\n]+)\n\Z"
|
|
112
|
+
)
|
|
113
|
+
SHIPMENT_RECORD_LINE = re.compile(
|
|
114
|
+
r"- (?P<date>\d{4}-\d{2}-\d{2}) — shipped — (?P<event>[^\r\n]+)\n\Z"
|
|
115
|
+
)
|
|
116
|
+
SHIPMENT_EVENTS = {
|
|
117
|
+
"archive preflight passed; shipment recorded",
|
|
118
|
+
"archive preflight passed; shipment recorded; program complete",
|
|
119
|
+
}
|
|
109
120
|
|
|
110
121
|
|
|
111
122
|
class UndoError(RuntimeError):
|
|
@@ -375,6 +386,80 @@ def pending_transaction(repo: Path) -> Optional[dict[str, object]]:
|
|
|
375
386
|
raise UndoError(str(error)) from error
|
|
376
387
|
|
|
377
388
|
|
|
389
|
+
def _valid_state_event_line(line: str, phase: str) -> bool:
|
|
390
|
+
pattern = SHIP_LOG_ONLY_LINE if phase == "ship" else SHIPMENT_RECORD_LINE
|
|
391
|
+
match = pattern.fullmatch(line)
|
|
392
|
+
if match is None:
|
|
393
|
+
return False
|
|
394
|
+
try:
|
|
395
|
+
event_date = date.fromisoformat(match.group("date"))
|
|
396
|
+
except ValueError:
|
|
397
|
+
return False
|
|
398
|
+
event = match.group("event")
|
|
399
|
+
return event_date.isoformat() == match.group("date") and event == " ".join(event.split())
|
|
400
|
+
|
|
401
|
+
|
|
402
|
+
def _owned_ship_log_events(
|
|
403
|
+
prepared_state: str,
|
|
404
|
+
current_state: str,
|
|
405
|
+
phase: object,
|
|
406
|
+
status: object,
|
|
407
|
+
) -> Optional[str]:
|
|
408
|
+
"""Return only ship log lines appended around a helper-owned archive event.
|
|
409
|
+
|
|
410
|
+
The prepared text must remain byte-for-byte unchanged. For shipped/done,
|
|
411
|
+
record-shipment changes only phase and status before appending its one
|
|
412
|
+
generated shipment event, so validate that exact prefix and event shape.
|
|
413
|
+
"""
|
|
414
|
+
if (phase, status) == ("ship", "active"):
|
|
415
|
+
if current_state == prepared_state:
|
|
416
|
+
return ""
|
|
417
|
+
if not current_state.startswith(prepared_state):
|
|
418
|
+
return None
|
|
419
|
+
suffix = current_state[len(prepared_state):]
|
|
420
|
+
separator = "" if prepared_state.endswith("\n") else "\n"
|
|
421
|
+
if separator:
|
|
422
|
+
if not suffix.startswith(separator):
|
|
423
|
+
return None
|
|
424
|
+
suffix = suffix[len(separator):]
|
|
425
|
+
lines = suffix.splitlines(keepends=True)
|
|
426
|
+
if not lines or "".join(lines) != suffix:
|
|
427
|
+
return None
|
|
428
|
+
if not all(_valid_state_event_line(line, "ship") for line in lines):
|
|
429
|
+
return None
|
|
430
|
+
return separator + suffix
|
|
431
|
+
|
|
432
|
+
if (phase, status) != ("shipped", "done"):
|
|
433
|
+
return None
|
|
434
|
+
try:
|
|
435
|
+
shipped_prefix = pipeline_state._set_frontmatter(
|
|
436
|
+
prepared_state,
|
|
437
|
+
{"phase": "shipped", "status": "done"},
|
|
438
|
+
)
|
|
439
|
+
except PipelineStateError:
|
|
440
|
+
return None
|
|
441
|
+
if not current_state.startswith(shipped_prefix):
|
|
442
|
+
return None
|
|
443
|
+
suffix = current_state[len(shipped_prefix):]
|
|
444
|
+
separator = "" if shipped_prefix.endswith("\n") else "\n"
|
|
445
|
+
if separator:
|
|
446
|
+
if not suffix.startswith(separator):
|
|
447
|
+
return None
|
|
448
|
+
suffix = suffix[len(separator):]
|
|
449
|
+
lines = suffix.splitlines(keepends=True)
|
|
450
|
+
if not lines or "".join(lines) != suffix:
|
|
451
|
+
return None
|
|
452
|
+
shipment_event = lines[-1]
|
|
453
|
+
if not _valid_state_event_line(shipment_event, "shipped"):
|
|
454
|
+
return None
|
|
455
|
+
if SHIPMENT_RECORD_LINE.fullmatch(shipment_event).group("event") not in SHIPMENT_EVENTS:
|
|
456
|
+
return None
|
|
457
|
+
ship_events = lines[:-1]
|
|
458
|
+
if not all(_valid_state_event_line(line, "ship") for line in ship_events):
|
|
459
|
+
return None
|
|
460
|
+
return separator + "".join(ship_events) if ship_events else ""
|
|
461
|
+
|
|
462
|
+
|
|
378
463
|
def _archive_metadata_error(
|
|
379
464
|
repo: Path,
|
|
380
465
|
state_fields: dict[str, object],
|
|
@@ -420,8 +505,13 @@ def _archive_metadata_error(
|
|
|
420
505
|
if final_review.exists() or gap_reviews:
|
|
421
506
|
reviewed_head, _ = archive_milestone.parse_final_review(archive_path)
|
|
422
507
|
archive_milestone.validate_gap_reviews(archive_path, reviewed_head)
|
|
423
|
-
if
|
|
424
|
-
|
|
508
|
+
if not archive_milestone.reviewed_head_covers(
|
|
509
|
+
repo, reviewed_head, _optional_rev(repo, "HEAD")
|
|
510
|
+
):
|
|
511
|
+
return (
|
|
512
|
+
"archive undo final review does not match current HEAD; "
|
|
513
|
+
f"{archive_milestone._common.RUNTIME_PIN_ALLOWANCE}"
|
|
514
|
+
)
|
|
425
515
|
expected_inventory.add("review/FINAL.md")
|
|
426
516
|
expected_inventory.update(
|
|
427
517
|
path.relative_to(archive_path).as_posix() for path in gap_reviews
|
|
@@ -476,10 +566,14 @@ def _archive_metadata_error(
|
|
|
476
566
|
"archive",
|
|
477
567
|
configured,
|
|
478
568
|
)
|
|
479
|
-
current_state = (repo / ".project" / "STATE.md").
|
|
569
|
+
current_state = (repo / ".project" / "STATE.md").read_bytes().decode("utf-8")
|
|
480
570
|
phase = state_fields.get("phase")
|
|
481
571
|
status = state_fields.get("status")
|
|
572
|
+
ship_events = _owned_ship_log_events(prepared_state, current_state, phase, status)
|
|
482
573
|
if (phase, status) == ("ship", "active"):
|
|
574
|
+
if ship_events is None:
|
|
575
|
+
return "archive undo found unowned STATE.md changes"
|
|
576
|
+
prepared_state += ship_events
|
|
483
577
|
if current_state != prepared_state:
|
|
484
578
|
return "archive undo found unowned STATE.md changes"
|
|
485
579
|
if ".project/ROADMAP.md" in dirty:
|
|
@@ -487,14 +581,13 @@ def _archive_metadata_error(
|
|
|
487
581
|
return None
|
|
488
582
|
if (phase, status) != ("shipped", "done"):
|
|
489
583
|
return "archive undo requires ship/active or shipped/done"
|
|
584
|
+
if ship_events is None:
|
|
585
|
+
return "archive undo found unowned shipment STATE.md changes"
|
|
586
|
+
prepared_state += ship_events
|
|
490
587
|
|
|
491
588
|
prepared = _state_from_text(prepared_state, "prepared archive STATE.md")
|
|
492
|
-
events = (
|
|
493
|
-
"archive preflight passed; shipment recorded",
|
|
494
|
-
"archive preflight passed; shipment recorded; program complete",
|
|
495
|
-
)
|
|
496
589
|
rendered_states = []
|
|
497
|
-
for event in
|
|
590
|
+
for event in SHIPMENT_EVENTS:
|
|
498
591
|
try:
|
|
499
592
|
_, _, rendered = _render_transition(
|
|
500
593
|
prepared,
|
|
@@ -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
|