@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
package/scripts/isolation.py
CHANGED
|
@@ -15,6 +15,7 @@ import hashlib
|
|
|
15
15
|
import io
|
|
16
16
|
import json
|
|
17
17
|
import shlex
|
|
18
|
+
import shutil
|
|
18
19
|
import os
|
|
19
20
|
import re
|
|
20
21
|
import stat
|
|
@@ -27,11 +28,6 @@ import tempfile
|
|
|
27
28
|
from pathlib import Path, PurePosixPath
|
|
28
29
|
from typing import Dict, Iterator, Optional, Sequence, Set, Tuple
|
|
29
30
|
|
|
30
|
-
if sys.platform == "win32":
|
|
31
|
-
import msvcrt
|
|
32
|
-
else:
|
|
33
|
-
import fcntl
|
|
34
|
-
|
|
35
31
|
if __package__:
|
|
36
32
|
from .pipeline_git import (
|
|
37
33
|
attest_commit_body,
|
|
@@ -176,6 +172,12 @@ def sidecar_root(primary: Path, kind: str, name: str, *, pin: bool = False) -> P
|
|
|
176
172
|
raise IsolationError(str(error)) from error
|
|
177
173
|
|
|
178
174
|
|
|
175
|
+
def release_workspace_quietly(primary: Path) -> None:
|
|
176
|
+
"""Best-effort placement hygiene after a sidecar retires; never blocks landing."""
|
|
177
|
+
with contextlib.suppress(ValueError, OSError):
|
|
178
|
+
worktree_paths.release_workspace(primary)
|
|
179
|
+
|
|
180
|
+
|
|
179
181
|
def validate_task_id(task_id: str) -> str:
|
|
180
182
|
if not TASK_ID_RE.fullmatch(task_id):
|
|
181
183
|
raise IsolationError(f"invalid task id: {task_id}")
|
|
@@ -439,8 +441,9 @@ def collect_artifact_recoveries(primary: Path) -> list[Dict[str, object]]:
|
|
|
439
441
|
|
|
440
442
|
|
|
441
443
|
def _git_file(repo: Path, revision: str, path: str) -> bytes:
|
|
444
|
+
git_path = PurePosixPath(path.replace("\\", "/")).as_posix()
|
|
442
445
|
result = subprocess.run(
|
|
443
|
-
("git", "-C", str(repo), "show", f"{revision}:{
|
|
446
|
+
("git", "-C", str(repo), "show", f"{revision}:{git_path}"),
|
|
444
447
|
capture_output=True,
|
|
445
448
|
check=False,
|
|
446
449
|
)
|
|
@@ -481,6 +484,70 @@ def _same_directory(path: Path, expected: os.stat_result) -> bool:
|
|
|
481
484
|
return stat.S_ISDIR(current.st_mode) and os.path.samestat(current, expected)
|
|
482
485
|
|
|
483
486
|
|
|
487
|
+
def _long_path(absolute: str) -> str:
|
|
488
|
+
if absolute.startswith("\\\\?\\"):
|
|
489
|
+
return absolute
|
|
490
|
+
if absolute.startswith("\\\\"):
|
|
491
|
+
return "\\\\?\\UNC\\" + absolute[2:]
|
|
492
|
+
return "\\\\?\\" + absolute
|
|
493
|
+
|
|
494
|
+
|
|
495
|
+
def _force_rmtree(path: Path) -> None:
|
|
496
|
+
"""Delete a sidecar tree directly, tolerating read-only files and long paths."""
|
|
497
|
+
def _writable(function, target, _error):
|
|
498
|
+
os.chmod(target, stat.S_IWRITE)
|
|
499
|
+
function(target)
|
|
500
|
+
|
|
501
|
+
# The \\?\ prefix sidesteps Windows MAX_PATH on deep sidecar trees.
|
|
502
|
+
target = Path(_long_path(os.path.abspath(path))) if os.name == "nt" else path
|
|
503
|
+
try:
|
|
504
|
+
shutil.rmtree(target, onerror=_writable)
|
|
505
|
+
except OSError as error:
|
|
506
|
+
raise IsolationError(f"could not delete {path}: {error}") from error
|
|
507
|
+
|
|
508
|
+
|
|
509
|
+
def _remove_worktree(primary: Path, destination: Path, force: bool) -> None:
|
|
510
|
+
"""Remove a sidecar worktree, tolerating populated submodules and locked trees.
|
|
511
|
+
|
|
512
|
+
git refuses to remove worktrees whose submodules were ever initialized, and
|
|
513
|
+
forced removal can still fail deleting deep trees (Windows "Directory not
|
|
514
|
+
empty"). A dirty sidecar keeps git's refusal unless `force`, so forced
|
|
515
|
+
retries only run for a provably clean tree. The direct-delete fallback runs
|
|
516
|
+
only for git's tree-deletion failures, so any other git error leaves the
|
|
517
|
+
sidecar intact for diagnosis.
|
|
518
|
+
"""
|
|
519
|
+
tree_delete_failure = ("failed to delete", "directory not empty", "permission denied")
|
|
520
|
+
|
|
521
|
+
def failure(*flags: str) -> str:
|
|
522
|
+
result = run_git(primary, "worktree", "remove", *flags, str(destination))
|
|
523
|
+
return (result.stderr or result.stdout).strip() if result.returncode else ""
|
|
524
|
+
|
|
525
|
+
detail = failure("--force") if force else failure()
|
|
526
|
+
if not detail:
|
|
527
|
+
return
|
|
528
|
+
if not force and destination.is_dir():
|
|
529
|
+
status = run_git(
|
|
530
|
+
destination, "status", "--porcelain", "--untracked-files=all"
|
|
531
|
+
)
|
|
532
|
+
if status.returncode != 0 or status.stdout.strip():
|
|
533
|
+
raise IsolationError(detail or "git worktree remove failed")
|
|
534
|
+
run_git(destination, "submodule", "deinit", "--all", "--force")
|
|
535
|
+
detail = failure("--force")
|
|
536
|
+
if not detail:
|
|
537
|
+
return
|
|
538
|
+
if destination.is_dir() and any(
|
|
539
|
+
marker in detail.lower() for marker in tree_delete_failure
|
|
540
|
+
):
|
|
541
|
+
_force_rmtree(destination)
|
|
542
|
+
detail = failure("--force")
|
|
543
|
+
if detail and not destination.is_dir():
|
|
544
|
+
pruned = run_git(primary, "worktree", "prune")
|
|
545
|
+
if pruned.returncode == 0:
|
|
546
|
+
detail = ""
|
|
547
|
+
if detail or destination.is_dir() or destination.resolve() in _registered_worktrees(primary):
|
|
548
|
+
raise IsolationError(detail or "git worktree remove failed")
|
|
549
|
+
|
|
550
|
+
|
|
484
551
|
def _cleanup_reserved_worktree(
|
|
485
552
|
primary: Path,
|
|
486
553
|
branch: str,
|
|
@@ -758,7 +825,7 @@ def retire_member_task(
|
|
|
758
825
|
raise IsolationError(f"member task branch {ref} has unlanded commits")
|
|
759
826
|
destination = sidecar_root(checkout, "task", name)
|
|
760
827
|
copy_rejected = None
|
|
761
|
-
if force:
|
|
828
|
+
if force and task_file:
|
|
762
829
|
rejected, copy_rejected = _rejected_member_attempt_error(coordinator, member, task_id, task_file, destination)
|
|
763
830
|
if rejected:
|
|
764
831
|
raise IsolationError(rejected)
|
|
@@ -766,9 +833,7 @@ def retire_member_task(
|
|
|
766
833
|
if destination.resolve() in registered:
|
|
767
834
|
if registered[destination.resolve()] != ref:
|
|
768
835
|
raise IsolationError(f"member task sidecar is on another branch: {destination}")
|
|
769
|
-
|
|
770
|
-
if removed.returncode != 0:
|
|
771
|
-
raise IsolationError((removed.stderr or removed.stdout).strip() or "git worktree remove failed")
|
|
836
|
+
_remove_worktree(checkout, destination, force)
|
|
772
837
|
authorization = run_git(checkout, "update-ref", "-d", member_task_authorization_ref(project, task_id))
|
|
773
838
|
if authorization.returncode != 0:
|
|
774
839
|
raise IsolationError((authorization.stderr or authorization.stdout).strip() or "git update-ref failed")
|
|
@@ -776,6 +841,7 @@ def retire_member_task(
|
|
|
776
841
|
deleted = run_git(checkout, "update-ref", "-d", ref, tip.stdout.strip())
|
|
777
842
|
if deleted.returncode != 0:
|
|
778
843
|
raise IsolationError((deleted.stderr or deleted.stdout).strip() or "git update-ref failed")
|
|
844
|
+
release_workspace_quietly(checkout)
|
|
779
845
|
return copy_rejected
|
|
780
846
|
|
|
781
847
|
|
|
@@ -936,9 +1002,11 @@ def activate_member_task(
|
|
|
936
1002
|
created = run_git(checkout, "update-ref", ref, member_base, "0" * 40)
|
|
937
1003
|
if created.returncode != 0 and run_git(checkout, "rev-parse", "--verify", "--quiet", ref).stdout.strip() != member_base:
|
|
938
1004
|
raise IsolationError("could not record member task authorization")
|
|
939
|
-
|
|
1005
|
+
result = {"agent": agent, "base": resolved_base, "copy": str(copy), "member": member,
|
|
940
1006
|
"member_base": member_base, "status": "in-progress", "task_branch": branch,
|
|
941
1007
|
"task_file": task_file, "worktree": str(sidecar)}
|
|
1008
|
+
_write_native_member_dispatch_record(coordinator, result)
|
|
1009
|
+
return result
|
|
942
1010
|
|
|
943
1011
|
|
|
944
1012
|
def retained_member_task(
|
|
@@ -1050,25 +1118,8 @@ def _member_journal_path(coordinator: Path, task_id: str) -> Path:
|
|
|
1050
1118
|
@contextlib.contextmanager
|
|
1051
1119
|
def _member_landing_lock(coordinator: Path) -> Iterator[None]:
|
|
1052
1120
|
path = common_git_dir(coordinator).joinpath(*MEMBER_LANDING_DIR, ".lock")
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
if sys.platform == "win32":
|
|
1056
|
-
handle.seek(0, os.SEEK_END)
|
|
1057
|
-
if handle.tell() == 0:
|
|
1058
|
-
handle.write(b"\0")
|
|
1059
|
-
handle.flush()
|
|
1060
|
-
handle.seek(0)
|
|
1061
|
-
msvcrt.locking(handle.fileno(), msvcrt.LK_LOCK, 1)
|
|
1062
|
-
else:
|
|
1063
|
-
fcntl.flock(handle.fileno(), fcntl.LOCK_EX)
|
|
1064
|
-
try:
|
|
1065
|
-
yield
|
|
1066
|
-
finally:
|
|
1067
|
-
if sys.platform == "win32":
|
|
1068
|
-
handle.seek(0)
|
|
1069
|
-
msvcrt.locking(handle.fileno(), msvcrt.LK_UNLCK, 1)
|
|
1070
|
-
else:
|
|
1071
|
-
fcntl.flock(handle.fileno(), fcntl.LOCK_UN)
|
|
1121
|
+
with _common.exclusive_lock(path):
|
|
1122
|
+
yield
|
|
1072
1123
|
|
|
1073
1124
|
|
|
1074
1125
|
def _require_member_coordinator_branch(coordinator: Path) -> None:
|
|
@@ -1411,8 +1462,9 @@ def retire_member_verify(created: Dict[str, Dict[str, str]]) -> None:
|
|
|
1411
1462
|
"""Remove the member siblings of a coordinator verify sidecar; they hold no output."""
|
|
1412
1463
|
for record in reversed(list(created.values())):
|
|
1413
1464
|
checkout, destination, branch = _member_verify_sidecar(record)
|
|
1414
|
-
|
|
1465
|
+
_remove_worktree(checkout, destination, True)
|
|
1415
1466
|
git_output(checkout, "branch", "-D", branch)
|
|
1467
|
+
release_workspace_quietly(checkout)
|
|
1416
1468
|
|
|
1417
1469
|
|
|
1418
1470
|
def check_member_verify(created: Dict[str, Dict[str, str]]) -> None:
|
|
@@ -1444,6 +1496,89 @@ def clean_verify(primary: Path, worktree: Path, base: str, branch: str) -> Dict[
|
|
|
1444
1496
|
return {"worktree": str(worktree), "branch": branch, "base": expected, "removed": removed}
|
|
1445
1497
|
|
|
1446
1498
|
|
|
1499
|
+
def ensure_verify_sidecar(primary: Path, base: str, branch: str) -> Dict[str, object]:
|
|
1500
|
+
"""Return the named verify sidecar, recreating it when it is missing.
|
|
1501
|
+
|
|
1502
|
+
Finish needs the sidecar prepare-task created. A deleted sidecar must be
|
|
1503
|
+
rebuildable from the recorded base without a clean primary: the serial
|
|
1504
|
+
task's own product changes are exactly the tree finish reproduces next,
|
|
1505
|
+
so this must not reuse isolate_verify's clean-tree gate.
|
|
1506
|
+
"""
|
|
1507
|
+
primary = require_directory(primary, "primary worktree")
|
|
1508
|
+
if worktree_root(primary) != primary:
|
|
1509
|
+
raise IsolationError(f"primary is not its Git root: {primary}")
|
|
1510
|
+
resolved_base = require_commit(primary, require_full_sha(base))
|
|
1511
|
+
if not branch.startswith(VERIFY_BRANCH_PREFIX):
|
|
1512
|
+
raise IsolationError("verify sidecar reproduction requires a verify branch")
|
|
1513
|
+
destination = _sidecar_path_for_branch(primary, branch)
|
|
1514
|
+
ref = f"refs/heads/{branch}"
|
|
1515
|
+
registered = _registered_worktrees(primary)
|
|
1516
|
+
if destination.is_symlink() or (destination.exists() and not destination.is_dir()):
|
|
1517
|
+
raise IsolationError(f"worktree path already exists: {destination}")
|
|
1518
|
+
if destination.is_dir():
|
|
1519
|
+
try:
|
|
1520
|
+
# A leftover empty directory (a half-deleted sidecar) clears;
|
|
1521
|
+
# anything non-empty must be the sidecar itself.
|
|
1522
|
+
destination.rmdir()
|
|
1523
|
+
except OSError:
|
|
1524
|
+
try:
|
|
1525
|
+
same = (
|
|
1526
|
+
worktree_root(destination) == destination
|
|
1527
|
+
and destination.resolve() in registered
|
|
1528
|
+
and registered[destination.resolve()] == ref
|
|
1529
|
+
and common_git_dir(destination) == common_git_dir(primary)
|
|
1530
|
+
)
|
|
1531
|
+
except IsolationError:
|
|
1532
|
+
same = False
|
|
1533
|
+
if not same:
|
|
1534
|
+
raise IsolationError(
|
|
1535
|
+
f"verify sidecar ownership changed: {destination}"
|
|
1536
|
+
) from None
|
|
1537
|
+
if (
|
|
1538
|
+
require_attached(destination) != branch
|
|
1539
|
+
or current_sha(destination) != resolved_base
|
|
1540
|
+
):
|
|
1541
|
+
raise IsolationError(f"verify sidecar HEAD changed: {destination}")
|
|
1542
|
+
return {
|
|
1543
|
+
"base": resolved_base,
|
|
1544
|
+
"branch": branch,
|
|
1545
|
+
"kind": "verify",
|
|
1546
|
+
"mode": "sidecar",
|
|
1547
|
+
"worktree": str(destination),
|
|
1548
|
+
}
|
|
1549
|
+
if destination.resolve() in registered:
|
|
1550
|
+
cleared = run_git(primary, "worktree", "remove", str(destination))
|
|
1551
|
+
if cleared.returncode != 0:
|
|
1552
|
+
run_git(primary, "worktree", "prune")
|
|
1553
|
+
if destination.resolve() in _registered_worktrees(primary):
|
|
1554
|
+
raise IsolationError(
|
|
1555
|
+
(cleared.stderr or cleared.stdout).strip()
|
|
1556
|
+
or "could not clear stale worktree registration"
|
|
1557
|
+
)
|
|
1558
|
+
existing = run_git(primary, "show-ref", "--verify", "--quiet", ref)
|
|
1559
|
+
if existing.returncode == 0:
|
|
1560
|
+
deleted = run_git(primary, "update-ref", "-d", ref)
|
|
1561
|
+
if deleted.returncode != 0:
|
|
1562
|
+
raise IsolationError(
|
|
1563
|
+
(deleted.stderr or deleted.stdout).strip() or "git update-ref failed"
|
|
1564
|
+
)
|
|
1565
|
+
create_named_worktree(primary, branch, destination, resolved_base)
|
|
1566
|
+
return {
|
|
1567
|
+
"base": resolved_base,
|
|
1568
|
+
"branch": branch,
|
|
1569
|
+
"kind": "verify",
|
|
1570
|
+
"mode": "sidecar",
|
|
1571
|
+
"worktree": str(destination),
|
|
1572
|
+
}
|
|
1573
|
+
|
|
1574
|
+
|
|
1575
|
+
def reproduce_verify_sidecar(sidecar_path: Path, tree: str) -> None:
|
|
1576
|
+
"""Copy the primary's current task tree into the verify sidecar (clean_verify's inverse)."""
|
|
1577
|
+
git_output(sidecar_path, "read-tree", "-u", "--reset", tree)
|
|
1578
|
+
if git_output(sidecar_path, "write-tree") != tree:
|
|
1579
|
+
raise IsolationError("sidecar reproduction differs from the primary task diff")
|
|
1580
|
+
|
|
1581
|
+
|
|
1447
1582
|
def _split_paths(block: str) -> Set[str]:
|
|
1448
1583
|
return {line.strip() for line in block.splitlines() if line.strip()}
|
|
1449
1584
|
|
|
@@ -1451,7 +1586,7 @@ def _split_paths(block: str) -> Set[str]:
|
|
|
1451
1586
|
# Top-level .project entries the pipeline owns; ship checks filter other entries by Git visibility.
|
|
1452
1587
|
PROJECT_ENTRIES = frozenset({
|
|
1453
1588
|
"STATE.md", "LESSONS.md", "REPOSITORY.md", "MEMBERS.md", "CHARTER.md", "ROADMAP.md",
|
|
1454
|
-
"SYNTHESIS.md",
|
|
1589
|
+
"SYNTHESIS.md", "config.json", "model-policy.json",
|
|
1455
1590
|
"intent", "research", "plan", "tasks", "review", "build", "discuss", "archive", "next",
|
|
1456
1591
|
})
|
|
1457
1592
|
|
|
@@ -1556,14 +1691,24 @@ def _commit_pending(
|
|
|
1556
1691
|
|
|
1557
1692
|
|
|
1558
1693
|
def validate_allowed_changes(
|
|
1559
|
-
repo: Path,
|
|
1694
|
+
repo: Path,
|
|
1695
|
+
base: str,
|
|
1696
|
+
pending: Set[str],
|
|
1697
|
+
allowed: Set[str],
|
|
1698
|
+
tolerated: Set[str] = frozenset(),
|
|
1560
1699
|
) -> None:
|
|
1561
|
-
|
|
1700
|
+
# Tolerated paths (primary bookkeeping) may stay uncommitted; the next
|
|
1701
|
+
# `.project` checkpoint commits them.
|
|
1702
|
+
landing = pending - tolerated
|
|
1703
|
+
if not landing:
|
|
1562
1704
|
raise IsolationError("no changes to land")
|
|
1563
|
-
since_base = committed_paths_since(repo, base) |
|
|
1705
|
+
since_base = committed_paths_since(repo, base) | landing
|
|
1564
1706
|
unexpected = sorted(path for path in since_base if path not in allowed)
|
|
1565
1707
|
if unexpected:
|
|
1566
|
-
raise IsolationError(
|
|
1708
|
+
raise IsolationError(
|
|
1709
|
+
"unexpected paths: " + ", ".join(unexpected)
|
|
1710
|
+
+ "; landing allows only the task file and its declared files"
|
|
1711
|
+
)
|
|
1567
1712
|
def _clean_blob_oid(repo: Path, path: str, text: str, *, write: bool) -> str:
|
|
1568
1713
|
arguments = ["hash-object"]
|
|
1569
1714
|
if write:
|
|
@@ -1583,21 +1728,23 @@ def commit_allowed_changes(
|
|
|
1583
1728
|
subject: str,
|
|
1584
1729
|
allowed: Set[str],
|
|
1585
1730
|
body: str,
|
|
1731
|
+
tolerated: Set[str] = frozenset(),
|
|
1586
1732
|
) -> tuple[str, str, Dict[str, Optional[tuple[str, str]]]]:
|
|
1587
1733
|
require_attached(repo)
|
|
1588
1734
|
pending = uncommitted_paths(repo)
|
|
1589
|
-
validate_allowed_changes(repo, base, pending, allowed)
|
|
1735
|
+
validate_allowed_changes(repo, base, pending, allowed, tolerated)
|
|
1736
|
+
staging = pending - tolerated
|
|
1590
1737
|
reset = run_git(repo, "reset", "-q", "HEAD")
|
|
1591
1738
|
if reset.returncode != 0:
|
|
1592
1739
|
raise IsolationError("could not clear the index before landing")
|
|
1593
|
-
for path in sorted(
|
|
1740
|
+
for path in sorted(staging):
|
|
1594
1741
|
added = run_git(repo, "add", "-A", "--", path)
|
|
1595
1742
|
if added.returncode != 0:
|
|
1596
1743
|
raise IsolationError(f"could not stage {path}")
|
|
1597
1744
|
staged = _split_paths(
|
|
1598
1745
|
git_output(repo, "diff", "--cached", "--name-only", "--relative")
|
|
1599
1746
|
)
|
|
1600
|
-
if staged !=
|
|
1747
|
+
if staged != staging:
|
|
1601
1748
|
raise IsolationError("staged paths do not match the uncommitted change set")
|
|
1602
1749
|
verified_tree = git_output(repo, "write-tree")
|
|
1603
1750
|
verified_delta = _tree_delta(repo, base, verified_tree)
|
|
@@ -1743,6 +1890,17 @@ def activate_task(
|
|
|
1743
1890
|
if task_branch is None:
|
|
1744
1891
|
if branch.startswith(TASK_BRANCH_PREFIX):
|
|
1745
1892
|
raise IsolationError("parallel task activation requires --task-branch")
|
|
1893
|
+
live = [
|
|
1894
|
+
path
|
|
1895
|
+
for path, ref in _registered_worktrees(worktree).items()
|
|
1896
|
+
if ref == f"refs/heads/{task_branch_name(task_id)}"
|
|
1897
|
+
and path != worktree
|
|
1898
|
+
]
|
|
1899
|
+
if live:
|
|
1900
|
+
raise IsolationError(
|
|
1901
|
+
"task has an active parallel worktree; serial activation in "
|
|
1902
|
+
f"another worktree is refused (activate {live[0]} with --task-branch)"
|
|
1903
|
+
)
|
|
1746
1904
|
elif task_branch != task_branch_name(task_id) or branch != task_branch:
|
|
1747
1905
|
raise IsolationError("task activation branch does not match the task worktree")
|
|
1748
1906
|
normalized_task = relative_posix(task_file)
|
|
@@ -1773,7 +1931,7 @@ def activate_task(
|
|
|
1773
1931
|
if existing.returncode != 0 or existing.stdout.strip() != resolved_base:
|
|
1774
1932
|
_replace_regular_file(task_path, text.encode("utf-8"), mode)
|
|
1775
1933
|
raise IsolationError("could not record task worktree authorization")
|
|
1776
|
-
|
|
1934
|
+
result = {
|
|
1777
1935
|
"agent": agent,
|
|
1778
1936
|
"base": resolved_base,
|
|
1779
1937
|
"status": "in-progress",
|
|
@@ -1781,6 +1939,30 @@ def activate_task(
|
|
|
1781
1939
|
"task_file": normalized_task,
|
|
1782
1940
|
"worktree": str(worktree),
|
|
1783
1941
|
}
|
|
1942
|
+
_write_native_dispatch_record(worktree, result)
|
|
1943
|
+
return result
|
|
1944
|
+
|
|
1945
|
+
|
|
1946
|
+
def _write_native_dispatch_record(worktree: Path, activation: Dict[str, object]) -> None:
|
|
1947
|
+
try:
|
|
1948
|
+
if __package__:
|
|
1949
|
+
from . import dispatch_driver
|
|
1950
|
+
else:
|
|
1951
|
+
import dispatch_driver
|
|
1952
|
+
except ImportError:
|
|
1953
|
+
return
|
|
1954
|
+
dispatch_driver.record_native_activation(worktree, activation)
|
|
1955
|
+
|
|
1956
|
+
|
|
1957
|
+
def _write_native_member_dispatch_record(coordinator: Path, activation: Dict[str, object]) -> None:
|
|
1958
|
+
try:
|
|
1959
|
+
if __package__:
|
|
1960
|
+
from . import dispatch_driver
|
|
1961
|
+
else:
|
|
1962
|
+
import dispatch_driver
|
|
1963
|
+
except ImportError:
|
|
1964
|
+
return
|
|
1965
|
+
dispatch_driver.record_native_member_activation(coordinator, activation)
|
|
1784
1966
|
|
|
1785
1967
|
|
|
1786
1968
|
def deactivate_task(
|
|
@@ -1899,6 +2081,59 @@ def stamp_task_landed(task_path: Path, base: str) -> None:
|
|
|
1899
2081
|
)
|
|
1900
2082
|
|
|
1901
2083
|
|
|
2084
|
+
def block_serial_task(
|
|
2085
|
+
primary: Path, task_file: str, task_id: str, base: str
|
|
2086
|
+
) -> Dict[str, str]:
|
|
2087
|
+
"""Record an explicit blocked result on its still-active serial task."""
|
|
2088
|
+
primary = require_directory(primary, "primary worktree")
|
|
2089
|
+
if worktree_root(primary) != primary:
|
|
2090
|
+
raise IsolationError(f"primary is not its Git root: {primary}")
|
|
2091
|
+
normalized_task = relative_posix(task_file)
|
|
2092
|
+
if not normalized_task.startswith(".project/tasks/"):
|
|
2093
|
+
raise IsolationError("serial task file must stay under .project/tasks/")
|
|
2094
|
+
task_id = validate_task_id(task_id)
|
|
2095
|
+
expected_base = require_commit(primary, require_full_sha(base))
|
|
2096
|
+
task_path = _real_file(primary, normalized_task, "serial task file")
|
|
2097
|
+
text, mode = _read_task_text(task_path)
|
|
2098
|
+
fields, error = task_frontmatter(text)
|
|
2099
|
+
if error or fields is None:
|
|
2100
|
+
raise IsolationError(error or "serial task frontmatter is unreadable")
|
|
2101
|
+
if fields.get("id") != task_id:
|
|
2102
|
+
raise IsolationError("serial task id changed before blocked result")
|
|
2103
|
+
if fields.get("status") not in {"in-progress", "blocked"}:
|
|
2104
|
+
raise IsolationError("serial task is no longer active")
|
|
2105
|
+
if fields.get("base") != expected_base:
|
|
2106
|
+
raise IsolationError("serial task base changed before blocked result")
|
|
2107
|
+
if fields.get("task_branch") != "null":
|
|
2108
|
+
raise IsolationError("serial task records a task branch")
|
|
2109
|
+
recorded_worktree = fields.get("worktree")
|
|
2110
|
+
if not isinstance(recorded_worktree, str) or Path(recorded_worktree).resolve() != primary:
|
|
2111
|
+
raise IsolationError("serial task worktree changed before blocked result")
|
|
2112
|
+
|
|
2113
|
+
if fields.get("status") == "blocked":
|
|
2114
|
+
return {"task_id": task_id, "task_file": normalized_task,
|
|
2115
|
+
"base": expected_base, "status": "blocked"}
|
|
2116
|
+
|
|
2117
|
+
head, body = split_frontmatter(text)
|
|
2118
|
+
replaced = False
|
|
2119
|
+
lines = []
|
|
2120
|
+
for line in head:
|
|
2121
|
+
if _frontmatter_key(line) == "status":
|
|
2122
|
+
if replaced:
|
|
2123
|
+
raise IsolationError("serial task frontmatter repeats status")
|
|
2124
|
+
replaced = True
|
|
2125
|
+
lines.append("status: blocked")
|
|
2126
|
+
else:
|
|
2127
|
+
lines.append(line)
|
|
2128
|
+
if not replaced:
|
|
2129
|
+
raise IsolationError("serial task frontmatter is missing status")
|
|
2130
|
+
_replace_regular_file(
|
|
2131
|
+
task_path, ("---\n" + "\n".join(lines) + "\n---\n" + body).encode("utf-8"), mode
|
|
2132
|
+
)
|
|
2133
|
+
return {"task_id": task_id, "task_file": normalized_task,
|
|
2134
|
+
"base": expected_base, "status": "blocked"}
|
|
2135
|
+
|
|
2136
|
+
|
|
1902
2137
|
def _restore_landing_state(
|
|
1903
2138
|
worktree: Path, task_path: Path, task_bytes: bytes, task_mode: int, index_tree: str
|
|
1904
2139
|
) -> None:
|
|
@@ -1933,7 +2168,12 @@ def _restore_rejected_commit(
|
|
|
1933
2168
|
|
|
1934
2169
|
|
|
1935
2170
|
def _stamp_and_commit(
|
|
1936
|
-
worktree: Path,
|
|
2171
|
+
worktree: Path,
|
|
2172
|
+
base: str,
|
|
2173
|
+
subject: str,
|
|
2174
|
+
task_file: str,
|
|
2175
|
+
allowed: Set[str],
|
|
2176
|
+
tolerated: Set[str] = frozenset(),
|
|
1937
2177
|
) -> tuple[str, Dict[str, Optional[tuple[str, str]]]]:
|
|
1938
2178
|
task_path = worktree / task_file
|
|
1939
2179
|
task_text, task_mode = _read_task_text(task_path)
|
|
@@ -1954,14 +2194,14 @@ def _stamp_and_commit(
|
|
|
1954
2194
|
pending = uncommitted_paths(worktree)
|
|
1955
2195
|
if stamped != task_bytes:
|
|
1956
2196
|
pending.add(task_file)
|
|
1957
|
-
validate_allowed_changes(worktree, base, pending, allowed)
|
|
2197
|
+
validate_allowed_changes(worktree, base, pending, allowed, tolerated)
|
|
1958
2198
|
original_head = current_sha(worktree)
|
|
1959
2199
|
index_tree = git_output(worktree, "write-tree")
|
|
1960
2200
|
try:
|
|
1961
2201
|
_replace_regular_file(task_path, stamped, task_mode)
|
|
1962
|
-
body = task_commit_body(task_file, uncommitted_paths(worktree), base)
|
|
2202
|
+
body = task_commit_body(task_file, uncommitted_paths(worktree) - tolerated, base)
|
|
1963
2203
|
commit, verified_tree, verified_delta = commit_allowed_changes(
|
|
1964
|
-
worktree, base, subject, allowed, body
|
|
2204
|
+
worktree, base, subject, allowed, body, tolerated
|
|
1965
2205
|
)
|
|
1966
2206
|
proof_error = _landing_commit_proof_error(
|
|
1967
2207
|
worktree,
|
|
@@ -2039,6 +2279,7 @@ def land(
|
|
|
2039
2279
|
subject,
|
|
2040
2280
|
relative_posix(task_file),
|
|
2041
2281
|
allowed,
|
|
2282
|
+
BOOKKEEPING_PATHS,
|
|
2042
2283
|
)
|
|
2043
2284
|
return {
|
|
2044
2285
|
"bound_branch": bound,
|
|
@@ -2333,7 +2574,7 @@ def _landing_retry_error(
|
|
|
2333
2574
|
if task_file not in pending:
|
|
2334
2575
|
return "interrupted landing does not retain the task change"
|
|
2335
2576
|
try:
|
|
2336
|
-
validate_allowed_changes(repo, base, pending, contract_paths)
|
|
2577
|
+
validate_allowed_changes(repo, base, pending, contract_paths, BOOKKEEPING_PATHS)
|
|
2337
2578
|
except IsolationError as validation_error:
|
|
2338
2579
|
return str(validation_error)
|
|
2339
2580
|
return None
|
|
@@ -2556,6 +2797,40 @@ def _resume_task_error(
|
|
|
2556
2797
|
return _retained_task_contract_error(current_text, isolate_text)
|
|
2557
2798
|
|
|
2558
2799
|
|
|
2800
|
+
def _serial_bookkeeping_error(primary: Path, base: str, head: str) -> Optional[str]:
|
|
2801
|
+
"""Prove that serial task history advanced only through linear .project commits."""
|
|
2802
|
+
if base == head:
|
|
2803
|
+
return None
|
|
2804
|
+
if run_git(primary, "merge-base", "--is-ancestor", base, head).returncode != 0:
|
|
2805
|
+
return "recorded base is not an ancestor of the current branch"
|
|
2806
|
+
commits = git_output(
|
|
2807
|
+
primary, "rev-list", "--first-parent", "--reverse", f"{base}..{head}"
|
|
2808
|
+
).splitlines()
|
|
2809
|
+
previous = base
|
|
2810
|
+
for commit in commits:
|
|
2811
|
+
parents = git_output(primary, "rev-list", "--parents", "-n", "1", commit).split()
|
|
2812
|
+
if len(parents) != 2 or parents[1] != previous:
|
|
2813
|
+
return "history contains an unproven merge or commit sequence"
|
|
2814
|
+
paths = git_output(
|
|
2815
|
+
primary,
|
|
2816
|
+
"diff-tree",
|
|
2817
|
+
"--no-commit-id",
|
|
2818
|
+
"--name-only",
|
|
2819
|
+
"-r",
|
|
2820
|
+
"--no-renames",
|
|
2821
|
+
previous,
|
|
2822
|
+
commit,
|
|
2823
|
+
).splitlines()
|
|
2824
|
+
if not paths:
|
|
2825
|
+
return "history contains an empty commit"
|
|
2826
|
+
if any(path != ".project" and not path.startswith(".project/") for path in paths):
|
|
2827
|
+
return "history contains changes outside .project"
|
|
2828
|
+
previous = commit
|
|
2829
|
+
if previous != head:
|
|
2830
|
+
return "history does not prove a first-parent path from the recorded base"
|
|
2831
|
+
return None
|
|
2832
|
+
|
|
2833
|
+
|
|
2559
2834
|
MEMBER_RECORD_RE = re.compile(
|
|
2560
2835
|
r"Task: (?P<task>\S+)\nBase: (?P<base>[0-9a-f]{40})\n"
|
|
2561
2836
|
r"Member: (?P<member>\S+) (?P<landing>[0-9a-f]{40}) (?P<member_base>[0-9a-f]{40})"
|
|
@@ -2845,8 +3120,12 @@ def _recover_task(
|
|
|
2845
3120
|
)
|
|
2846
3121
|
except IsolationError as error:
|
|
2847
3122
|
return result("block", reason=f"{status} task has invalid base: {error}")
|
|
2848
|
-
|
|
2849
|
-
|
|
3123
|
+
advance_error = _serial_bookkeeping_error(primary, base, head)
|
|
3124
|
+
if advance_error:
|
|
3125
|
+
return result(
|
|
3126
|
+
"block",
|
|
3127
|
+
reason=f"{status} serial task advanced past its base: {advance_error}",
|
|
3128
|
+
)
|
|
2850
3129
|
primary_worktree, primary_error = _worktree_report(primary, primary, bound)
|
|
2851
3130
|
if primary_error:
|
|
2852
3131
|
return result("block", reason=primary_error)
|
|
@@ -2860,6 +3139,15 @@ def _recover_task(
|
|
|
2860
3139
|
str(primary),
|
|
2861
3140
|
"null",
|
|
2862
3141
|
)
|
|
3142
|
+
if head != base and resume_error is None:
|
|
3143
|
+
return result(
|
|
3144
|
+
"reconcile",
|
|
3145
|
+
base=base,
|
|
3146
|
+
reason=(
|
|
3147
|
+
"serial task advanced through .project bookkeeping; reconcile the "
|
|
3148
|
+
"attempt and any retained Verify sidecar before resetting it to pending"
|
|
3149
|
+
),
|
|
3150
|
+
)
|
|
2863
3151
|
if resume_error:
|
|
2864
3152
|
return result("block", reason=resume_error)
|
|
2865
3153
|
return result("resume", base=base)
|
|
@@ -2888,8 +3176,12 @@ def _recover_task(
|
|
|
2888
3176
|
if serial_retained:
|
|
2889
3177
|
if str(fields.get("task_branch", "")) != "null":
|
|
2890
3178
|
return result("block", reason=f"{status} serial task records a task branch")
|
|
2891
|
-
|
|
2892
|
-
|
|
3179
|
+
advance_error = _serial_bookkeeping_error(primary, base, head)
|
|
3180
|
+
if advance_error:
|
|
3181
|
+
return result(
|
|
3182
|
+
"block",
|
|
3183
|
+
reason=f"{status} serial task advanced past its base: {advance_error}",
|
|
3184
|
+
)
|
|
2893
3185
|
primary_worktree, primary_error = _worktree_report(primary, primary, bound)
|
|
2894
3186
|
if primary_error:
|
|
2895
3187
|
return result("block", reason=primary_error)
|
|
@@ -4321,6 +4613,7 @@ def retire(
|
|
|
4321
4613
|
or f"could not delete {branch}"
|
|
4322
4614
|
)
|
|
4323
4615
|
branch_retired = True
|
|
4616
|
+
release_workspace_quietly(primary)
|
|
4324
4617
|
return {
|
|
4325
4618
|
"bound_branch": bound,
|
|
4326
4619
|
"branch": branch,
|
|
@@ -4354,22 +4647,13 @@ def retire(
|
|
|
4354
4647
|
raise IsolationError(
|
|
4355
4648
|
"worktree is dirty; pass --force only from the retry-retirement path"
|
|
4356
4649
|
)
|
|
4357
|
-
|
|
4358
|
-
primary,
|
|
4359
|
-
"worktree",
|
|
4360
|
-
"remove",
|
|
4361
|
-
*(("--force",) if force else ()),
|
|
4362
|
-
str(resolved_worktree),
|
|
4363
|
-
)
|
|
4364
|
-
if remove.returncode != 0:
|
|
4365
|
-
raise IsolationError(
|
|
4366
|
-
(remove.stderr or remove.stdout).strip() or "git worktree remove failed"
|
|
4367
|
-
)
|
|
4650
|
+
_remove_worktree(primary, resolved_worktree, force)
|
|
4368
4651
|
deleted = run_git(primary, "branch", "-D", retire_branch)
|
|
4369
4652
|
if deleted.returncode != 0:
|
|
4370
4653
|
raise IsolationError(
|
|
4371
4654
|
(deleted.stderr or deleted.stdout).strip() or f"could not delete {retire_branch}"
|
|
4372
4655
|
)
|
|
4656
|
+
release_workspace_quietly(primary)
|
|
4373
4657
|
return {
|
|
4374
4658
|
"bound_branch": bound,
|
|
4375
4659
|
"branch": retire_branch,
|
|
@@ -4379,6 +4663,54 @@ def retire(
|
|
|
4379
4663
|
}
|
|
4380
4664
|
|
|
4381
4665
|
|
|
4666
|
+
def retire_unused_verify(
|
|
4667
|
+
primary: Path, worktree: Path, base: str, branch: str
|
|
4668
|
+
) -> Dict[str, object]:
|
|
4669
|
+
"""Retire a Verify sidecar only when it is provably untouched since creation."""
|
|
4670
|
+
primary = require_directory(primary, "primary worktree")
|
|
4671
|
+
if worktree_root(primary) != primary:
|
|
4672
|
+
raise IsolationError(f"primary is not its Git root: {primary}")
|
|
4673
|
+
if not branch.startswith(VERIFY_BRANCH_PREFIX):
|
|
4674
|
+
raise IsolationError("unused Verify retirement requires a verify branch")
|
|
4675
|
+
expected = _sidecar_path_for_branch(primary, branch)
|
|
4676
|
+
resolved_worktree = worktree.resolve()
|
|
4677
|
+
if resolved_worktree != expected:
|
|
4678
|
+
raise IsolationError("unused Verify path differs from its branch")
|
|
4679
|
+
expected_base = require_commit(primary, require_full_sha(base))
|
|
4680
|
+
|
|
4681
|
+
if os.path.lexists(worktree):
|
|
4682
|
+
if worktree.is_symlink() or not worktree.is_dir() or worktree.resolve() != worktree:
|
|
4683
|
+
raise IsolationError(f"unused Verify path is not an owned worktree: {worktree}")
|
|
4684
|
+
if worktree_root(worktree) != worktree:
|
|
4685
|
+
raise IsolationError(f"unused Verify path is not its Git root: {worktree}")
|
|
4686
|
+
if common_git_dir(primary) != common_git_dir(worktree):
|
|
4687
|
+
raise IsolationError("unused Verify sidecar belongs to another repository")
|
|
4688
|
+
if require_attached(worktree) != branch:
|
|
4689
|
+
raise IsolationError("unused Verify sidecar branch changed")
|
|
4690
|
+
if current_sha(worktree) != expected_base:
|
|
4691
|
+
raise IsolationError("unused Verify sidecar HEAD changed; preserving it")
|
|
4692
|
+
dirty = git_output(
|
|
4693
|
+
worktree,
|
|
4694
|
+
"status",
|
|
4695
|
+
"--porcelain",
|
|
4696
|
+
"--untracked-files=all",
|
|
4697
|
+
"--ignored=matching",
|
|
4698
|
+
)
|
|
4699
|
+
if dirty:
|
|
4700
|
+
raise IsolationError("unused Verify sidecar has uncommitted files; preserving it")
|
|
4701
|
+
else:
|
|
4702
|
+
branch_head = run_git(
|
|
4703
|
+
primary, "rev-parse", "--verify", "--quiet", f"refs/heads/{branch}"
|
|
4704
|
+
)
|
|
4705
|
+
if branch_head.returncode == 0 and branch_head.stdout.strip() != expected_base:
|
|
4706
|
+
raise IsolationError("unused Verify branch HEAD changed; preserving it")
|
|
4707
|
+
for registered_path, registered_branch in _registered_worktrees(primary).items():
|
|
4708
|
+
if registered_branch == f"refs/heads/{branch}" and registered_path != expected:
|
|
4709
|
+
raise IsolationError("unused Verify branch is checked out at another path")
|
|
4710
|
+
|
|
4711
|
+
return retire(primary, expected, branch, False)
|
|
4712
|
+
|
|
4713
|
+
|
|
4382
4714
|
def parser() -> argparse.ArgumentParser:
|
|
4383
4715
|
argument_parser = argparse.ArgumentParser(description=__doc__)
|
|
4384
4716
|
subparsers = argument_parser.add_subparsers(dest="command", required=True)
|