@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:
|
|
@@ -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')
|
|
@@ -115,9 +115,10 @@ as stated in Lookahead mode.
|
|
|
115
115
|
In patch mode, tasks already `done` keep their landed metadata; only
|
|
116
116
|
the appended patch tasks must start clean.
|
|
117
117
|
- Require every dependency id to exist, forbid later-wave dependencies,
|
|
118
|
-
detect cycles, and forbid file overlap between planned tasks
|
|
119
|
-
wave. Same-wave
|
|
120
|
-
|
|
118
|
+
detect cycles, and forbid unordered file overlap between planned tasks
|
|
119
|
+
in the same wave. Same-wave tasks may share files only when one
|
|
120
|
+
transitively depends on the other through `deps`; the build executes
|
|
121
|
+
them in dependency layers.
|
|
121
122
|
- Reject a declared dependency that carries neither data nor a prerequisite
|
|
122
123
|
effect. A `deps` edge is valid only when the dependent consumes a symbol,
|
|
123
124
|
signature, schema, endpoint, file format, or path named in that
|
|
@@ -157,6 +158,13 @@ as stated in Lookahead mode.
|
|
|
157
158
|
command's output — not an internal unit test.
|
|
158
159
|
Require the surface to land in the same wave as the capability behind
|
|
159
160
|
it, never in a later polish wave.
|
|
161
|
+
- When INTENT.md has `## Edge coverage`, require a PLAN.md
|
|
162
|
+
`## Held-out checks` row for every `held-out` edge and for no other
|
|
163
|
+
edge. The named test is in that task's `files`, the task's Verify runs
|
|
164
|
+
it, and the task's Acceptance criteria state the edge id and its ruling
|
|
165
|
+
(see [spec-reach probes](references/spec-probes.md)). An edge or
|
|
166
|
+
prohibition ruled `criterion SCn` needs no extra row; it is covered as
|
|
167
|
+
SCn. Put each `judgment` prohibition in its owning task's Approach.
|
|
160
168
|
- Prove every intent constraint and synthesis decision is covered, that no
|
|
161
169
|
scope-out veto appears in a task, and that `Project verify` is a real,
|
|
162
170
|
non-placeholder command in PLAN.md. Every Verify — task and project —
|
|
@@ -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
|
|
|
@@ -38,8 +38,9 @@ the how.
|
|
|
38
38
|
security-critical; `verify-only` suits polish and low-risk feature waves
|
|
39
39
|
whose Verify commands meaningfully cover the criteria.
|
|
40
40
|
|
|
41
|
-
Every dependency must be in an earlier wave or in the same wave
|
|
42
|
-
|
|
41
|
+
Every dependency must be in an earlier wave or in the same wave. Same-wave
|
|
42
|
+
tasks may share files only when one transitively depends on the other. Produce
|
|
43
|
+
an acyclic graph; same-wave chains execute in layers.
|
|
43
44
|
|
|
44
45
|
Declare a dependency only when something real crosses it: data — the dependent
|
|
45
46
|
consumes a symbol, signature, schema, endpoint, file format, or path named in
|
|
@@ -72,8 +73,17 @@ plan order above, which still keeps plan-invalidating risk in wave 1.
|
|
|
72
73
|
criterion may appear in only one surface block. Put the surface in the same
|
|
73
74
|
wave as the capability behind it. A milestone with a surface is not done
|
|
74
75
|
when only its internals run.
|
|
76
|
+
- When INTENT.md carries `## Edge coverage` or `## Prohibitions` (read the
|
|
77
|
+
supplied [spec-reach probes](spec-probes.md)), treat every ruling as a hard
|
|
78
|
+
limit on its criterion. For each `held-out` edge, write one PLAN.md
|
|
79
|
+
`## Held-out checks` row: a test file in the owning task's `files`, a
|
|
80
|
+
Verify that runs it, and an Acceptance criterion that names the edge id
|
|
81
|
+
and states its ruling from Detail. Omit the section when no edge is
|
|
82
|
+
held-out. Put each `judgment` prohibition's Must not and Detail in the
|
|
83
|
+
owning task's Approach.
|
|
75
84
|
- Make `files` exhaustive, including imports, routes, generated artifacts,
|
|
76
|
-
tests, and wiring. Require disjoint files for same-wave tasks
|
|
85
|
+
tests, and wiring. Require disjoint files for same-wave tasks that are not
|
|
86
|
+
dependency-ordered.
|
|
77
87
|
- Write an Interface contract in every task: `None` for independent tasks;
|
|
78
88
|
for any tasks that exchange a symbol, signature, schema, endpoint, file
|
|
79
89
|
format, or path, the exact shapes both sides code against — identical text
|
|
@@ -105,7 +115,7 @@ family list. Copy `Finding skeptics:` from INTENT.md into PLAN.md Config as
|
|
|
105
115
|
invent `on`. Quick-lane plans always write both values as `off`.
|
|
106
116
|
|
|
107
117
|
Before returning, check veto exclusion, decision coverage, Intent coverage,
|
|
108
|
-
the Surface contract, dependencies, file overlap, task size, criteria, and
|
|
118
|
+
the Surface contract, Held-out checks, dependencies, file overlap, task size, criteria, and
|
|
109
119
|
Verify commands.
|
|
110
120
|
|
|
111
121
|
Return a brief structured summary: wave count, task count, and the wave-1
|