@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
|
@@ -16,9 +16,11 @@ from __future__ import annotations
|
|
|
16
16
|
import argparse
|
|
17
17
|
import errno
|
|
18
18
|
import hashlib
|
|
19
|
+
import importlib
|
|
19
20
|
import json
|
|
20
21
|
import os
|
|
21
22
|
import re
|
|
23
|
+
import shlex
|
|
22
24
|
import subprocess
|
|
23
25
|
import sys
|
|
24
26
|
import tempfile
|
|
@@ -31,6 +33,7 @@ try:
|
|
|
31
33
|
from scripts import (_common, archive_milestone, build_state, discussion_validate, isolation,
|
|
32
34
|
pipeline_state, review_findings, review_panel, workflow_run, task_context, model_policy)
|
|
33
35
|
from scripts import check_handoffs as contracts
|
|
36
|
+
from scripts.pipeline_git import is_bound_branch
|
|
34
37
|
except ImportError: # bundled copy inside a skill's scripts directory
|
|
35
38
|
import _common
|
|
36
39
|
import archive_milestone
|
|
@@ -44,6 +47,7 @@ except ImportError: # bundled copy inside a skill's scripts directory
|
|
|
44
47
|
import review_panel
|
|
45
48
|
import workflow_run
|
|
46
49
|
import model_policy
|
|
50
|
+
from pipeline_git import is_bound_branch
|
|
47
51
|
|
|
48
52
|
if os.name == "nt":
|
|
49
53
|
import msvcrt
|
|
@@ -71,12 +75,91 @@ class DriverStop(RuntimeError):
|
|
|
71
75
|
STOP_ERRORS = (DriverStop, isolation.IsolationError, build_state.BuildStateError,
|
|
72
76
|
contracts.HandoffError, review_findings.ReviewFindingsError,
|
|
73
77
|
pipeline_state.PipelineStateError, archive_milestone.ArchiveError, model_policy.PolicyError)
|
|
78
|
+
_RUNTIME_ACTIVATION_ORDER = (
|
|
79
|
+
"_common",
|
|
80
|
+
"pipeline_git",
|
|
81
|
+
"worktree_paths",
|
|
82
|
+
"build_recovery",
|
|
83
|
+
"isolation",
|
|
84
|
+
"pipeline_state",
|
|
85
|
+
"state_checkpoint",
|
|
86
|
+
"check_task_briefs",
|
|
87
|
+
"build_state",
|
|
88
|
+
"check_handoffs",
|
|
89
|
+
"task_context",
|
|
90
|
+
"archive_milestone",
|
|
91
|
+
"review_findings",
|
|
92
|
+
"model_policy",
|
|
93
|
+
)
|
|
74
94
|
RECOVERY_BLOCKED = "recovery blocked"
|
|
75
95
|
# The stops the build contract answers with build/blocked when the round raises them (steps 1 and 2);
|
|
76
96
|
# the same stops from review, panel, or skeptics stay with the parent.
|
|
77
97
|
BLOCKING_STOPS = {"dependency-deadlock": "dependency deadlock", RECOVERY_BLOCKED: "blocked recovery"}
|
|
78
98
|
|
|
79
99
|
|
|
100
|
+
def _bundled_helpers() -> bool:
|
|
101
|
+
return "scripts.isolation" not in sys.modules
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _refresh_stop_errors() -> None:
|
|
105
|
+
global STOP_ERRORS
|
|
106
|
+
STOP_ERRORS = (DriverStop, isolation.IsolationError, build_state.BuildStateError,
|
|
107
|
+
contracts.HandoffError, review_findings.ReviewFindingsError,
|
|
108
|
+
pipeline_state.PipelineStateError, archive_milestone.ArchiveError,
|
|
109
|
+
model_policy.PolicyError)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _reload_pinned_modules(runtime_root: Path) -> None:
|
|
113
|
+
global isolation, build_state, task_context, contracts, pipeline_state
|
|
114
|
+
global archive_milestone, review_findings, model_policy
|
|
115
|
+
if _bundled_helpers():
|
|
116
|
+
path = str(runtime_root)
|
|
117
|
+
if path not in sys.path:
|
|
118
|
+
sys.path.insert(0, path)
|
|
119
|
+
for name in _RUNTIME_ACTIVATION_ORDER:
|
|
120
|
+
module = sys.modules.get(name)
|
|
121
|
+
if module is not None:
|
|
122
|
+
importlib.reload(module)
|
|
123
|
+
isolation = sys.modules["isolation"]
|
|
124
|
+
build_state = sys.modules["build_state"]
|
|
125
|
+
task_context = sys.modules["task_context"]
|
|
126
|
+
contracts = sys.modules["check_handoffs"]
|
|
127
|
+
pipeline_state = sys.modules["pipeline_state"]
|
|
128
|
+
archive_milestone = sys.modules["archive_milestone"]
|
|
129
|
+
review_findings = sys.modules["review_findings"]
|
|
130
|
+
model_policy = sys.modules["model_policy"]
|
|
131
|
+
else:
|
|
132
|
+
import scripts
|
|
133
|
+
path = str(runtime_root)
|
|
134
|
+
if path not in scripts.__path__:
|
|
135
|
+
scripts.__path__ = [path, *[entry for entry in scripts.__path__ if entry != path]]
|
|
136
|
+
for name in _RUNTIME_ACTIVATION_ORDER:
|
|
137
|
+
module = sys.modules.get(f"scripts.{name}")
|
|
138
|
+
if module is not None:
|
|
139
|
+
importlib.reload(module)
|
|
140
|
+
isolation = scripts.isolation
|
|
141
|
+
build_state = scripts.build_state
|
|
142
|
+
task_context = scripts.task_context
|
|
143
|
+
contracts = scripts.check_handoffs
|
|
144
|
+
pipeline_state = scripts.pipeline_state
|
|
145
|
+
archive_milestone = scripts.archive_milestone
|
|
146
|
+
review_findings = scripts.review_findings
|
|
147
|
+
model_policy = scripts.model_policy
|
|
148
|
+
_refresh_stop_errors()
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def _activate_pinned_runtime(repo: Path) -> None:
|
|
152
|
+
if not os.path.lexists(repo / ".gsd-path/runtime.json"):
|
|
153
|
+
return
|
|
154
|
+
resolved = subprocess.run(
|
|
155
|
+
[sys.executable, "-B", str(repo / ".gsd-path/status_runtime.py"),
|
|
156
|
+
"--repo", str(repo), "--runtime-path"],
|
|
157
|
+
cwd=repo, capture_output=True, encoding="utf-8", errors="replace")
|
|
158
|
+
if resolved.returncode:
|
|
159
|
+
raise DriverStop(resolved.stderr.strip() or "pinned runtime resolution failed")
|
|
160
|
+
_reload_pinned_modules(Path(resolved.stdout.strip()))
|
|
161
|
+
|
|
162
|
+
|
|
80
163
|
def now() -> str:
|
|
81
164
|
return datetime.now(timezone.utc).isoformat(timespec="seconds")
|
|
82
165
|
|
|
@@ -107,6 +190,169 @@ def attempts_used(root: Path, task_id: str) -> int:
|
|
|
107
190
|
if load_state(path).get("origin") == "dispatch")
|
|
108
191
|
|
|
109
192
|
|
|
193
|
+
def dispatch_primary(worktree: Path) -> Path:
|
|
194
|
+
"""The bound-branch primary that owns dispatch records for a task checkout."""
|
|
195
|
+
worktree = worktree.resolve()
|
|
196
|
+
if is_bound_branch(isolation.require_attached(worktree)):
|
|
197
|
+
return worktree
|
|
198
|
+
matches = [
|
|
199
|
+
path.resolve()
|
|
200
|
+
for path, branch in isolation._registered_worktrees(worktree).items()
|
|
201
|
+
if branch and is_bound_branch(branch.removeprefix("refs/heads/"))
|
|
202
|
+
]
|
|
203
|
+
if len(matches) != 1:
|
|
204
|
+
raise isolation.IsolationError(
|
|
205
|
+
"dispatch records require exactly one primary worktree for the bound branch")
|
|
206
|
+
return matches[0]
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def should_open_native_shell(root: Path, task_id: str, base: str) -> bool:
|
|
210
|
+
"""Whether activate-task should append a native dispatch attempt record."""
|
|
211
|
+
task_dir = root / task_id
|
|
212
|
+
if not task_dir.is_dir():
|
|
213
|
+
return True
|
|
214
|
+
attempts = sorted(task_dir.glob("attempt-*/state.json"),
|
|
215
|
+
key=lambda path: attempt_number(path.parent))
|
|
216
|
+
if not attempts:
|
|
217
|
+
return True
|
|
218
|
+
latest = read_attempt(attempts[-1])
|
|
219
|
+
if latest.get("outcome") is None and not latest.get("command"):
|
|
220
|
+
return False
|
|
221
|
+
if latest.get("outcome") == "blocked" and str(latest.get("base")) == base:
|
|
222
|
+
return False
|
|
223
|
+
return True
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def open_native_shell(primary: Path, *, task_id: str, base: str, worktree: str, task_file: str,
|
|
227
|
+
task_branch: Optional[str], mode: str, wave: Optional[int] = None,
|
|
228
|
+
title: Optional[str] = None, files: Optional[List[str]] = None,
|
|
229
|
+
sidecar: Optional[object] = None, member: Optional[str] = None,
|
|
230
|
+
member_base: Optional[str] = None, contract_file: Optional[str] = None) -> None:
|
|
231
|
+
"""Open a dispatch attempt record for a host-spawned coder before it runs."""
|
|
232
|
+
root = records_root(primary)
|
|
233
|
+
root.mkdir(parents=True, exist_ok=True)
|
|
234
|
+
record: Dict[str, object] = {
|
|
235
|
+
"task_id": task_id,
|
|
236
|
+
"base": base,
|
|
237
|
+
"worktree": worktree,
|
|
238
|
+
"task_file": task_file,
|
|
239
|
+
"task_branch": task_branch,
|
|
240
|
+
"mode": mode,
|
|
241
|
+
"origin": "dispatch",
|
|
242
|
+
"outcome": None,
|
|
243
|
+
"dispatched_at": now(),
|
|
244
|
+
"native": True,
|
|
245
|
+
}
|
|
246
|
+
if wave is not None:
|
|
247
|
+
record["wave"] = wave
|
|
248
|
+
if title is not None:
|
|
249
|
+
record["title"] = title
|
|
250
|
+
if files is not None:
|
|
251
|
+
record["files"] = files
|
|
252
|
+
if sidecar is not None:
|
|
253
|
+
record["sidecar"] = sidecar
|
|
254
|
+
if member is not None:
|
|
255
|
+
record["member"] = member
|
|
256
|
+
if member_base is not None:
|
|
257
|
+
record["member_base"] = member_base
|
|
258
|
+
if contract_file is not None:
|
|
259
|
+
record["contract_file"] = contract_file
|
|
260
|
+
task_dir = root / task_id
|
|
261
|
+
if not should_open_native_shell(root, task_id, base):
|
|
262
|
+
attempts = sorted(task_dir.glob("attempt-*/state.json"),
|
|
263
|
+
key=lambda path: attempt_number(path.parent))
|
|
264
|
+
if not attempts:
|
|
265
|
+
return
|
|
266
|
+
state_path = attempts[-1]
|
|
267
|
+
state = load_state(state_path)
|
|
268
|
+
if state.get("outcome") is not None or state.get("command"):
|
|
269
|
+
return
|
|
270
|
+
attempt = state.get("attempt")
|
|
271
|
+
state.update(record)
|
|
272
|
+
if attempt is not None:
|
|
273
|
+
state["attempt"] = attempt
|
|
274
|
+
state["outcome"] = None
|
|
275
|
+
for key in ("pid", "finished_at", "exit_code", "timed_out", "reason", "question", "commit",
|
|
276
|
+
"answer", "command", "usage_recorded"):
|
|
277
|
+
state.pop(key, None)
|
|
278
|
+
save_state(state_path, state)
|
|
279
|
+
exit_path = state_path.with_name("exit.json")
|
|
280
|
+
if exit_path.is_file():
|
|
281
|
+
exit_path.unlink()
|
|
282
|
+
return
|
|
283
|
+
attempt = max((attempt_number(path) for path in task_dir.glob("attempt-*")), default=0) + 1
|
|
284
|
+
attempt_dir = task_dir / f"attempt-{attempt}"
|
|
285
|
+
attempt_dir.mkdir(parents=True)
|
|
286
|
+
record["attempt"] = attempt
|
|
287
|
+
save_state(attempt_dir / "state.json", record)
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
def record_native_activation(worktree: Path, activation: Dict[str, object]) -> None:
|
|
291
|
+
"""Record a native task activation in the milestone dispatch ledger."""
|
|
292
|
+
primary = dispatch_primary(worktree)
|
|
293
|
+
task_file = str(activation["task_file"])
|
|
294
|
+
task_path = worktree / task_file
|
|
295
|
+
fields, _ = isolation.task_frontmatter(task_path.read_text(encoding="utf-8"))
|
|
296
|
+
task_id = str(activation.get("task_id") or (fields or {}).get("id"))
|
|
297
|
+
wave = None
|
|
298
|
+
if fields and fields.get("wave") is not None:
|
|
299
|
+
wave = int(fields["wave"])
|
|
300
|
+
open_native_shell(
|
|
301
|
+
primary,
|
|
302
|
+
task_id=task_id,
|
|
303
|
+
base=str(activation["base"]),
|
|
304
|
+
worktree=str(activation["worktree"]),
|
|
305
|
+
task_file=task_file,
|
|
306
|
+
task_branch=activation.get("task_branch"),
|
|
307
|
+
mode="parallel" if activation.get("task_branch") else "serial",
|
|
308
|
+
wave=wave,
|
|
309
|
+
title=str(fields.get("title")) if fields else None,
|
|
310
|
+
files=list(fields.get("files") or []) if fields else None,
|
|
311
|
+
)
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
def record_native_member_activation(coordinator: Path, activation: Dict[str, object]) -> None:
|
|
315
|
+
"""Record a native member-task activation in the coordinator dispatch ledger."""
|
|
316
|
+
task_file = str(activation["task_file"])
|
|
317
|
+
sidecar = Path(str(activation["worktree"]))
|
|
318
|
+
copy = Path(str(activation["copy"]))
|
|
319
|
+
fields, _ = isolation.task_frontmatter(copy.read_text(encoding="utf-8"))
|
|
320
|
+
task_id = str(activation.get("task_id") or (fields or {}).get("id"))
|
|
321
|
+
wave = None
|
|
322
|
+
if fields and fields.get("wave") is not None:
|
|
323
|
+
wave = int(fields["wave"])
|
|
324
|
+
open_native_shell(
|
|
325
|
+
coordinator,
|
|
326
|
+
task_id=task_id,
|
|
327
|
+
base=str(activation["base"]),
|
|
328
|
+
worktree=str(sidecar),
|
|
329
|
+
task_file=copy.relative_to(sidecar).as_posix(),
|
|
330
|
+
task_branch=str(activation["task_branch"]),
|
|
331
|
+
mode="member",
|
|
332
|
+
wave=wave,
|
|
333
|
+
title=str(fields.get("title")) if fields else None,
|
|
334
|
+
files=list(fields.get("files") or []) if fields else None,
|
|
335
|
+
member=str(activation["member"]),
|
|
336
|
+
member_base=str(activation["member_base"]),
|
|
337
|
+
contract_file=task_file,
|
|
338
|
+
)
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
def reusable_shell_attempt(root: Path, state: Dict[str, object]) -> Optional[Path]:
|
|
342
|
+
"""A native activation shell the driver can attach its child wrapper to."""
|
|
343
|
+
task_dir = root / str(state["task_id"])
|
|
344
|
+
attempts = sorted(task_dir.glob("attempt-*/state.json"),
|
|
345
|
+
key=lambda path: attempt_number(path.parent))
|
|
346
|
+
if not attempts:
|
|
347
|
+
return None
|
|
348
|
+
prior = read_attempt(attempts[-1])
|
|
349
|
+
if prior.get("outcome") is not None or prior.get("command") or prior.get("pid"):
|
|
350
|
+
return None
|
|
351
|
+
if str(prior.get("base")) != str(state.get("base")):
|
|
352
|
+
return None
|
|
353
|
+
return attempts[-1].parent
|
|
354
|
+
|
|
355
|
+
|
|
110
356
|
def acquire_lock(primary: Path) -> BinaryIO:
|
|
111
357
|
root = records_root(primary)
|
|
112
358
|
root.mkdir(parents=True, exist_ok=True)
|
|
@@ -307,7 +553,7 @@ def retain_selection(root: Path, state: Dict[str, object]) -> Dict[str, object]:
|
|
|
307
553
|
raise DriverStop('cannot redispatch while the previous child is active')
|
|
308
554
|
if prior.get('model_selection'):
|
|
309
555
|
state['model_selection'] = prior['model_selection']
|
|
310
|
-
|
|
556
|
+
elif prior.get('command'):
|
|
311
557
|
state['command'] = prior['command']
|
|
312
558
|
reassignment = task_dir / 'model-reassignment.json'
|
|
313
559
|
if reassignment.exists():
|
|
@@ -323,13 +569,23 @@ def spawn(root: Path, state: Dict[str, object], options: argparse.Namespace,
|
|
|
323
569
|
state = retain_selection(root, state)
|
|
324
570
|
task_dir = root / str(state["task_id"])
|
|
325
571
|
argv, selection = selected_command(state, options, command or options.child_command)
|
|
326
|
-
|
|
327
|
-
attempt_dir
|
|
328
|
-
|
|
572
|
+
attempt_dir = reusable_shell_attempt(root, state)
|
|
573
|
+
if attempt_dir is None:
|
|
574
|
+
attempt = max((attempt_number(path) for path in task_dir.glob("attempt-*")), default=0) + 1
|
|
575
|
+
attempt_dir = task_dir / f"attempt-{attempt}"
|
|
576
|
+
attempt_dir.mkdir(parents=True)
|
|
577
|
+
else:
|
|
578
|
+
attempt = attempt_number(attempt_dir)
|
|
329
579
|
stale = ("pid", "finished_at", "exit_code", "timed_out", "reason", "question", "commit", "answer",
|
|
330
580
|
"usage_recorded")
|
|
331
|
-
|
|
332
|
-
|
|
581
|
+
incoming = {key: value for key, value in state.items()
|
|
582
|
+
if not key.startswith("_") and key not in stale}
|
|
583
|
+
if (attempt_dir / "state.json").is_file():
|
|
584
|
+
preserved = {key: value for key, value in read_attempt(attempt_dir / "state.json").items()
|
|
585
|
+
if not key.startswith("_") and key not in stale}
|
|
586
|
+
state = {**preserved, **incoming}
|
|
587
|
+
else:
|
|
588
|
+
state = incoming
|
|
333
589
|
if selection is not None:
|
|
334
590
|
state['model_selection'] = selection
|
|
335
591
|
state.update({"attempt": attempt, "command": argv,
|
|
@@ -446,14 +702,14 @@ def finish_task(primary: Path, state: Dict[str, object]) -> Dict[str, object]:
|
|
|
446
702
|
if not command:
|
|
447
703
|
raise DriverStop("task has no Verify command", task=task_id)
|
|
448
704
|
if state["mode"] == "serial":
|
|
449
|
-
# ponytail: sidecar reproduction is git plumbing that belongs in isolation.py as the
|
|
450
|
-
# inverse of clean_verify; lift it there when a second caller appears.
|
|
451
705
|
sidecar = state["sidecar"]
|
|
452
|
-
|
|
706
|
+
ensured = isolation.ensure_verify_sidecar(primary, base, str(sidecar["branch"]))
|
|
707
|
+
sidecar_path = Path(str(ensured["worktree"]))
|
|
453
708
|
tree = snapshot_tree(primary)
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
709
|
+
try:
|
|
710
|
+
isolation.reproduce_verify_sidecar(sidecar_path, tree)
|
|
711
|
+
except isolation.IsolationError as error:
|
|
712
|
+
raise DriverStop(str(error), task=task_id) from error
|
|
457
713
|
execution = run_verify(command, sidecar_path)
|
|
458
714
|
location = f"sidecar {sidecar['branch']}"
|
|
459
715
|
isolation.clean_verify(primary, sidecar_path, base, str(sidecar["branch"]))
|
|
@@ -659,6 +915,26 @@ class Round:
|
|
|
659
915
|
verdict = task["verdict"]
|
|
660
916
|
if verdict in ("recovered", "attested"):
|
|
661
917
|
self.proven[str(task.get("task_id"))] = str(task.get("commit"))
|
|
918
|
+
if (verdict == "reconcile" and task.get("status") == "in-progress"
|
|
919
|
+
and task.get("task_branch") is None):
|
|
920
|
+
task_id = str(task.get("task_id"))
|
|
921
|
+
verify_name = f"task-{task_id.lower()}-verify"
|
|
922
|
+
verify_path = isolation.sidecar_root(self.primary, "verify", verify_name)
|
|
923
|
+
verify_branch = isolation.verify_branch_name(verify_name)
|
|
924
|
+
diagnose = shlex.join([
|
|
925
|
+
sys.executable,
|
|
926
|
+
"-B",
|
|
927
|
+
str(Path(__file__).with_name("pipeline_diagnose.py")),
|
|
928
|
+
"diagnose",
|
|
929
|
+
"--repo",
|
|
930
|
+
str(self.primary),
|
|
931
|
+
])
|
|
932
|
+
raise DriverStop(
|
|
933
|
+
f"task {task_id} needs recovery: {task.get('reason')}; inspect Verify "
|
|
934
|
+
f"sidecar {verify_path} ({verify_branch}) before resetting it to pending; "
|
|
935
|
+
f"inventory it with `{diagnose}`",
|
|
936
|
+
recover=task,
|
|
937
|
+
)
|
|
662
938
|
if verdict == "recovered" and task.get("member") and task.get("task_id") not in retired_members:
|
|
663
939
|
isolation.retire_member_task(self.primary, str(task["member"]), str(task["task_id"]))
|
|
664
940
|
if verdict == "recovered" and worktree and worktree["clean"] and task.get("task_branch"):
|
|
@@ -673,12 +949,22 @@ class Round:
|
|
|
673
949
|
# settle exited children ---------------------------------------------
|
|
674
950
|
|
|
675
951
|
def isolate_live(self, state: Dict[str, object]) -> bool:
|
|
676
|
-
"""
|
|
952
|
+
"""Keep blocked attempts reportable until the corresponding task is reset."""
|
|
677
953
|
task_path = Path(str(state["worktree"])) / str(state["task_file"])
|
|
678
954
|
if not task_path.is_file():
|
|
679
955
|
return False
|
|
680
956
|
fields, _ = isolation.task_frontmatter(task_path.read_text(encoding="utf-8"))
|
|
681
|
-
|
|
957
|
+
if fields is None:
|
|
958
|
+
return False
|
|
959
|
+
if fields.get("status") == "in-progress":
|
|
960
|
+
return True
|
|
961
|
+
return (
|
|
962
|
+
state.get("mode") == "serial"
|
|
963
|
+
and fields.get("status") == "blocked"
|
|
964
|
+
and fields.get("id") == state.get("task_id")
|
|
965
|
+
and fields.get("base") == state.get("base")
|
|
966
|
+
and fields.get("task_branch") == "null"
|
|
967
|
+
)
|
|
682
968
|
|
|
683
969
|
def settle(self) -> bool:
|
|
684
970
|
"""Classify every exited child. Returns True when a landing or redispatch changed the round."""
|
|
@@ -717,8 +1003,15 @@ class Round:
|
|
|
717
1003
|
elif state.get("finished_at") is None:
|
|
718
1004
|
if child_running(state):
|
|
719
1005
|
self.receipt["in_flight"].append(self.summary(state))
|
|
1006
|
+
elif (state.get("native") and not state.get("command") and not state.get("pid")
|
|
1007
|
+
and self.isolate_live(state)):
|
|
1008
|
+
continue
|
|
720
1009
|
else:
|
|
721
|
-
|
|
1010
|
+
state.update(read_attempt(Path(str(state["_path"]))))
|
|
1011
|
+
if state.get("finished_at") is not None:
|
|
1012
|
+
changed = self.classify(state) or changed
|
|
1013
|
+
else:
|
|
1014
|
+
self.fail(state, ORPHANED)
|
|
722
1015
|
else:
|
|
723
1016
|
changed = self.classify(state) or changed
|
|
724
1017
|
return changed
|
|
@@ -729,6 +1022,35 @@ class Round:
|
|
|
729
1022
|
update_state(state, outcome="blocked", reason=reason)
|
|
730
1023
|
self.receipt["blocked"].append({**self.summary(state), "reason": reason, **details})
|
|
731
1024
|
|
|
1025
|
+
def blocked_result(self, state: Dict[str, object], **diagnostics: object) -> bool:
|
|
1026
|
+
"""Record an explicit serial block and retire only its untouched Verify sidecar."""
|
|
1027
|
+
reason = "child returned blocked"
|
|
1028
|
+
details: Dict[str, object] = dict(diagnostics)
|
|
1029
|
+
if state.get("mode") == "serial":
|
|
1030
|
+
try:
|
|
1031
|
+
task = isolation.block_serial_task(
|
|
1032
|
+
self.primary,
|
|
1033
|
+
str(state["task_file"]),
|
|
1034
|
+
str(state["task_id"]),
|
|
1035
|
+
str(state["base"]),
|
|
1036
|
+
)
|
|
1037
|
+
except STOP_ERRORS as error:
|
|
1038
|
+
self.fail(state, f"{reason}; could not record blocked task: {error}")
|
|
1039
|
+
return False
|
|
1040
|
+
details["task"] = task
|
|
1041
|
+
sidecar = state.get("sidecar")
|
|
1042
|
+
if isinstance(sidecar, dict):
|
|
1043
|
+
worktree = Path(str(sidecar["worktree"]))
|
|
1044
|
+
branch = str(sidecar["branch"])
|
|
1045
|
+
try:
|
|
1046
|
+
details["verify_cleanup"] = isolation.retire_unused_verify(
|
|
1047
|
+
self.primary, worktree, str(state["base"]), branch
|
|
1048
|
+
)
|
|
1049
|
+
except STOP_ERRORS as error:
|
|
1050
|
+
reason += f"; Verify sidecar retained at {worktree}: {error}"
|
|
1051
|
+
self.fail(state, reason, **details)
|
|
1052
|
+
return False
|
|
1053
|
+
|
|
732
1054
|
def classify(self, state: Dict[str, object]) -> bool:
|
|
733
1055
|
attempt_dir = Path(str(state["_path"])).parent
|
|
734
1056
|
if self.budgeted and not state.get("usage_recorded"):
|
|
@@ -756,6 +1078,10 @@ class Round:
|
|
|
756
1078
|
update_state(state, outcome="question", question=unanswered[0], answered=False)
|
|
757
1079
|
self.receipt["questions"].append({**self.summary(state), "question": unanswered[0],
|
|
758
1080
|
"log_delta": delta.strip()})
|
|
1081
|
+
elif verdict == "blocked" and named == str(state["task_id"]).lower():
|
|
1082
|
+
return self.blocked_result(
|
|
1083
|
+
state, stdout_tail=tail(message), log_delta=delta.strip()
|
|
1084
|
+
)
|
|
759
1085
|
elif verdict != "ready" or named != str(state["task_id"]).lower():
|
|
760
1086
|
self.fail(state, "child returned blocked" if verdict == "blocked"
|
|
761
1087
|
else "child returned no RESULT line", stdout_tail=tail(message),
|
|
@@ -790,8 +1116,14 @@ class Round:
|
|
|
790
1116
|
# dispatch -------------------------------------------------------------
|
|
791
1117
|
|
|
792
1118
|
def in_flight_states(self) -> List[Dict[str, object]]:
|
|
793
|
-
|
|
794
|
-
|
|
1119
|
+
states = []
|
|
1120
|
+
for state in latest_states(self.root):
|
|
1121
|
+
if state.get("outcome") is not None or state.get("wave") != self.receipt["wave"]:
|
|
1122
|
+
continue
|
|
1123
|
+
if state.get("native") and not state.get("command") and not state.get("pid"):
|
|
1124
|
+
continue
|
|
1125
|
+
states.append(state)
|
|
1126
|
+
return states
|
|
795
1127
|
|
|
796
1128
|
def checkpoint_bookkeeping(self) -> None:
|
|
797
1129
|
open_ids = {state["task_id"] for state in latest_states(self.root)
|
|
@@ -815,6 +1147,24 @@ class Round:
|
|
|
815
1147
|
ready = build_state.ready(str(self.primary), self.project_dir)
|
|
816
1148
|
self.receipt["steps"].append({"script": "build_state.py ready", "result": ready})
|
|
817
1149
|
wave = ready["current_wave"]
|
|
1150
|
+
blocked_review = ready.get("blocked_review")
|
|
1151
|
+
if blocked_review:
|
|
1152
|
+
waiting_waves = blocked_review["waiting_waves"]
|
|
1153
|
+
if self.receipt["wave"] in waiting_waves:
|
|
1154
|
+
raise DriverStop(
|
|
1155
|
+
f"wave {self.receipt['wave']} has unfinished tasks but wave "
|
|
1156
|
+
f"{blocked_review['wave']} review cycle {blocked_review['cycle']} "
|
|
1157
|
+
"is blocked; it must pass before dispatch")
|
|
1158
|
+
self.checkpoint_bookkeeping()
|
|
1159
|
+
return not in_flight
|
|
1160
|
+
repair_for_wave = ready.get("repair_for_wave")
|
|
1161
|
+
if repair_for_wave is not None and self.receipt["wave"] != wave:
|
|
1162
|
+
if self.receipt["wave"] > repair_for_wave:
|
|
1163
|
+
raise DriverStop(
|
|
1164
|
+
f"wave {self.receipt['wave']} is waiting on blocked wave {repair_for_wave}; "
|
|
1165
|
+
f"dispatch repair wave {wave} first")
|
|
1166
|
+
self.checkpoint_bookkeeping()
|
|
1167
|
+
return not in_flight
|
|
818
1168
|
if wave is not None and wave < self.receipt["wave"]:
|
|
819
1169
|
raise DriverStop(f"wave {wave} is still unfinished; run round --wave {wave} first")
|
|
820
1170
|
self.checkpoint_bookkeeping()
|
|
@@ -1686,7 +2036,7 @@ def fix_tasks(primary: Path, options: argparse.Namespace) -> Dict[str, object]:
|
|
|
1686
2036
|
texts = contracts._task_texts(primary, options.project_dir)
|
|
1687
2037
|
groups = {group["locator"]: group for group in findings["groups"]}
|
|
1688
2038
|
today = now()[:10]
|
|
1689
|
-
#
|
|
2039
|
+
# Unordered same-wave file overlap is rejected, so repairs land in a newly appended wave.
|
|
1690
2040
|
plan_path = project / "plan/PLAN.md"
|
|
1691
2041
|
plan_text = plan_path.read_text(encoding="utf-8")
|
|
1692
2042
|
depths, waves = contracts._plan_waves(plan_text)
|
|
@@ -1820,7 +2170,8 @@ Heavy: {'yes' if heavy else 'no'}
|
|
|
1820
2170
|
receipt["plan_wave"] = fix_wave
|
|
1821
2171
|
lint = subprocess.run([sys.executable, "-B", str(Path(__file__).resolve().parent / "check_task_briefs.py"),
|
|
1822
2172
|
"--repo", str(primary), "--base", isolation.current_sha(primary),
|
|
1823
|
-
"--tasks-dir", f"{options.project_dir}/tasks"
|
|
2173
|
+
"--tasks-dir", f"{options.project_dir}/tasks",
|
|
2174
|
+
"--project-dir", options.project_dir], capture_output=True, encoding="utf-8", errors="replace")
|
|
1824
2175
|
receipt["steps"].append({"script": "check_task_briefs.py", "exit_code": lint.returncode,
|
|
1825
2176
|
"stdout": lint.stdout, "stderr": lint.stderr})
|
|
1826
2177
|
if lint.returncode:
|
|
@@ -1935,7 +2286,11 @@ def positive_int(value: str) -> int:
|
|
|
1935
2286
|
|
|
1936
2287
|
|
|
1937
2288
|
def default_resource(name: str) -> Optional[Path]:
|
|
1938
|
-
|
|
2289
|
+
script_root = Path(__file__).resolve().parent.parent
|
|
2290
|
+
candidate = script_root / name
|
|
2291
|
+
if candidate.is_file():
|
|
2292
|
+
return candidate
|
|
2293
|
+
candidate = script_root / "skills" / "gsd-path" / name
|
|
1939
2294
|
return candidate if candidate.is_file() else None
|
|
1940
2295
|
|
|
1941
2296
|
|
|
@@ -2026,6 +2381,7 @@ def main(argv=None) -> int:
|
|
|
2026
2381
|
if arguments.action == "_child":
|
|
2027
2382
|
return child_main(arguments.state)
|
|
2028
2383
|
primary = arguments.repo.resolve()
|
|
2384
|
+
_activate_pinned_runtime(primary)
|
|
2029
2385
|
lock = None
|
|
2030
2386
|
try:
|
|
2031
2387
|
if arguments.action != "status":
|
|
@@ -2078,8 +2434,22 @@ def main(argv=None) -> int:
|
|
|
2078
2434
|
raise DriverStop(f"task {arguments.task_id} dispatch record is {record['outcome']}; "
|
|
2079
2435
|
"use round")
|
|
2080
2436
|
if record.get("finished_at") is None:
|
|
2081
|
-
|
|
2082
|
-
|
|
2437
|
+
if child_running(record):
|
|
2438
|
+
raise DriverStop(f"task {arguments.task_id} still has a running child")
|
|
2439
|
+
if record.get("command"):
|
|
2440
|
+
raise DriverStop(f"task {arguments.task_id} still has a running child")
|
|
2441
|
+
live = state_from_task(primary, arguments.task_id)
|
|
2442
|
+
merged = dict(record, **live)
|
|
2443
|
+
merged["_path"] = record["_path"]
|
|
2444
|
+
try:
|
|
2445
|
+
landing = finish_task(primary, merged)
|
|
2446
|
+
except STOP_ERRORS as error:
|
|
2447
|
+
update_state(record, outcome="blocked", reason=str(error))
|
|
2448
|
+
raise
|
|
2449
|
+
update_state(record, outcome="landed", commit=landing["commit"])
|
|
2450
|
+
current.receipt["landed"].append(landing)
|
|
2451
|
+
else:
|
|
2452
|
+
current.classify(record)
|
|
2083
2453
|
else: # a coder dispatched by hand: derive the isolate from the task frontmatter
|
|
2084
2454
|
current.receipt["landed"].append(
|
|
2085
2455
|
finish_task(primary, native or state_from_task(primary, arguments.task_id)))
|
package/scripts/git_guard.py
CHANGED
|
@@ -20,7 +20,7 @@ sys.path.insert(0, str(_HERE / "runtime" if (_HERE / "runtime" / "isolation.py")
|
|
|
20
20
|
sys.dont_write_bytecode = True # a hook must not leave __pycache__ in the worktree
|
|
21
21
|
try:
|
|
22
22
|
from isolation import BOOKKEEPING_PREFIXES, NULL_SHA, _landing_state, task_frontmatter
|
|
23
|
-
from pipeline_git import is_ship_subject, ship_commit_body, task_commit_body
|
|
23
|
+
from pipeline_git import is_ship_subject, ship_body_matches, ship_commit_body, task_commit_body
|
|
24
24
|
from pipeline_state import _completion_status
|
|
25
25
|
import members
|
|
26
26
|
except ImportError as error: # pragma: no cover - broken install
|
|
@@ -82,7 +82,7 @@ INTEGRATION_STATE_FIELDS = LEGACY_STATE_FIELDS | {
|
|
|
82
82
|
"integration",
|
|
83
83
|
}
|
|
84
84
|
PROVENANCE_STATE_FIELDS = INTEGRATION_STATE_FIELDS | {"integration_source"}
|
|
85
|
-
INTEGRATION_MODES = {"direct", "pull-request"}
|
|
85
|
+
INTEGRATION_MODES = {"direct", "pull-request", "external-landing"}
|
|
86
86
|
INTEGRATION_SOURCES = {"default", "milestone"}
|
|
87
87
|
|
|
88
88
|
|
|
@@ -311,7 +311,7 @@ def ship_contract_violations(entries, subject, body, new_archives, state):
|
|
|
311
311
|
if [row["name"] for row in rows] != locked or any(row["status"] != "integrated" for row in rows):
|
|
312
312
|
found.append("ship commit requires every locked member closed; run archive_milestone.py close-members")
|
|
313
313
|
expected_body = ship_commit_body(archive, reviewed_head, rows).rstrip("\n")
|
|
314
|
-
if body
|
|
314
|
+
if not ship_body_matches(body, expected_body):
|
|
315
315
|
found.append("ship commit body does not match Archive, Reviewed-HEAD, and closed members")
|
|
316
316
|
changed_paths = {
|
|
317
317
|
new if code in "CR" else old
|