@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
|
@@ -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")
|
|
@@ -255,6 +255,7 @@ def check_member(
|
|
|
255
255
|
coordinator_name: str,
|
|
256
256
|
checkout: Path,
|
|
257
257
|
recorded_remote: Optional[str] = None,
|
|
258
|
+
refuse_reserved_refs: bool = True,
|
|
258
259
|
) -> str:
|
|
259
260
|
"""Refuse a checkout that cannot join; return its origin URL."""
|
|
260
261
|
if checkout.is_symlink() or not checkout.is_dir():
|
|
@@ -294,13 +295,14 @@ def check_member(
|
|
|
294
295
|
raise MembersError(f"member STATE.md is unreadable: {error}") from error
|
|
295
296
|
if state.milestone is not None and (state.phase, state.status) != ("shipped", "done"):
|
|
296
297
|
raise MembersError(f"member has an active milestone: {state.milestone}")
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
298
|
+
if refuse_reserved_refs:
|
|
299
|
+
prefixes = member_ref_prefixes(coordinator_name) + tuple(
|
|
300
|
+
"refs/remotes/origin/" + branch.format(coordinator_name) for branch in MEMBER_BRANCH_FORMATS
|
|
301
|
+
)
|
|
302
|
+
refs = _git(checkout, "for-each-ref", "--format=%(refname)").splitlines()
|
|
303
|
+
colliding = [ref for ref in refs if ref.startswith(prefixes)]
|
|
304
|
+
if colliding:
|
|
305
|
+
raise MembersError("member refs collide with coordinator names: " + ", ".join(colliding))
|
|
304
306
|
return remote
|
|
305
307
|
|
|
306
308
|
|
|
@@ -620,7 +622,7 @@ def validate_members(repo: Path) -> list[dict[str, str]]:
|
|
|
620
622
|
members = read_members(root)
|
|
621
623
|
for member in members:
|
|
622
624
|
checkout = Path(member["checkout"])
|
|
623
|
-
check_member(root, state.project, checkout, member["remote"])
|
|
625
|
+
check_member(root, state.project, checkout, member["remote"], refuse_reserved_refs=False)
|
|
624
626
|
try:
|
|
625
627
|
role = member_role(checkout)
|
|
626
628
|
except MembersError as error:
|
|
@@ -639,7 +641,7 @@ def repair_members(repo: Path) -> list[dict[str, str]]:
|
|
|
639
641
|
members = read_members(root)
|
|
640
642
|
for member in members:
|
|
641
643
|
checkout = Path(member["checkout"])
|
|
642
|
-
check_member(root, state.project, checkout, member["remote"])
|
|
644
|
+
check_member(root, state.project, checkout, member["remote"], refuse_reserved_refs=False)
|
|
643
645
|
_refuse_foreign_marker(checkout.resolve(), root, state.project, member["name"])
|
|
644
646
|
for member in members:
|
|
645
647
|
checkout = Path(member["checkout"]).resolve()
|
|
@@ -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')
|
|
@@ -146,6 +146,29 @@ def ship_commit_body(archive_path: str, reviewed_head: str, members: Sequence[di
|
|
|
146
146
|
)
|
|
147
147
|
|
|
148
148
|
|
|
149
|
+
_TRAILER_LINE = re.compile(r"^[A-Za-z-]+:\s")
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def ship_body_matches(body: str, expected_body: str) -> bool:
|
|
153
|
+
"""True when body equals the contract or adds only optional git trailers after it."""
|
|
154
|
+
normalized = body.rstrip("\n")
|
|
155
|
+
expected = expected_body.rstrip("\n")
|
|
156
|
+
if normalized == expected:
|
|
157
|
+
return True
|
|
158
|
+
prefix = expected + "\n"
|
|
159
|
+
if not normalized.startswith(prefix):
|
|
160
|
+
return False
|
|
161
|
+
remainder = normalized[len(prefix):]
|
|
162
|
+
if not remainder:
|
|
163
|
+
return True
|
|
164
|
+
for line in remainder.splitlines():
|
|
165
|
+
if not line.strip():
|
|
166
|
+
return False
|
|
167
|
+
if not _TRAILER_LINE.match(line):
|
|
168
|
+
return False
|
|
169
|
+
return True
|
|
170
|
+
|
|
171
|
+
|
|
149
172
|
def integrate_commit_body(
|
|
150
173
|
archive_path: str,
|
|
151
174
|
ship_commit: str,
|
|
@@ -270,22 +293,50 @@ def _symbolic_branch(repo: Path) -> str:
|
|
|
270
293
|
return result.stdout.strip()
|
|
271
294
|
|
|
272
295
|
|
|
273
|
-
def
|
|
274
|
-
"""Return
|
|
275
|
-
|
|
296
|
+
def worktree_list_porcelain(repo: Path) -> tuple[str, bool]:
|
|
297
|
+
"""Return worktree list output and whether it is NUL-separated."""
|
|
298
|
+
result = _run_git(repo, "worktree", "list", "--porcelain", "-z", check=False)
|
|
299
|
+
if result.returncode == 0:
|
|
300
|
+
return result.stdout, True
|
|
301
|
+
result = _run_git(repo, "worktree", "list", "--porcelain", check=False)
|
|
302
|
+
if result.returncode != 0:
|
|
303
|
+
message = (result.stderr or result.stdout).strip() or "could not list worktrees"
|
|
304
|
+
raise PipelineGitError(message)
|
|
305
|
+
return result.stdout, False
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
def branch_worktrees_from_porcelain(output: str, *, nul_separated: bool) -> dict[str, Path]:
|
|
276
309
|
worktrees: dict[str, Path] = {}
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
310
|
+
if nul_separated:
|
|
311
|
+
current_path: Optional[Path] = None
|
|
312
|
+
for field in output.split("\0"):
|
|
313
|
+
if not field:
|
|
314
|
+
continue
|
|
315
|
+
if field.startswith("worktree "):
|
|
316
|
+
current_path = Path(field.removeprefix("worktree ")).resolve()
|
|
317
|
+
elif field.startswith("branch ") and current_path is not None:
|
|
318
|
+
ref = field.removeprefix("branch ")
|
|
319
|
+
prefix = "refs/heads/"
|
|
320
|
+
if ref.startswith(prefix):
|
|
321
|
+
worktrees[ref.removeprefix(prefix)] = current_path
|
|
322
|
+
return worktrees
|
|
323
|
+
for block in output.strip().split("\n\n"):
|
|
324
|
+
fields = block.splitlines()
|
|
325
|
+
if not fields or not fields[0].startswith("worktree "):
|
|
326
|
+
continue
|
|
327
|
+
current_path = Path(fields[0].removeprefix("worktree ")).resolve()
|
|
328
|
+
for line in fields[1:]:
|
|
329
|
+
if line.startswith("branch refs/heads/"):
|
|
330
|
+
worktrees[line.removeprefix("branch refs/heads/")] = current_path
|
|
286
331
|
return worktrees
|
|
287
332
|
|
|
288
333
|
|
|
334
|
+
def _branch_worktrees(repo: Path) -> dict[str, Path]:
|
|
335
|
+
"""Return every checked-out local branch from Git's worktree registry."""
|
|
336
|
+
output, nul_separated = worktree_list_porcelain(repo)
|
|
337
|
+
return branch_worktrees_from_porcelain(output, nul_separated=nul_separated)
|
|
338
|
+
|
|
339
|
+
|
|
289
340
|
def _remote_ref_sha(repo: Path, ref: str) -> Optional[str]:
|
|
290
341
|
if not _origin_remote_exists(repo):
|
|
291
342
|
raise PipelineGitError("origin remote is not configured")
|
|
@@ -91,7 +91,7 @@ STATE_FIELDS = (
|
|
|
91
91
|
"integration_source",
|
|
92
92
|
)
|
|
93
93
|
LEGACY_STATE_FIELDS = STATE_FIELDS[:7]
|
|
94
|
-
INTEGRATION_MODES = ("direct", "pull-request")
|
|
94
|
+
INTEGRATION_MODES = ("direct", "pull-request", "external-landing")
|
|
95
95
|
INTEGRATION_SOURCES = ("default", "milestone")
|
|
96
96
|
NULL = "null"
|
|
97
97
|
SLUG_RE = re.compile(r"^[a-z0-9][a-z0-9-]*$")
|
|
@@ -141,6 +141,8 @@ CROSS_PHASE_TRANSITIONS = {
|
|
|
141
141
|
("decide", "done", "plan", "active"),
|
|
142
142
|
("roadmap", "done", "define", "active"),
|
|
143
143
|
("plan", "done", "build", "active"),
|
|
144
|
+
("plan", "active", "define", "active"),
|
|
145
|
+
("plan", "blocked", "define", "active"),
|
|
144
146
|
("build", "blocked", "define", "active"),
|
|
145
147
|
("build", "blocked", "plan", "active"),
|
|
146
148
|
("build", "active", "ship", "active"),
|
|
@@ -193,11 +195,24 @@ def _run_git(
|
|
|
193
195
|
*arguments: str,
|
|
194
196
|
check: bool = True,
|
|
195
197
|
) -> subprocess.CompletedProcess[str]:
|
|
198
|
+
command = _common.git_command(repo, *arguments)
|
|
196
199
|
result = subprocess.run(
|
|
197
|
-
|
|
200
|
+
command,
|
|
198
201
|
capture_output=True,
|
|
199
202
|
encoding="utf-8", errors="replace",
|
|
200
203
|
)
|
|
204
|
+
if (
|
|
205
|
+
check
|
|
206
|
+
and result.returncode != 0
|
|
207
|
+
and arguments
|
|
208
|
+
and arguments[0] == "commit"
|
|
209
|
+
and _common.git_index_lock_retryable(result.stderr)
|
|
210
|
+
):
|
|
211
|
+
result = subprocess.run(
|
|
212
|
+
command,
|
|
213
|
+
capture_output=True,
|
|
214
|
+
encoding="utf-8", errors="replace",
|
|
215
|
+
)
|
|
201
216
|
if check and result.returncode != 0:
|
|
202
217
|
detail = result.stderr.strip() or result.stdout.strip()
|
|
203
218
|
raise PipelineStateError(f"git {' '.join(arguments)} failed: {detail}")
|
|
@@ -911,36 +926,13 @@ def _legacy_bind_next_landing(repo: Path, state: PipelineState) -> str:
|
|
|
911
926
|
return landing
|
|
912
927
|
|
|
913
928
|
|
|
914
|
-
def
|
|
929
|
+
def _bind_next_journal_recovery(
|
|
915
930
|
repo: Path,
|
|
916
931
|
state: PipelineState,
|
|
932
|
+
path: Path,
|
|
933
|
+
transaction: dict[str, object],
|
|
934
|
+
current: Optional[str],
|
|
917
935
|
) -> Optional[dict[str, object]]:
|
|
918
|
-
journal_root = _git_path(repo, BIND_NEXT_JOURNAL_DIR)
|
|
919
|
-
if not journal_root.exists() and not journal_root.is_symlink():
|
|
920
|
-
return None
|
|
921
|
-
if journal_root.is_symlink() or not journal_root.is_dir():
|
|
922
|
-
raise PipelineStateError(f"bind-next journal root must be a real directory: {journal_root}")
|
|
923
|
-
|
|
924
|
-
matches: list[tuple[Path, dict[str, object]]] = []
|
|
925
|
-
for path in sorted(journal_root.glob("*.json")):
|
|
926
|
-
transaction = _read_json(path)
|
|
927
|
-
if transaction.get("schema") not in {
|
|
928
|
-
LEGACY_BIND_NEXT_JOURNAL_SCHEMA,
|
|
929
|
-
BIND_NEXT_JOURNAL_SCHEMA,
|
|
930
|
-
}:
|
|
931
|
-
raise PipelineStateError(f"bind-next journal has invalid schema: {path}")
|
|
932
|
-
if transaction.get("repo") != str(repo):
|
|
933
|
-
raise PipelineStateError(f"bind-next journal belongs to another worktree: {path}")
|
|
934
|
-
if transaction.get("previous_branch") == state.branch:
|
|
935
|
-
matches.append((path, transaction))
|
|
936
|
-
if not matches:
|
|
937
|
-
return None
|
|
938
|
-
if len(matches) != 1:
|
|
939
|
-
raise PipelineStateError("multiple bind-next journals claim the current STATE branch")
|
|
940
|
-
if state.phase != "shipped" or state.status != "done":
|
|
941
|
-
raise PipelineStateError("bind-next journal requires STATE shipped/done")
|
|
942
|
-
|
|
943
|
-
path, transaction = matches[0]
|
|
944
936
|
required = (
|
|
945
937
|
"branch",
|
|
946
938
|
"previous_branch",
|
|
@@ -1022,7 +1014,32 @@ def _bind_next_recovery(
|
|
|
1022
1014
|
if stage not in {"prepared", "switched", "retired"}:
|
|
1023
1015
|
raise PipelineStateError("bind-next journal has invalid stage")
|
|
1024
1016
|
|
|
1025
|
-
|
|
1017
|
+
stale_retired = False
|
|
1018
|
+
if stage == "retired":
|
|
1019
|
+
target_ref = _run_git(
|
|
1020
|
+
repo,
|
|
1021
|
+
"show-ref",
|
|
1022
|
+
"--verify",
|
|
1023
|
+
"--quiet",
|
|
1024
|
+
f"refs/heads/{branch}",
|
|
1025
|
+
check=False,
|
|
1026
|
+
)
|
|
1027
|
+
if target_ref.returncode not in {0, 1}:
|
|
1028
|
+
detail = (target_ref.stderr or target_ref.stdout).strip()
|
|
1029
|
+
raise PipelineStateError(
|
|
1030
|
+
f"could not check bind-next target branch {branch}: {detail}"
|
|
1031
|
+
)
|
|
1032
|
+
target_exists = target_ref.returncode == 0
|
|
1033
|
+
stale_retired = current not in {previous, branch} or not target_exists
|
|
1034
|
+
if stale_retired:
|
|
1035
|
+
# If the completed target is gone while the previous branch is checked
|
|
1036
|
+
# out, still prove that branch has not moved away from the recorded ship.
|
|
1037
|
+
if current == previous:
|
|
1038
|
+
head = _run_git(repo, "rev-parse", "HEAD").stdout.strip()
|
|
1039
|
+
if head != ship:
|
|
1040
|
+
raise PipelineStateError("bind-next previous branch is not at journal ship")
|
|
1041
|
+
return None
|
|
1042
|
+
|
|
1026
1043
|
if current not in {previous, branch}:
|
|
1027
1044
|
raise PipelineStateError("bind-next journal does not own the current branch")
|
|
1028
1045
|
if current == previous and stage != "prepared":
|
|
@@ -1057,6 +1074,52 @@ def _bind_next_recovery(
|
|
|
1057
1074
|
return result
|
|
1058
1075
|
|
|
1059
1076
|
|
|
1077
|
+
def _bind_next_recovery(
|
|
1078
|
+
repo: Path,
|
|
1079
|
+
state: PipelineState,
|
|
1080
|
+
) -> Optional[dict[str, object]]:
|
|
1081
|
+
journal_root = _git_path(repo, BIND_NEXT_JOURNAL_DIR)
|
|
1082
|
+
if not journal_root.exists() and not journal_root.is_symlink():
|
|
1083
|
+
return None
|
|
1084
|
+
if journal_root.is_symlink() or not journal_root.is_dir():
|
|
1085
|
+
raise PipelineStateError(f"bind-next journal root must be a real directory: {journal_root}")
|
|
1086
|
+
|
|
1087
|
+
matches: list[tuple[Path, dict[str, object]]] = []
|
|
1088
|
+
for path in sorted(journal_root.glob("*.json")):
|
|
1089
|
+
transaction = _read_json(path)
|
|
1090
|
+
if transaction.get("schema") not in {
|
|
1091
|
+
LEGACY_BIND_NEXT_JOURNAL_SCHEMA,
|
|
1092
|
+
BIND_NEXT_JOURNAL_SCHEMA,
|
|
1093
|
+
}:
|
|
1094
|
+
raise PipelineStateError(f"bind-next journal has invalid schema: {path}")
|
|
1095
|
+
if transaction.get("repo") != str(repo):
|
|
1096
|
+
raise PipelineStateError(f"bind-next journal belongs to another worktree: {path}")
|
|
1097
|
+
if transaction.get("previous_branch") == state.branch:
|
|
1098
|
+
matches.append((path, transaction))
|
|
1099
|
+
if not matches:
|
|
1100
|
+
return None
|
|
1101
|
+
if state.phase != "shipped" or state.status != "done":
|
|
1102
|
+
if len(matches) != 1:
|
|
1103
|
+
raise PipelineStateError("multiple bind-next journals claim the current STATE branch")
|
|
1104
|
+
raise PipelineStateError("bind-next journal requires STATE shipped/done")
|
|
1105
|
+
|
|
1106
|
+
current = _current_branch(repo)
|
|
1107
|
+
recoveries: list[dict[str, object]] = []
|
|
1108
|
+
for path, transaction in matches:
|
|
1109
|
+
recovery = _bind_next_journal_recovery(
|
|
1110
|
+
repo,
|
|
1111
|
+
state,
|
|
1112
|
+
path,
|
|
1113
|
+
transaction,
|
|
1114
|
+
current,
|
|
1115
|
+
)
|
|
1116
|
+
if recovery is not None:
|
|
1117
|
+
recoveries.append(recovery)
|
|
1118
|
+
if len(recoveries) > 1:
|
|
1119
|
+
raise PipelineStateError("multiple bind-next journals claim the current STATE branch")
|
|
1120
|
+
return recoveries[0] if recoveries else None
|
|
1121
|
+
|
|
1122
|
+
|
|
1060
1123
|
def _pending_discussion_block(repo: Path, phase: Optional[str]) -> Optional[str]:
|
|
1061
1124
|
pending, error = _pending_answers(repo)
|
|
1062
1125
|
if error:
|
|
@@ -1077,7 +1140,7 @@ def route_state(repo: Path, project_dir: str = ".project") -> dict[str, object]:
|
|
|
1077
1140
|
result["recovery"] = recovery
|
|
1078
1141
|
if route.get("phase") == "define":
|
|
1079
1142
|
route["mode"] = "corrections"
|
|
1080
|
-
elif route.get("phase") == "plan":
|
|
1143
|
+
elif route.get("phase") == "plan" and recovery.get("source", "build") == "build":
|
|
1081
1144
|
route["mode"] = "build-repair"
|
|
1082
1145
|
if route["action"] == "run-phase" and result["state"]["archive"] is None:
|
|
1083
1146
|
reason = _pending_discussion_block(_repo_root(repo), route.get("phase"))
|
|
@@ -1668,6 +1731,8 @@ def _validate_transition(
|
|
|
1668
1731
|
},
|
|
1669
1732
|
("define", "done", "plan", "active"): {"planning started"},
|
|
1670
1733
|
("decide", "done", "plan", "active"): {"planning started"},
|
|
1734
|
+
("plan", "active", "define", "active"): {"plan intent corrections requested"},
|
|
1735
|
+
("plan", "blocked", "define", "active"): {"plan intent corrections requested"},
|
|
1671
1736
|
("ship", "blocked", "plan", "active"): {"patch plan reopened"},
|
|
1672
1737
|
("ship", "active", "shipped", "done"): {
|
|
1673
1738
|
"archive preflight passed; shipment recorded",
|
|
@@ -1720,9 +1785,10 @@ def transition_state(
|
|
|
1720
1785
|
)
|
|
1721
1786
|
if reason:
|
|
1722
1787
|
raise PipelineStateError(reason)
|
|
1723
|
-
|
|
1788
|
+
leaving_define = (state.phase, after.phase, after.status) == ("define", "define", "done")
|
|
1789
|
+
if leaving_define or (state.phase in {"research", "decide"} and (
|
|
1724
1790
|
after.phase != state.phase or after.status == "done"
|
|
1725
|
-
):
|
|
1791
|
+
)):
|
|
1726
1792
|
if __package__:
|
|
1727
1793
|
from . import check_handoffs
|
|
1728
1794
|
else:
|
|
@@ -1731,6 +1797,8 @@ def transition_state(
|
|
|
1731
1797
|
except ImportError: # pragma: no cover - package imports used by tests
|
|
1732
1798
|
from scripts import check_handoffs
|
|
1733
1799
|
validator = {
|
|
1800
|
+
# Approved intent carries its spec-reach probe tables complete.
|
|
1801
|
+
"define": check_handoffs.validate_intent_probes,
|
|
1734
1802
|
"research": check_handoffs.validate_research_artifacts,
|
|
1735
1803
|
"decide": check_handoffs.validate_decide_artifacts,
|
|
1736
1804
|
}[state.phase]
|
|
@@ -1747,6 +1815,16 @@ def transition_state(
|
|
|
1747
1815
|
rendered, after.phase,
|
|
1748
1816
|
_build_recovery().MARKER + json.dumps(recovery, sort_keys=True),
|
|
1749
1817
|
)
|
|
1818
|
+
elif (
|
|
1819
|
+
state.phase == "plan"
|
|
1820
|
+
and state.status in {"active", "blocked"}
|
|
1821
|
+
and (after.phase, after.status) == ("define", "active")
|
|
1822
|
+
):
|
|
1823
|
+
recovery = _build_recovery().begin(resolved, state, after, event)
|
|
1824
|
+
rendered = _append_event(
|
|
1825
|
+
rendered, after.phase,
|
|
1826
|
+
_build_recovery().MARKER + json.dumps(recovery, sort_keys=True),
|
|
1827
|
+
)
|
|
1750
1828
|
_atomic_write(path, rendered)
|
|
1751
1829
|
return {
|
|
1752
1830
|
"schema": STATE_SCHEMA,
|
|
@@ -18,14 +18,14 @@ from pathlib import Path, PurePosixPath
|
|
|
18
18
|
from typing import Mapping, Optional, Sequence
|
|
19
19
|
|
|
20
20
|
if __package__:
|
|
21
|
-
from .isolation import IsolationError, checkpoint as isolation_checkpoint,
|
|
21
|
+
from .isolation import IsolationError, checkpoint as isolation_checkpoint, PROJECT_ENTRIES
|
|
22
22
|
else:
|
|
23
23
|
try:
|
|
24
|
-
from isolation import IsolationError, checkpoint as isolation_checkpoint,
|
|
24
|
+
from isolation import IsolationError, checkpoint as isolation_checkpoint, PROJECT_ENTRIES
|
|
25
25
|
except ModuleNotFoundError as error: # pragma: no cover - package imports used by tests
|
|
26
26
|
if error.name != "isolation":
|
|
27
27
|
raise
|
|
28
|
-
from scripts.isolation import IsolationError, checkpoint as isolation_checkpoint,
|
|
28
|
+
from scripts.isolation import IsolationError, checkpoint as isolation_checkpoint, PROJECT_ENTRIES
|
|
29
29
|
|
|
30
30
|
|
|
31
31
|
if __package__:
|
|
@@ -444,14 +444,14 @@ def _validate_plan_briefs(repo: Path, kind: str, project_dir: str) -> None:
|
|
|
444
444
|
# Pre-Git approval defers base-dependent checks to build.
|
|
445
445
|
return
|
|
446
446
|
if __package__:
|
|
447
|
-
from .check_task_briefs import BriefError, _member_bases, validate_task_briefs
|
|
447
|
+
from .check_task_briefs import BriefError, _member_bases, plan_validation_bases, validate_task_briefs
|
|
448
448
|
else:
|
|
449
449
|
try:
|
|
450
|
-
from check_task_briefs import BriefError, _member_bases, validate_task_briefs
|
|
450
|
+
from check_task_briefs import BriefError, _member_bases, plan_validation_bases, validate_task_briefs
|
|
451
451
|
except ModuleNotFoundError as error: # pragma: no cover - package imports
|
|
452
452
|
if error.name != "check_task_briefs":
|
|
453
453
|
raise
|
|
454
|
-
from scripts.check_task_briefs import BriefError, _member_bases, validate_task_briefs
|
|
454
|
+
from scripts.check_task_briefs import BriefError, _member_bases, plan_validation_bases, validate_task_briefs
|
|
455
455
|
if __package__:
|
|
456
456
|
from . import check_handoffs
|
|
457
457
|
else:
|
|
@@ -462,28 +462,10 @@ def _validate_plan_briefs(repo: Path, kind: str, project_dir: str) -> None:
|
|
|
462
462
|
raise
|
|
463
463
|
from scripts import check_handoffs
|
|
464
464
|
try:
|
|
465
|
-
tasks, dependency_files = check_handoffs.plan_brief_inputs(repo, project_dir)
|
|
466
|
-
landed_bases = {}
|
|
467
465
|
member_base = _member_bases(repo)
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
agent = check_handoffs._task_scalar(text, task_id, "agent")
|
|
472
|
-
if agent in {"", "null"}:
|
|
473
|
-
raise BriefError(f"{task_id} landed task has no recorded agent")
|
|
474
|
-
member = check_handoffs._strict_frontmatter(text, task_id).get("repo")
|
|
475
|
-
base_repo = repo
|
|
476
|
-
if member is not None:
|
|
477
|
-
located = member_base(member) if isinstance(member, str) and member else None
|
|
478
|
-
if located is None:
|
|
479
|
-
raise BriefError(f"repo: names no member in MEMBERS.md: {member}")
|
|
480
|
-
base_repo = located[0]
|
|
481
|
-
recorded_base = check_handoffs._task_scalar(text, task_id, "base")
|
|
482
|
-
try:
|
|
483
|
-
landed_bases[task_id] = require_commit(base_repo, require_full_sha(recorded_base))
|
|
484
|
-
except IsolationError as error:
|
|
485
|
-
raise BriefError(f"{task_id} landed task has invalid historical base: {error}") from error
|
|
486
|
-
dependency_files[task_id] = set()
|
|
466
|
+
tasks, dependency_files, landed_bases = plan_validation_bases(
|
|
467
|
+
repo, project_dir, member_base, initial=True
|
|
468
|
+
)
|
|
487
469
|
validate_task_briefs(
|
|
488
470
|
repo, head.stdout.strip(), f"{project_dir}/tasks",
|
|
489
471
|
dependency_files=dependency_files, landed_bases=landed_bases,
|
|
@@ -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:
|
|
@@ -100,6 +100,10 @@ Obtain a confident answer in every area before finishing:
|
|
|
100
100
|
give every named surface at least one success criterion observable
|
|
101
101
|
there. A criterion satisfied by a passing test alone does not cover a
|
|
102
102
|
surface.
|
|
103
|
+
9. **Edges and prohibitions** — once the success criteria are drafted, run
|
|
104
|
+
the [spec-reach probes](references/spec-probes.md): every applicable edge
|
|
105
|
+
and every kept prohibition gets the user's ruling in INTENT.md
|
|
106
|
+
`## Edge coverage` and `## Prohibitions`.
|
|
103
107
|
|
|
104
108
|
## Program mode
|
|
105
109
|
|
|
@@ -149,7 +153,10 @@ State ownership. There is no re-interview of charter or roadmap scope:
|
|
|
149
153
|
`Lane: milestone`, `Surfaces:` copied verbatim from the entry, and
|
|
150
154
|
`Review panel:` copied from CHARTER.md (default
|
|
151
155
|
the configured preference when CHARTER omits it). The confirmation may override the copied
|
|
152
|
-
panel value; do not invent `detected` or a named list.
|
|
156
|
+
panel value; do not invent `detected` or a named list. Run the
|
|
157
|
+
[spec-reach probes](references/spec-probes.md) over the derived criteria
|
|
158
|
+
and draft `## Edge coverage` and `## Prohibitions` with recommended
|
|
159
|
+
rulings. When brownfield,
|
|
153
160
|
fill `## Current state` from the map, record doc-vs-code rulings as in
|
|
154
161
|
Brownfield mode, and add protected existing behavior under Scope out.
|
|
155
162
|
Write Ground truth paths from the track root: `.project` normally, or
|
|
@@ -157,7 +164,9 @@ State ownership. There is no re-interview of charter or roadmap scope:
|
|
|
157
164
|
active `research/` paths.
|
|
158
165
|
3. Present one confirmation, not an interview: playback the derivation (and
|
|
159
166
|
brownfield ground truth when present), link the resolved absolute INTENT.md
|
|
160
|
-
path, and ask approve or adjust.
|
|
167
|
+
path, and ask approve or adjust. The confirmation lists each proposed edge
|
|
168
|
+
and prohibition ruling for the user to accept or change; it never rules on
|
|
169
|
+
them silently. A requested change that contradicts the
|
|
161
170
|
charter or the resolved roadmap entry is a scope change — this phase never
|
|
162
171
|
edits the roadmap; surface it to the user for a `$gsd-path-roadmap` re-slice
|
|
163
172
|
at the next milestone boundary.
|
|
@@ -254,7 +263,15 @@ question. The rules change for every primary mode:
|
|
|
254
263
|
the brownfield map already established. Milestone mode skips this step.
|
|
255
264
|
5. Stop when coverage is complete or the user says `enough`. Record remaining
|
|
256
265
|
uncertainty under `## Open questions` with `RESEARCH` or `NEEDS-USER`.
|
|
257
|
-
|
|
266
|
+
`enough` does not skip the next step.
|
|
267
|
+
6. In standard mode, run the [spec-reach probes](references/spec-probes.md)
|
|
268
|
+
over the drafted success criteria: one question round for edges, then one
|
|
269
|
+
for kept prohibitions, each with a recommended ruling first. A ruling
|
|
270
|
+
observable where the user sees the work becomes or amends a success
|
|
271
|
+
criterion; write every ruling into `## Edge coverage` and
|
|
272
|
+
`## Prohibitions`. Program mode skips this step (each milestone runs it
|
|
273
|
+
in milestone mode); milestone mode runs it inside its confirmation.
|
|
274
|
+
7. Classify the proposed milestone lane before writing the approval draft.
|
|
258
275
|
Milestone mode keeps `Lane: milestone` and skips this classification.
|
|
259
276
|
Program mode writes CHARTER.md instead of INTENT.md. Otherwise choose
|
|
260
277
|
`quick` when scope fits at most two deliverable-sized tasks (project
|
|
@@ -275,6 +292,11 @@ question. The rules change for every primary mode:
|
|
|
275
292
|
## Output contract
|
|
276
293
|
|
|
277
294
|
At approval, retain the already reviewed `Lane:` value and its one-line reason.
|
|
295
|
+
Before every approval question on INTENT.md, run the bundled `python3
|
|
296
|
+
<absolute-bundled-script> intent --repo <absolute-root>` helper
|
|
297
|
+
(`scripts/check_handoffs.py`, plus `--project-dir .project/next` in
|
|
298
|
+
lookahead). It checks the success criteria, Surfaces, and both probe tables;
|
|
299
|
+
fix a failure in the draft before asking. It approves nothing.
|
|
278
300
|
Before presenting an approval draft without a CHARTER or explicit panel choice,
|
|
279
301
|
read `settings.review_panel.value` from the selected runtime's `path_config.py
|
|
280
302
|
show --repo <absolute-root>` (bundle for legacy projects; off for older selected
|
|
@@ -302,5 +324,8 @@ Use the executable phase handoff in AGENTS.md for the next action.
|
|
|
302
324
|
- In brownfield mode, state mapped facts for correction; never re-ask them.
|
|
303
325
|
- Treat solution-shaped problems as hypotheses and uncover the underlying pain.
|
|
304
326
|
- Preserve vetoes and corrections verbatim.
|
|
327
|
+
- Never rule on an edge's behavior or a prohibition yourself; recommend,
|
|
328
|
+
then record the user's ruling. An edge the user has not ruled on is a
|
|
329
|
+
required answer, including under pre-approval.
|
|
305
330
|
- Wrap up when every coverage-checklist area is answered or the user signals
|
|
306
331
|
done, and tag remaining gaps.
|