@opengsd/gsd-path 1.4.0 → 1.6.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 +460 -341
- package/DOCS.md +16 -5
- package/FULL.md +67 -6
- package/HOOKS.md +158 -10
- package/MIGRATE.md +12 -0
- package/QUICK.md +4 -2
- package/README.md +136 -116
- package/RUNTIME.md +24 -6
- package/UPDATE.md +39 -3
- package/WORKFLOW.md +80 -4
- package/package.json +5 -2
- package/platforms/shared-agents/dispatch.md +13 -0
- package/scripts/_common.py +256 -2
- package/scripts/archive_milestone.py +14 -13
- package/scripts/build_recovery.py +155 -58
- package/scripts/build_state.py +274 -8
- package/scripts/check_docs_audit.py +77 -4
- package/scripts/check_handoffs.py +635 -57
- package/scripts/check_task_briefs.py +293 -32
- package/scripts/detect_project.py +260 -20
- package/scripts/discussion_records.py +9 -1
- package/scripts/discussion_validate.py +93 -35
- package/scripts/dispatch_driver.py +549 -53
- package/scripts/git_guard.py +27 -8
- package/scripts/guard_hook.py +2630 -210
- package/scripts/install.mjs +99 -11
- package/scripts/install.py +231 -46
- package/scripts/integration.py +244 -65
- package/scripts/isolation.py +539 -70
- package/scripts/loop_run.py +16 -4
- package/scripts/members.py +77 -32
- package/scripts/migrate_core.py +2 -0
- package/scripts/model_policy.py +5 -1
- package/scripts/path_config.py +3 -3
- package/scripts/pipeline_diagnose.py +17 -2
- package/scripts/pipeline_git.py +131 -42
- package/scripts/pipeline_state.py +139 -39
- package/scripts/pipeline_undo.py +118 -16
- package/scripts/promote_lookahead.py +3 -3
- package/scripts/review_findings.py +180 -44
- package/scripts/roadmap.py +9 -1
- package/scripts/runtime_store.py +21 -2
- package/scripts/skill-resources.json +9 -2
- package/scripts/state_checkpoint.py +39 -36
- package/scripts/state_promote.py +15 -4
- package/scripts/status_runtime.py +22 -4
- package/scripts/task_context.py +14 -3
- package/scripts/wizard.mjs +1 -0
- package/scripts/workflow_run.py +14 -4
- package/scripts/worktree_paths.py +54 -2
- package/skills/gsd-path/DEFINE.md +34 -5
- package/skills/gsd-path/PLAN.md +14 -3
- package/skills/gsd-path/RESEARCH.md +11 -0
- package/skills/gsd-path/ROADMAP.md +16 -6
- package/skills/gsd-path/SHIP.md +32 -5
- package/skills/gsd-path/SKILL.md +52 -24
- package/skills/gsd-path/references/build-native.md +112 -10
- package/skills/gsd-path/references/build-recovery.md +13 -0
- package/skills/gsd-path/references/coder.md +7 -3
- package/skills/gsd-path/references/dispatch.md +13 -0
- package/skills/gsd-path/references/docs-auditor.md +5 -0
- package/skills/gsd-path/references/operating-rules.md +11 -3
- package/skills/gsd-path/references/planner.md +26 -4
- package/skills/gsd-path/references/reviewer-final.md +20 -2
- package/skills/gsd-path/references/reviewer-wave.md +56 -12
- package/skills/gsd-path/references/reviewer.md +3 -1
- package/skills/gsd-path/references/runtime-selection.md +9 -0
- package/skills/gsd-path/references/spec-probes.md +150 -0
- package/skills/gsd-path/scripts/_common.py +256 -2
- package/skills/gsd-path/scripts/archive_milestone.py +14 -13
- package/skills/gsd-path/scripts/build_recovery.py +155 -58
- package/skills/gsd-path/scripts/build_state.py +274 -8
- package/skills/gsd-path/scripts/check_docs_audit.py +77 -4
- package/skills/gsd-path/scripts/check_handoffs.py +635 -57
- package/skills/gsd-path/scripts/check_task_briefs.py +293 -32
- package/skills/gsd-path/scripts/detect_project.py +260 -20
- package/skills/gsd-path/scripts/discussion_records.py +9 -1
- package/skills/gsd-path/scripts/discussion_validate.py +93 -35
- package/skills/gsd-path/scripts/dispatch_driver.py +549 -53
- package/skills/gsd-path/scripts/git_guard.py +27 -8
- package/skills/gsd-path/scripts/integration.py +244 -65
- package/skills/gsd-path/scripts/isolation.py +539 -70
- package/skills/gsd-path/scripts/loop_run.py +16 -4
- package/skills/gsd-path/scripts/members.py +77 -32
- package/skills/gsd-path/scripts/model_policy.py +5 -1
- package/skills/gsd-path/scripts/path_config.py +3 -3
- package/skills/gsd-path/scripts/pipeline_git.py +131 -42
- package/skills/gsd-path/scripts/pipeline_state.py +139 -39
- package/skills/gsd-path/scripts/promote_lookahead.py +3 -3
- package/skills/gsd-path/scripts/review_findings.py +180 -44
- package/skills/gsd-path/scripts/roadmap.py +9 -1
- package/skills/gsd-path/scripts/state_checkpoint.py +39 -36
- package/skills/gsd-path/scripts/state_promote.py +15 -4
- package/skills/gsd-path/scripts/status_runtime.py +22 -4
- package/skills/gsd-path/scripts/task_context.py +14 -3
- package/skills/gsd-path/scripts/workflow_run.py +14 -4
- 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 +2 -1
- package/skills/gsd-path/templates/task.md +1 -0
- package/skills/gsd-path/templates/wave-review.md +28 -9
- package/skills/gsd-path-build/references/build-native.md +112 -10
- package/skills/gsd-path-build/references/build-recovery.md +13 -0
- package/skills/gsd-path-build/references/coder.md +7 -3
- package/skills/gsd-path-build/references/dispatch.md +13 -0
- package/skills/gsd-path-build/references/operating-rules.md +11 -3
- package/skills/gsd-path-build/references/reviewer-final.md +20 -2
- package/skills/gsd-path-build/references/reviewer-wave.md +56 -12
- package/skills/gsd-path-build/references/reviewer.md +3 -1
- package/skills/gsd-path-build/references/runtime-selection.md +9 -0
- package/skills/gsd-path-build/references/spec-probes.md +150 -0
- package/skills/gsd-path-build/scripts/_common.py +256 -2
- package/skills/gsd-path-build/scripts/archive_milestone.py +14 -13
- package/skills/gsd-path-build/scripts/build_recovery.py +155 -58
- package/skills/gsd-path-build/scripts/build_state.py +274 -8
- package/skills/gsd-path-build/scripts/check_handoffs.py +635 -57
- package/skills/gsd-path-build/scripts/check_task_briefs.py +293 -32
- package/skills/gsd-path-build/scripts/detect_project.py +260 -20
- package/skills/gsd-path-build/scripts/discussion_records.py +9 -1
- package/skills/gsd-path-build/scripts/discussion_validate.py +93 -35
- package/skills/gsd-path-build/scripts/dispatch_driver.py +549 -53
- package/skills/gsd-path-build/scripts/integration.py +244 -65
- package/skills/gsd-path-build/scripts/isolation.py +539 -70
- package/skills/gsd-path-build/scripts/loop_run.py +16 -4
- package/skills/gsd-path-build/scripts/members.py +77 -32
- package/skills/gsd-path-build/scripts/model_policy.py +5 -1
- package/skills/gsd-path-build/scripts/path_config.py +3 -3
- package/skills/gsd-path-build/scripts/pipeline_git.py +131 -42
- package/skills/gsd-path-build/scripts/pipeline_state.py +139 -39
- package/skills/gsd-path-build/scripts/promote_lookahead.py +3 -3
- package/skills/gsd-path-build/scripts/review_findings.py +180 -44
- package/skills/gsd-path-build/scripts/roadmap.py +9 -1
- package/skills/gsd-path-build/scripts/state_checkpoint.py +39 -36
- package/skills/gsd-path-build/scripts/state_promote.py +15 -4
- package/skills/gsd-path-build/scripts/task_context.py +14 -3
- package/skills/gsd-path-build/scripts/workflow_run.py +14 -4
- package/skills/gsd-path-build/scripts/worktree_paths.py +54 -2
- package/skills/gsd-path-build/templates/task.md +1 -0
- package/skills/gsd-path-build/templates/wave-review.md +28 -9
- package/skills/gsd-path-decide/references/dispatch.md +13 -0
- package/skills/gsd-path-decide/references/operating-rules.md +11 -3
- package/skills/gsd-path-decide/references/runtime-selection.md +9 -0
- package/skills/gsd-path-decide/scripts/_common.py +256 -2
- package/skills/gsd-path-decide/scripts/archive_milestone.py +14 -13
- package/skills/gsd-path-decide/scripts/build_recovery.py +155 -58
- package/skills/gsd-path-decide/scripts/check_handoffs.py +635 -57
- package/skills/gsd-path-decide/scripts/check_task_briefs.py +293 -32
- package/skills/gsd-path-decide/scripts/detect_project.py +260 -20
- package/skills/gsd-path-decide/scripts/discussion_records.py +9 -1
- package/skills/gsd-path-decide/scripts/discussion_validate.py +93 -35
- package/skills/gsd-path-decide/scripts/integration.py +244 -65
- package/skills/gsd-path-decide/scripts/isolation.py +539 -70
- package/skills/gsd-path-decide/scripts/loop_run.py +16 -4
- package/skills/gsd-path-decide/scripts/members.py +77 -32
- package/skills/gsd-path-decide/scripts/model_policy.py +5 -1
- package/skills/gsd-path-decide/scripts/path_config.py +3 -3
- package/skills/gsd-path-decide/scripts/pipeline_git.py +131 -42
- package/skills/gsd-path-decide/scripts/pipeline_state.py +139 -39
- package/skills/gsd-path-decide/scripts/promote_lookahead.py +3 -3
- package/skills/gsd-path-decide/scripts/roadmap.py +9 -1
- package/skills/gsd-path-decide/scripts/state_checkpoint.py +39 -36
- package/skills/gsd-path-decide/scripts/state_promote.py +15 -4
- package/skills/gsd-path-decide/scripts/worktree_paths.py +54 -2
- package/skills/gsd-path-define/SKILL.md +34 -5
- package/skills/gsd-path-define/references/build-recovery.md +13 -0
- package/skills/gsd-path-define/references/operating-rules.md +11 -3
- package/skills/gsd-path-define/references/runtime-selection.md +9 -0
- package/skills/gsd-path-define/references/spec-probes.md +150 -0
- package/skills/gsd-path-define/scripts/_common.py +256 -2
- package/skills/gsd-path-define/scripts/archive_milestone.py +14 -13
- package/skills/gsd-path-define/scripts/build_recovery.py +155 -58
- package/skills/gsd-path-define/scripts/check_handoffs.py +635 -57
- package/skills/gsd-path-define/scripts/check_task_briefs.py +293 -32
- package/skills/gsd-path-define/scripts/detect_project.py +260 -20
- package/skills/gsd-path-define/scripts/discussion_records.py +9 -1
- package/skills/gsd-path-define/scripts/discussion_validate.py +93 -35
- package/skills/gsd-path-define/scripts/integration.py +244 -65
- package/skills/gsd-path-define/scripts/isolation.py +539 -70
- package/skills/gsd-path-define/scripts/loop_run.py +16 -4
- package/skills/gsd-path-define/scripts/members.py +77 -32
- package/skills/gsd-path-define/scripts/model_policy.py +5 -1
- package/skills/gsd-path-define/scripts/path_config.py +3 -3
- package/skills/gsd-path-define/scripts/pipeline_git.py +131 -42
- package/skills/gsd-path-define/scripts/pipeline_state.py +139 -39
- package/skills/gsd-path-define/scripts/promote_lookahead.py +3 -3
- package/skills/gsd-path-define/scripts/roadmap.py +9 -1
- package/skills/gsd-path-define/scripts/state_checkpoint.py +39 -36
- package/skills/gsd-path-define/scripts/state_promote.py +15 -4
- 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 +2 -1
- package/skills/gsd-path-discuss/references/operating-rules.md +11 -3
- package/skills/gsd-path-discuss/references/runtime-selection.md +9 -0
- package/skills/gsd-path-discuss/scripts/_common.py +256 -2
- package/skills/gsd-path-discuss/scripts/discussion_records.py +9 -1
- package/skills/gsd-path-discuss/scripts/loop_run.py +16 -4
- package/skills/gsd-path-discuss/scripts/model_policy.py +5 -1
- 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/references/operating-rules.md +11 -3
- package/skills/gsd-path-docs-audit/references/runtime-selection.md +9 -0
- package/skills/gsd-path-docs-audit/scripts/_common.py +256 -2
- package/skills/gsd-path-docs-audit/scripts/archive_milestone.py +14 -13
- package/skills/gsd-path-docs-audit/scripts/build_recovery.py +155 -58
- package/skills/gsd-path-docs-audit/scripts/check_docs_audit.py +77 -4
- package/skills/gsd-path-docs-audit/scripts/check_handoffs.py +635 -57
- package/skills/gsd-path-docs-audit/scripts/check_task_briefs.py +293 -32
- package/skills/gsd-path-docs-audit/scripts/detect_project.py +260 -20
- package/skills/gsd-path-docs-audit/scripts/discussion_records.py +9 -1
- package/skills/gsd-path-docs-audit/scripts/discussion_validate.py +93 -35
- package/skills/gsd-path-docs-audit/scripts/integration.py +244 -65
- package/skills/gsd-path-docs-audit/scripts/isolation.py +539 -70
- package/skills/gsd-path-docs-audit/scripts/loop_run.py +16 -4
- package/skills/gsd-path-docs-audit/scripts/members.py +77 -32
- package/skills/gsd-path-docs-audit/scripts/model_policy.py +5 -1
- package/skills/gsd-path-docs-audit/scripts/path_config.py +3 -3
- package/skills/gsd-path-docs-audit/scripts/pipeline_git.py +131 -42
- package/skills/gsd-path-docs-audit/scripts/pipeline_state.py +139 -39
- package/skills/gsd-path-docs-audit/scripts/promote_lookahead.py +3 -3
- package/skills/gsd-path-docs-audit/scripts/roadmap.py +9 -1
- package/skills/gsd-path-docs-audit/scripts/state_checkpoint.py +39 -36
- package/skills/gsd-path-docs-audit/scripts/state_promote.py +15 -4
- 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/references/operating-rules.md +11 -3
- package/skills/gsd-path-forensics/references/runtime-selection.md +9 -0
- package/skills/gsd-path-forensics/scripts/_common.py +256 -2
- package/skills/gsd-path-forensics/scripts/discussion_records.py +9 -1
- package/skills/gsd-path-forensics/scripts/loop_run.py +16 -4
- package/skills/gsd-path-forensics/scripts/model_policy.py +5 -1
- package/skills/gsd-path-forensics/scripts/path_config.py +3 -3
- package/skills/gsd-path-forensics/scripts/pipeline_diagnose.py +17 -2
- package/skills/gsd-path-forensics/scripts/pipeline_undo.py +118 -16
- 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/references/operating-rules.md +11 -3
- package/skills/gsd-path-inspect/references/runtime-selection.md +9 -0
- package/skills/gsd-path-inspect/scripts/_common.py +256 -2
- package/skills/gsd-path-inspect/scripts/archive_milestone.py +14 -13
- package/skills/gsd-path-inspect/scripts/build_recovery.py +155 -58
- package/skills/gsd-path-inspect/scripts/check_docs_audit.py +77 -4
- package/skills/gsd-path-inspect/scripts/check_handoffs.py +635 -57
- package/skills/gsd-path-inspect/scripts/check_task_briefs.py +293 -32
- package/skills/gsd-path-inspect/scripts/detect_project.py +260 -20
- package/skills/gsd-path-inspect/scripts/discussion_records.py +9 -1
- package/skills/gsd-path-inspect/scripts/discussion_validate.py +93 -35
- package/skills/gsd-path-inspect/scripts/integration.py +244 -65
- package/skills/gsd-path-inspect/scripts/isolation.py +539 -70
- package/skills/gsd-path-inspect/scripts/loop_run.py +16 -4
- package/skills/gsd-path-inspect/scripts/members.py +77 -32
- package/skills/gsd-path-inspect/scripts/model_policy.py +5 -1
- package/skills/gsd-path-inspect/scripts/path_config.py +3 -3
- package/skills/gsd-path-inspect/scripts/pipeline_git.py +131 -42
- package/skills/gsd-path-inspect/scripts/pipeline_state.py +139 -39
- package/skills/gsd-path-inspect/scripts/promote_lookahead.py +3 -3
- package/skills/gsd-path-inspect/scripts/roadmap.py +9 -1
- package/skills/gsd-path-inspect/scripts/state_checkpoint.py +39 -36
- package/skills/gsd-path-inspect/scripts/state_promote.py +15 -4
- package/skills/gsd-path-inspect/scripts/workflow_run.py +14 -4
- 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 +2 -1
- package/skills/gsd-path-loop/references/operating-rules.md +11 -3
- package/skills/gsd-path-loop/references/runtime-selection.md +9 -0
- package/skills/gsd-path-loop/scripts/_common.py +256 -2
- package/skills/gsd-path-loop/scripts/discussion_records.py +9 -1
- package/skills/gsd-path-loop/scripts/loop_run.py +16 -4
- package/skills/gsd-path-loop/scripts/model_policy.py +5 -1
- 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/SKILL.md +8 -0
- package/skills/gsd-path-migrate/references/operating-rules.md +11 -3
- package/skills/gsd-path-migrate/references/runtime-selection.md +9 -0
- package/skills/gsd-path-migrate/scripts/_common.py +256 -2
- package/skills/gsd-path-migrate/scripts/discussion_records.py +9 -1
- package/skills/gsd-path-migrate/scripts/loop_run.py +16 -4
- package/skills/gsd-path-migrate/scripts/migrate_core.py +2 -0
- package/skills/gsd-path-migrate/scripts/model_policy.py +5 -1
- package/skills/gsd-path-migrate/scripts/path_config.py +3 -3
- package/skills/gsd-path-plan/SKILL.md +14 -3
- package/skills/gsd-path-plan/references/build-recovery.md +13 -0
- package/skills/gsd-path-plan/references/dispatch.md +13 -0
- package/skills/gsd-path-plan/references/operating-rules.md +11 -3
- package/skills/gsd-path-plan/references/planner.md +26 -4
- package/skills/gsd-path-plan/references/runtime-selection.md +9 -0
- package/skills/gsd-path-plan/references/spec-probes.md +150 -0
- package/skills/gsd-path-plan/scripts/_common.py +256 -2
- package/skills/gsd-path-plan/scripts/archive_milestone.py +14 -13
- package/skills/gsd-path-plan/scripts/build_recovery.py +155 -58
- package/skills/gsd-path-plan/scripts/check_handoffs.py +635 -57
- package/skills/gsd-path-plan/scripts/check_task_briefs.py +293 -32
- package/skills/gsd-path-plan/scripts/detect_project.py +260 -20
- package/skills/gsd-path-plan/scripts/discussion_records.py +9 -1
- package/skills/gsd-path-plan/scripts/discussion_validate.py +93 -35
- package/skills/gsd-path-plan/scripts/integration.py +244 -65
- package/skills/gsd-path-plan/scripts/isolation.py +539 -70
- package/skills/gsd-path-plan/scripts/loop_run.py +16 -4
- package/skills/gsd-path-plan/scripts/members.py +77 -32
- package/skills/gsd-path-plan/scripts/model_policy.py +5 -1
- package/skills/gsd-path-plan/scripts/path_config.py +3 -3
- package/skills/gsd-path-plan/scripts/pipeline_git.py +131 -42
- package/skills/gsd-path-plan/scripts/pipeline_state.py +139 -39
- package/skills/gsd-path-plan/scripts/promote_lookahead.py +3 -3
- package/skills/gsd-path-plan/scripts/roadmap.py +9 -1
- package/skills/gsd-path-plan/scripts/state_checkpoint.py +39 -36
- package/skills/gsd-path-plan/scripts/state_promote.py +15 -4
- package/skills/gsd-path-plan/scripts/workflow_run.py +14 -4
- 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-plan/templates/task.md +1 -0
- package/skills/gsd-path-research/SKILL.md +11 -0
- package/skills/gsd-path-research/references/dispatch.md +13 -0
- package/skills/gsd-path-research/references/operating-rules.md +11 -3
- package/skills/gsd-path-research/references/runtime-selection.md +9 -0
- package/skills/gsd-path-research/scripts/_common.py +256 -2
- package/skills/gsd-path-research/scripts/archive_milestone.py +14 -13
- package/skills/gsd-path-research/scripts/build_recovery.py +155 -58
- package/skills/gsd-path-research/scripts/check_handoffs.py +635 -57
- package/skills/gsd-path-research/scripts/check_task_briefs.py +293 -32
- package/skills/gsd-path-research/scripts/detect_project.py +260 -20
- package/skills/gsd-path-research/scripts/discussion_records.py +9 -1
- package/skills/gsd-path-research/scripts/discussion_validate.py +93 -35
- package/skills/gsd-path-research/scripts/integration.py +244 -65
- package/skills/gsd-path-research/scripts/isolation.py +539 -70
- package/skills/gsd-path-research/scripts/loop_run.py +16 -4
- package/skills/gsd-path-research/scripts/members.py +77 -32
- package/skills/gsd-path-research/scripts/model_policy.py +5 -1
- package/skills/gsd-path-research/scripts/path_config.py +3 -3
- package/skills/gsd-path-research/scripts/pipeline_git.py +131 -42
- package/skills/gsd-path-research/scripts/pipeline_state.py +139 -39
- package/skills/gsd-path-research/scripts/promote_lookahead.py +3 -3
- package/skills/gsd-path-research/scripts/roadmap.py +9 -1
- package/skills/gsd-path-research/scripts/state_checkpoint.py +39 -36
- package/skills/gsd-path-research/scripts/state_promote.py +15 -4
- 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/SKILL.md +16 -6
- package/skills/gsd-path-roadmap/references/dispatch.md +13 -0
- package/skills/gsd-path-roadmap/references/operating-rules.md +11 -3
- package/skills/gsd-path-roadmap/references/runtime-selection.md +9 -0
- package/skills/gsd-path-roadmap/scripts/_common.py +256 -2
- package/skills/gsd-path-roadmap/scripts/archive_milestone.py +14 -13
- package/skills/gsd-path-roadmap/scripts/build_recovery.py +155 -58
- package/skills/gsd-path-roadmap/scripts/check_handoffs.py +635 -57
- package/skills/gsd-path-roadmap/scripts/check_task_briefs.py +293 -32
- package/skills/gsd-path-roadmap/scripts/detect_project.py +260 -20
- package/skills/gsd-path-roadmap/scripts/discussion_records.py +9 -1
- package/skills/gsd-path-roadmap/scripts/discussion_validate.py +93 -35
- package/skills/gsd-path-roadmap/scripts/integration.py +244 -65
- package/skills/gsd-path-roadmap/scripts/isolation.py +539 -70
- package/skills/gsd-path-roadmap/scripts/loop_run.py +16 -4
- package/skills/gsd-path-roadmap/scripts/members.py +77 -32
- package/skills/gsd-path-roadmap/scripts/model_policy.py +5 -1
- package/skills/gsd-path-roadmap/scripts/path_config.py +3 -3
- package/skills/gsd-path-roadmap/scripts/pipeline_git.py +131 -42
- package/skills/gsd-path-roadmap/scripts/pipeline_state.py +139 -39
- package/skills/gsd-path-roadmap/scripts/promote_lookahead.py +3 -3
- package/skills/gsd-path-roadmap/scripts/roadmap.py +9 -1
- package/skills/gsd-path-roadmap/scripts/state_checkpoint.py +39 -36
- package/skills/gsd-path-roadmap/scripts/state_promote.py +15 -4
- package/skills/gsd-path-roadmap/scripts/worktree_paths.py +54 -2
- package/skills/gsd-path-ship/SKILL.md +32 -5
- package/skills/gsd-path-ship/references/dispatch.md +13 -0
- package/skills/gsd-path-ship/references/operating-rules.md +11 -3
- package/skills/gsd-path-ship/references/reviewer-final.md +20 -2
- package/skills/gsd-path-ship/references/reviewer-wave.md +56 -12
- package/skills/gsd-path-ship/references/reviewer.md +3 -1
- package/skills/gsd-path-ship/references/runtime-selection.md +9 -0
- package/skills/gsd-path-ship/references/spec-probes.md +150 -0
- package/skills/gsd-path-ship/scripts/_common.py +256 -2
- package/skills/gsd-path-ship/scripts/archive_milestone.py +14 -13
- package/skills/gsd-path-ship/scripts/build_recovery.py +155 -58
- package/skills/gsd-path-ship/scripts/build_state.py +274 -8
- package/skills/gsd-path-ship/scripts/check_handoffs.py +635 -57
- package/skills/gsd-path-ship/scripts/check_task_briefs.py +293 -32
- package/skills/gsd-path-ship/scripts/detect_project.py +260 -20
- package/skills/gsd-path-ship/scripts/discussion_records.py +9 -1
- package/skills/gsd-path-ship/scripts/discussion_validate.py +93 -35
- package/skills/gsd-path-ship/scripts/integration.py +244 -65
- package/skills/gsd-path-ship/scripts/isolation.py +539 -70
- package/skills/gsd-path-ship/scripts/loop_run.py +16 -4
- package/skills/gsd-path-ship/scripts/members.py +77 -32
- package/skills/gsd-path-ship/scripts/model_policy.py +5 -1
- package/skills/gsd-path-ship/scripts/path_config.py +3 -3
- package/skills/gsd-path-ship/scripts/pipeline_git.py +131 -42
- package/skills/gsd-path-ship/scripts/pipeline_state.py +139 -39
- package/skills/gsd-path-ship/scripts/promote_lookahead.py +3 -3
- package/skills/gsd-path-ship/scripts/roadmap.py +9 -1
- package/skills/gsd-path-ship/scripts/state_checkpoint.py +39 -36
- package/skills/gsd-path-ship/scripts/state_promote.py +15 -4
- package/skills/gsd-path-ship/scripts/workflow_run.py +14 -4
- 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 +28 -9
- package/skills/gsd-path-undo/references/operating-rules.md +11 -3
- package/skills/gsd-path-undo/references/runtime-selection.md +9 -0
- package/skills/gsd-path-undo/scripts/_common.py +256 -2
- package/skills/gsd-path-undo/scripts/discussion_records.py +9 -1
- package/skills/gsd-path-undo/scripts/loop_run.py +16 -4
- package/skills/gsd-path-undo/scripts/model_policy.py +5 -1
- package/skills/gsd-path-undo/scripts/path_config.py +3 -3
- package/skills/gsd-path-undo/scripts/pipeline_undo.py +118 -16
- package/skills/gsd-path-undo/scripts/worktree_paths.py +54 -2
- package/skills/path/DEFINE.md +34 -5
- package/skills/path/PLAN.md +14 -3
- package/skills/path/RESEARCH.md +11 -0
- package/skills/path/ROADMAP.md +16 -6
- package/skills/path/SHIP.md +32 -5
- package/skills/path/SKILL.md +52 -24
- package/skills/path/references/build-native.md +112 -10
- package/skills/path/references/build-recovery.md +13 -0
- package/skills/path/references/coder.md +7 -3
- package/skills/path/references/dispatch.md +13 -0
- package/skills/path/references/docs-auditor.md +5 -0
- package/skills/path/references/operating-rules.md +11 -3
- package/skills/path/references/planner.md +26 -4
- package/skills/path/references/reviewer-final.md +20 -2
- package/skills/path/references/reviewer-wave.md +56 -12
- package/skills/path/references/reviewer.md +3 -1
- package/skills/path/references/runtime-selection.md +9 -0
- package/skills/path/references/spec-probes.md +150 -0
- package/skills/path/scripts/_common.py +256 -2
- package/skills/path/scripts/archive_milestone.py +14 -13
- package/skills/path/scripts/build_recovery.py +155 -58
- package/skills/path/scripts/build_state.py +274 -8
- package/skills/path/scripts/check_docs_audit.py +77 -4
- package/skills/path/scripts/check_handoffs.py +635 -57
- package/skills/path/scripts/check_task_briefs.py +293 -32
- package/skills/path/scripts/detect_project.py +260 -20
- package/skills/path/scripts/discussion_records.py +9 -1
- package/skills/path/scripts/discussion_validate.py +93 -35
- package/skills/path/scripts/dispatch_driver.py +549 -53
- package/skills/path/scripts/git_guard.py +27 -8
- package/skills/path/scripts/integration.py +244 -65
- package/skills/path/scripts/isolation.py +539 -70
- package/skills/path/scripts/loop_run.py +16 -4
- package/skills/path/scripts/members.py +77 -32
- package/skills/path/scripts/model_policy.py +5 -1
- package/skills/path/scripts/path_config.py +3 -3
- package/skills/path/scripts/pipeline_git.py +131 -42
- package/skills/path/scripts/pipeline_state.py +139 -39
- package/skills/path/scripts/promote_lookahead.py +3 -3
- package/skills/path/scripts/review_findings.py +180 -44
- package/skills/path/scripts/roadmap.py +9 -1
- package/skills/path/scripts/state_checkpoint.py +39 -36
- package/skills/path/scripts/state_promote.py +15 -4
- package/skills/path/scripts/status_runtime.py +22 -4
- package/skills/path/scripts/task_context.py +14 -3
- package/skills/path/scripts/workflow_run.py +14 -4
- 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 +2 -1
- package/skills/path/templates/task.md +1 -0
- package/skills/path/templates/wave-review.md +28 -9
- 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,21 +16,24 @@ 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
|
|
25
27
|
import time
|
|
26
28
|
from datetime import datetime, timezone
|
|
27
29
|
from pathlib import Path
|
|
28
|
-
from typing import BinaryIO, Callable, Dict, List, Optional
|
|
30
|
+
from typing import BinaryIO, Callable, Dict, List, Optional, Tuple
|
|
29
31
|
|
|
30
32
|
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,99 @@ 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
|
+
"roadmap",
|
|
81
|
+
"review_panel",
|
|
82
|
+
"loop_run",
|
|
83
|
+
"pipeline_git",
|
|
84
|
+
"worktree_paths",
|
|
85
|
+
"build_recovery",
|
|
86
|
+
"isolation",
|
|
87
|
+
"pipeline_state",
|
|
88
|
+
"state_checkpoint",
|
|
89
|
+
"check_task_briefs",
|
|
90
|
+
"build_state",
|
|
91
|
+
"check_handoffs",
|
|
92
|
+
"task_context",
|
|
93
|
+
# discussion_validate and archive_milestone import each other: the second
|
|
94
|
+
# reload binds the reloaded ArchiveError that STOP_ERRORS catches.
|
|
95
|
+
"discussion_validate",
|
|
96
|
+
"archive_milestone",
|
|
97
|
+
"discussion_validate",
|
|
98
|
+
"review_findings",
|
|
99
|
+
"model_policy",
|
|
100
|
+
)
|
|
74
101
|
RECOVERY_BLOCKED = "recovery blocked"
|
|
102
|
+
LEFTOVER_ISOLATE = "task {} is landed; remove its leftover isolate: {}"
|
|
75
103
|
# The stops the build contract answers with build/blocked when the round raises them (steps 1 and 2);
|
|
76
104
|
# the same stops from review, panel, or skeptics stay with the parent.
|
|
77
105
|
BLOCKING_STOPS = {"dependency-deadlock": "dependency deadlock", RECOVERY_BLOCKED: "blocked recovery"}
|
|
78
106
|
|
|
79
107
|
|
|
108
|
+
def _bundled_helpers() -> bool:
|
|
109
|
+
return "scripts.isolation" not in sys.modules
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _refresh_stop_errors() -> None:
|
|
113
|
+
global STOP_ERRORS
|
|
114
|
+
STOP_ERRORS = (DriverStop, isolation.IsolationError, build_state.BuildStateError,
|
|
115
|
+
contracts.HandoffError, review_findings.ReviewFindingsError,
|
|
116
|
+
pipeline_state.PipelineStateError, archive_milestone.ArchiveError,
|
|
117
|
+
model_policy.PolicyError)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def _reload_pinned_modules(runtime_root: Path) -> None:
|
|
121
|
+
global isolation, build_state, task_context, contracts, pipeline_state
|
|
122
|
+
global archive_milestone, review_findings, model_policy
|
|
123
|
+
if _bundled_helpers():
|
|
124
|
+
path = str(runtime_root)
|
|
125
|
+
if path not in sys.path:
|
|
126
|
+
sys.path.insert(0, path)
|
|
127
|
+
for name in _RUNTIME_ACTIVATION_ORDER:
|
|
128
|
+
module = sys.modules.get(name)
|
|
129
|
+
if module is not None:
|
|
130
|
+
importlib.reload(module)
|
|
131
|
+
isolation = sys.modules["isolation"]
|
|
132
|
+
build_state = sys.modules["build_state"]
|
|
133
|
+
task_context = sys.modules["task_context"]
|
|
134
|
+
contracts = sys.modules["check_handoffs"]
|
|
135
|
+
pipeline_state = sys.modules["pipeline_state"]
|
|
136
|
+
archive_milestone = sys.modules["archive_milestone"]
|
|
137
|
+
review_findings = sys.modules["review_findings"]
|
|
138
|
+
model_policy = sys.modules["model_policy"]
|
|
139
|
+
else:
|
|
140
|
+
import scripts
|
|
141
|
+
path = str(runtime_root)
|
|
142
|
+
if path not in scripts.__path__:
|
|
143
|
+
scripts.__path__ = [path, *[entry for entry in scripts.__path__ if entry != path]]
|
|
144
|
+
for name in _RUNTIME_ACTIVATION_ORDER:
|
|
145
|
+
module = sys.modules.get(f"scripts.{name}")
|
|
146
|
+
if module is not None:
|
|
147
|
+
importlib.reload(module)
|
|
148
|
+
isolation = scripts.isolation
|
|
149
|
+
build_state = scripts.build_state
|
|
150
|
+
task_context = scripts.task_context
|
|
151
|
+
contracts = scripts.check_handoffs
|
|
152
|
+
pipeline_state = scripts.pipeline_state
|
|
153
|
+
archive_milestone = scripts.archive_milestone
|
|
154
|
+
review_findings = scripts.review_findings
|
|
155
|
+
model_policy = scripts.model_policy
|
|
156
|
+
_refresh_stop_errors()
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def _activate_pinned_runtime(repo: Path) -> None:
|
|
160
|
+
if not os.path.lexists(repo / ".gsd-path/runtime.json"):
|
|
161
|
+
return
|
|
162
|
+
resolved = subprocess.run(
|
|
163
|
+
[sys.executable, "-B", str(repo / ".gsd-path/status_runtime.py"),
|
|
164
|
+
"--repo", str(repo), "--runtime-path"],
|
|
165
|
+
cwd=repo, capture_output=True, encoding="utf-8", errors="replace")
|
|
166
|
+
if resolved.returncode:
|
|
167
|
+
raise DriverStop(resolved.stderr.strip() or "pinned runtime resolution failed")
|
|
168
|
+
_reload_pinned_modules(Path(resolved.stdout.strip()))
|
|
169
|
+
|
|
170
|
+
|
|
80
171
|
def now() -> str:
|
|
81
172
|
return datetime.now(timezone.utc).isoformat(timespec="seconds")
|
|
82
173
|
|
|
@@ -107,6 +198,169 @@ def attempts_used(root: Path, task_id: str) -> int:
|
|
|
107
198
|
if load_state(path).get("origin") == "dispatch")
|
|
108
199
|
|
|
109
200
|
|
|
201
|
+
def dispatch_primary(worktree: Path) -> Path:
|
|
202
|
+
"""The bound-branch primary that owns dispatch records for a task checkout."""
|
|
203
|
+
worktree = worktree.resolve()
|
|
204
|
+
if is_bound_branch(isolation.require_attached(worktree)):
|
|
205
|
+
return worktree
|
|
206
|
+
matches = [
|
|
207
|
+
path.resolve()
|
|
208
|
+
for path, branch in isolation._registered_worktrees(worktree).items()
|
|
209
|
+
if branch and is_bound_branch(branch.removeprefix("refs/heads/"))
|
|
210
|
+
]
|
|
211
|
+
if len(matches) != 1:
|
|
212
|
+
raise isolation.IsolationError(
|
|
213
|
+
"dispatch records require exactly one primary worktree for the bound branch")
|
|
214
|
+
return matches[0]
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def should_open_native_shell(root: Path, task_id: str, base: str) -> bool:
|
|
218
|
+
"""Whether activate-task should append a native dispatch attempt record."""
|
|
219
|
+
task_dir = root / task_id
|
|
220
|
+
if not task_dir.is_dir():
|
|
221
|
+
return True
|
|
222
|
+
attempts = sorted(task_dir.glob("attempt-*/state.json"),
|
|
223
|
+
key=lambda path: attempt_number(path.parent))
|
|
224
|
+
if not attempts:
|
|
225
|
+
return True
|
|
226
|
+
latest = read_attempt(attempts[-1])
|
|
227
|
+
if latest.get("outcome") is None and not latest.get("command"):
|
|
228
|
+
return False
|
|
229
|
+
if latest.get("outcome") == "blocked" and str(latest.get("base")) == base:
|
|
230
|
+
return False
|
|
231
|
+
return True
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def open_native_shell(primary: Path, *, task_id: str, base: str, worktree: str, task_file: str,
|
|
235
|
+
task_branch: Optional[str], mode: str, wave: Optional[int] = None,
|
|
236
|
+
title: Optional[str] = None, files: Optional[List[str]] = None,
|
|
237
|
+
sidecar: Optional[object] = None, member: Optional[str] = None,
|
|
238
|
+
member_base: Optional[str] = None, contract_file: Optional[str] = None) -> None:
|
|
239
|
+
"""Open a dispatch attempt record for a host-spawned coder before it runs."""
|
|
240
|
+
root = records_root(primary)
|
|
241
|
+
root.mkdir(parents=True, exist_ok=True)
|
|
242
|
+
record: Dict[str, object] = {
|
|
243
|
+
"task_id": task_id,
|
|
244
|
+
"base": base,
|
|
245
|
+
"worktree": worktree,
|
|
246
|
+
"task_file": task_file,
|
|
247
|
+
"task_branch": task_branch,
|
|
248
|
+
"mode": mode,
|
|
249
|
+
"origin": "dispatch",
|
|
250
|
+
"outcome": None,
|
|
251
|
+
"dispatched_at": now(),
|
|
252
|
+
"native": True,
|
|
253
|
+
}
|
|
254
|
+
if wave is not None:
|
|
255
|
+
record["wave"] = wave
|
|
256
|
+
if title is not None:
|
|
257
|
+
record["title"] = title
|
|
258
|
+
if files is not None:
|
|
259
|
+
record["files"] = files
|
|
260
|
+
if sidecar is not None:
|
|
261
|
+
record["sidecar"] = sidecar
|
|
262
|
+
if member is not None:
|
|
263
|
+
record["member"] = member
|
|
264
|
+
if member_base is not None:
|
|
265
|
+
record["member_base"] = member_base
|
|
266
|
+
if contract_file is not None:
|
|
267
|
+
record["contract_file"] = contract_file
|
|
268
|
+
task_dir = root / task_id
|
|
269
|
+
if not should_open_native_shell(root, task_id, base):
|
|
270
|
+
attempts = sorted(task_dir.glob("attempt-*/state.json"),
|
|
271
|
+
key=lambda path: attempt_number(path.parent))
|
|
272
|
+
if not attempts:
|
|
273
|
+
return
|
|
274
|
+
state_path = attempts[-1]
|
|
275
|
+
state = load_state(state_path)
|
|
276
|
+
if state.get("outcome") is not None or state.get("command"):
|
|
277
|
+
return
|
|
278
|
+
attempt = state.get("attempt")
|
|
279
|
+
state.update(record)
|
|
280
|
+
if attempt is not None:
|
|
281
|
+
state["attempt"] = attempt
|
|
282
|
+
state["outcome"] = None
|
|
283
|
+
for key in ("pid", "finished_at", "exit_code", "timed_out", "reason", "question", "commit",
|
|
284
|
+
"answer", "command", "usage_recorded"):
|
|
285
|
+
state.pop(key, None)
|
|
286
|
+
save_state(state_path, state)
|
|
287
|
+
exit_path = state_path.with_name("exit.json")
|
|
288
|
+
if exit_path.is_file():
|
|
289
|
+
exit_path.unlink()
|
|
290
|
+
return
|
|
291
|
+
attempt = max((attempt_number(path) for path in task_dir.glob("attempt-*")), default=0) + 1
|
|
292
|
+
attempt_dir = task_dir / f"attempt-{attempt}"
|
|
293
|
+
attempt_dir.mkdir(parents=True)
|
|
294
|
+
record["attempt"] = attempt
|
|
295
|
+
save_state(attempt_dir / "state.json", record)
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
def record_native_activation(worktree: Path, activation: Dict[str, object]) -> None:
|
|
299
|
+
"""Record a native task activation in the milestone dispatch ledger."""
|
|
300
|
+
primary = dispatch_primary(worktree)
|
|
301
|
+
task_file = str(activation["task_file"])
|
|
302
|
+
task_path = worktree / task_file
|
|
303
|
+
fields, _ = isolation.task_frontmatter(task_path.read_text(encoding="utf-8"))
|
|
304
|
+
task_id = str(activation.get("task_id") or (fields or {}).get("id"))
|
|
305
|
+
wave = None
|
|
306
|
+
if fields and fields.get("wave") is not None:
|
|
307
|
+
wave = int(fields["wave"])
|
|
308
|
+
open_native_shell(
|
|
309
|
+
primary,
|
|
310
|
+
task_id=task_id,
|
|
311
|
+
base=str(activation["base"]),
|
|
312
|
+
worktree=str(activation["worktree"]),
|
|
313
|
+
task_file=task_file,
|
|
314
|
+
task_branch=activation.get("task_branch"),
|
|
315
|
+
mode="parallel" if activation.get("task_branch") else "serial",
|
|
316
|
+
wave=wave,
|
|
317
|
+
title=str(fields.get("title")) if fields else None,
|
|
318
|
+
files=list(fields.get("files") or []) if fields else None,
|
|
319
|
+
)
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
def record_native_member_activation(coordinator: Path, activation: Dict[str, object]) -> None:
|
|
323
|
+
"""Record a native member-task activation in the coordinator dispatch ledger."""
|
|
324
|
+
task_file = str(activation["task_file"])
|
|
325
|
+
sidecar = Path(str(activation["worktree"]))
|
|
326
|
+
copy = Path(str(activation["copy"]))
|
|
327
|
+
fields, _ = isolation.task_frontmatter(copy.read_text(encoding="utf-8"))
|
|
328
|
+
task_id = str(activation.get("task_id") or (fields or {}).get("id"))
|
|
329
|
+
wave = None
|
|
330
|
+
if fields and fields.get("wave") is not None:
|
|
331
|
+
wave = int(fields["wave"])
|
|
332
|
+
open_native_shell(
|
|
333
|
+
coordinator,
|
|
334
|
+
task_id=task_id,
|
|
335
|
+
base=str(activation["base"]),
|
|
336
|
+
worktree=str(sidecar),
|
|
337
|
+
task_file=copy.relative_to(sidecar).as_posix(),
|
|
338
|
+
task_branch=str(activation["task_branch"]),
|
|
339
|
+
mode="member",
|
|
340
|
+
wave=wave,
|
|
341
|
+
title=str(fields.get("title")) if fields else None,
|
|
342
|
+
files=list(fields.get("files") or []) if fields else None,
|
|
343
|
+
member=str(activation["member"]),
|
|
344
|
+
member_base=str(activation["member_base"]),
|
|
345
|
+
contract_file=task_file,
|
|
346
|
+
)
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
def reusable_shell_attempt(root: Path, state: Dict[str, object]) -> Optional[Path]:
|
|
350
|
+
"""A native activation shell the driver can attach its child wrapper to."""
|
|
351
|
+
task_dir = root / str(state["task_id"])
|
|
352
|
+
attempts = sorted(task_dir.glob("attempt-*/state.json"),
|
|
353
|
+
key=lambda path: attempt_number(path.parent))
|
|
354
|
+
if not attempts:
|
|
355
|
+
return None
|
|
356
|
+
prior = read_attempt(attempts[-1])
|
|
357
|
+
if prior.get("outcome") is not None or prior.get("command") or prior.get("pid"):
|
|
358
|
+
return None
|
|
359
|
+
if str(prior.get("base")) != str(state.get("base")):
|
|
360
|
+
return None
|
|
361
|
+
return attempts[-1].parent
|
|
362
|
+
|
|
363
|
+
|
|
110
364
|
def acquire_lock(primary: Path) -> BinaryIO:
|
|
111
365
|
root = records_root(primary)
|
|
112
366
|
root.mkdir(parents=True, exist_ok=True)
|
|
@@ -307,7 +561,7 @@ def retain_selection(root: Path, state: Dict[str, object]) -> Dict[str, object]:
|
|
|
307
561
|
raise DriverStop('cannot redispatch while the previous child is active')
|
|
308
562
|
if prior.get('model_selection'):
|
|
309
563
|
state['model_selection'] = prior['model_selection']
|
|
310
|
-
|
|
564
|
+
elif prior.get('command'):
|
|
311
565
|
state['command'] = prior['command']
|
|
312
566
|
reassignment = task_dir / 'model-reassignment.json'
|
|
313
567
|
if reassignment.exists():
|
|
@@ -323,13 +577,23 @@ def spawn(root: Path, state: Dict[str, object], options: argparse.Namespace,
|
|
|
323
577
|
state = retain_selection(root, state)
|
|
324
578
|
task_dir = root / str(state["task_id"])
|
|
325
579
|
argv, selection = selected_command(state, options, command or options.child_command)
|
|
326
|
-
|
|
327
|
-
attempt_dir
|
|
328
|
-
|
|
580
|
+
attempt_dir = reusable_shell_attempt(root, state)
|
|
581
|
+
if attempt_dir is None:
|
|
582
|
+
attempt = max((attempt_number(path) for path in task_dir.glob("attempt-*")), default=0) + 1
|
|
583
|
+
attempt_dir = task_dir / f"attempt-{attempt}"
|
|
584
|
+
attempt_dir.mkdir(parents=True)
|
|
585
|
+
else:
|
|
586
|
+
attempt = attempt_number(attempt_dir)
|
|
329
587
|
stale = ("pid", "finished_at", "exit_code", "timed_out", "reason", "question", "commit", "answer",
|
|
330
588
|
"usage_recorded")
|
|
331
|
-
|
|
332
|
-
|
|
589
|
+
incoming = {key: value for key, value in state.items()
|
|
590
|
+
if not key.startswith("_") and key not in stale}
|
|
591
|
+
if (attempt_dir / "state.json").is_file():
|
|
592
|
+
preserved = {key: value for key, value in read_attempt(attempt_dir / "state.json").items()
|
|
593
|
+
if not key.startswith("_") and key not in stale}
|
|
594
|
+
state = {**preserved, **incoming}
|
|
595
|
+
else:
|
|
596
|
+
state = incoming
|
|
333
597
|
if selection is not None:
|
|
334
598
|
state['model_selection'] = selection
|
|
335
599
|
state.update({"attempt": attempt, "command": argv,
|
|
@@ -384,6 +648,15 @@ def run_verify(command: str, cwd: Path) -> Dict[str, object]:
|
|
|
384
648
|
return {"exit_code": completed.returncode, "stdout": completed.stdout, "stderr": completed.stderr}
|
|
385
649
|
|
|
386
650
|
|
|
651
|
+
def landing_verify(command: str, regressions: List[Dict[str, object]],
|
|
652
|
+
cwd: Path) -> Tuple[Dict[str, object], List[Dict[str, object]]]:
|
|
653
|
+
"""The task's own Verify, then every regression Verify when the own one passes."""
|
|
654
|
+
execution = run_verify(command, cwd)
|
|
655
|
+
if execution["exit_code"]:
|
|
656
|
+
return execution, []
|
|
657
|
+
return execution, [{**item, **run_verify(str(item["command"]), cwd)} for item in regressions]
|
|
658
|
+
|
|
659
|
+
|
|
387
660
|
def state_from_task(primary: Path, task_id: str) -> Dict[str, object]:
|
|
388
661
|
"""A finish-able state derived from the task frontmatter alone, for coders dispatched by hand."""
|
|
389
662
|
tasks_dir = primary / ".project/tasks"
|
|
@@ -437,6 +710,22 @@ def state_from_task(primary: Path, task_id: str) -> Dict[str, object]:
|
|
|
437
710
|
|
|
438
711
|
def finish_task(primary: Path, state: Dict[str, object]) -> Dict[str, object]:
|
|
439
712
|
"""Verify in the isolate, land, record the ledger, retire. Returns the landing receipt."""
|
|
713
|
+
landing = land_task(primary, state)
|
|
714
|
+
try:
|
|
715
|
+
if state["mode"] == "member":
|
|
716
|
+
isolation.retire_member_task(primary, str(state["member"]), str(state["task_id"]))
|
|
717
|
+
else:
|
|
718
|
+
isolation.retire(primary, Path(str(state["worktree"])), state.get("task_branch") or None, False)
|
|
719
|
+
except isolation.IsolationError as error:
|
|
720
|
+
# The landing is proven, so a failed removal is a warning; Round.recover checks the isolate again.
|
|
721
|
+
landing["cleanup_warning"] = (f"{error}; the task is landed, but its isolate was not removed. The next "
|
|
722
|
+
"round or finish checks the isolate again and stops if the isolate is "
|
|
723
|
+
"still there")
|
|
724
|
+
return landing
|
|
725
|
+
|
|
726
|
+
|
|
727
|
+
def land_task(primary: Path, state: Dict[str, object]) -> Dict[str, object]:
|
|
728
|
+
"""Verify in the isolate, land, record the ledger. Returns the landing receipt."""
|
|
440
729
|
worktree = Path(str(state["worktree"]))
|
|
441
730
|
task_id = str(state["task_id"])
|
|
442
731
|
task_file = str(state["task_file"])
|
|
@@ -445,39 +734,59 @@ def finish_task(primary: Path, state: Dict[str, object]) -> Dict[str, object]:
|
|
|
445
734
|
command = _common.task_verify_command(task_path.read_text(encoding="utf-8"))
|
|
446
735
|
if not command:
|
|
447
736
|
raise DriverStop("task has no Verify command", task=task_id)
|
|
737
|
+
# A later task can break an earlier task that shares a file; that task's Verify also decides this landing.
|
|
738
|
+
regressions = build_state.regression_verifies(primary, task_id, command)
|
|
739
|
+
# A plan-approved `supersedes` retires an earlier Verify; the receipt and the Log show each one.
|
|
740
|
+
retired = build_state.retired_verifies(primary, task_id)
|
|
741
|
+
superseded = {"retired": retired} if retired else {}
|
|
448
742
|
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
743
|
sidecar = state["sidecar"]
|
|
452
|
-
|
|
744
|
+
ensured = isolation.ensure_verify_sidecar(primary, base, str(sidecar["branch"]))
|
|
745
|
+
sidecar_path = Path(str(ensured["worktree"]))
|
|
453
746
|
tree = snapshot_tree(primary)
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
747
|
+
try:
|
|
748
|
+
isolation.reproduce_verify_sidecar(sidecar_path, tree)
|
|
749
|
+
except isolation.IsolationError as error:
|
|
750
|
+
raise DriverStop(str(error), task=task_id) from error
|
|
751
|
+
execution, reruns = landing_verify(command, regressions, sidecar_path)
|
|
458
752
|
location = f"sidecar {sidecar['branch']}"
|
|
459
753
|
isolation.clean_verify(primary, sidecar_path, base, str(sidecar["branch"]))
|
|
460
754
|
isolation.retire(primary, sidecar_path, str(sidecar["branch"]), False)
|
|
461
755
|
else:
|
|
462
|
-
execution =
|
|
756
|
+
execution, reruns = landing_verify(command, regressions, worktree)
|
|
463
757
|
location = f"isolate {state['task_branch']}"
|
|
464
758
|
passed = execution["exit_code"] == 0
|
|
759
|
+
broken = [item for item in reruns if item["exit_code"]]
|
|
465
760
|
execution.update({"worktree": str(worktree), "location": location})
|
|
466
761
|
evidence_dir = (Path(str(state["_path"])).parent if state.get("_path")
|
|
467
762
|
else records_root(primary) / task_id / f"manual-{now().replace(':', '')}")
|
|
468
763
|
evidence_dir.mkdir(parents=True, exist_ok=True)
|
|
469
764
|
_common.atomic_write(evidence_dir / "verify.json",
|
|
470
|
-
json.dumps(execution,
|
|
765
|
+
json.dumps({**execution, "regressions": reruns, **superseded},
|
|
766
|
+
indent=2, sort_keys=True) + "\n")
|
|
471
767
|
execution["evidence"] = str(evidence_dir / "verify.json")
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
+ (f"
|
|
476
|
-
|
|
768
|
+
# The Verify of a superseding task runs in the place of the retired Verify: the label names both.
|
|
769
|
+
for label, run in [("Verify", execution),
|
|
770
|
+
*((f"regression Verify of {', '.join(item['tasks'])}"
|
|
771
|
+
+ (f" (supersedes {', '.join(item['supersedes'])})" if item.get("supersedes") else ""),
|
|
772
|
+
item) for item in reruns)]:
|
|
773
|
+
output = "\n".join(f" {line}" for line in tail(run["stdout"] + run["stderr"]).splitlines())
|
|
774
|
+
append_log(task_path, f"- {now()[:10]} — orchestrator {label} ({location}): "
|
|
775
|
+
f"{'fail' if run['exit_code'] else 'pass'}, exit {run['exit_code']}"
|
|
776
|
+
+ (f"; output tail:\n ```\n{output}\n ```" if output else ""))
|
|
777
|
+
for item in retired:
|
|
778
|
+
part = f": part of {item['part']}" if item.get("part") else ""
|
|
779
|
+
append_log(task_path, f"- {now()[:10]} — orchestrator regression Verify of {item['task']}{part} retired: "
|
|
780
|
+
f"superseded by {item['superseded_by']}")
|
|
781
|
+
if not passed or broken:
|
|
477
782
|
if state["mode"] == "parallel":
|
|
478
783
|
isolation.deactivate_task(worktree, task_id, str(state["task_branch"]))
|
|
479
784
|
elif state["mode"] == "member":
|
|
480
785
|
isolation.deactivate_member_task(primary, str(state["member"]), task_id)
|
|
786
|
+
if passed:
|
|
787
|
+
names = ", ".join(name for item in broken for name in item["tasks"])
|
|
788
|
+
raise DriverStop(f"regression Verify of {names} failed in the isolate", task=task_id,
|
|
789
|
+
execution=execution, regressions=broken)
|
|
481
790
|
raise DriverStop("Verify failed in the isolate", task=task_id, execution=execution)
|
|
482
791
|
if state["mode"] == "member":
|
|
483
792
|
member = str(state["member"])
|
|
@@ -488,9 +797,9 @@ def finish_task(primary: Path, state: Dict[str, object]) -> Dict[str, object]:
|
|
|
488
797
|
ledger = isolation.git_output(worktree, "rev-parse", f"{landing}^") == str(state["member_base"])
|
|
489
798
|
if ledger:
|
|
490
799
|
build_state.verify_record(str(primary), command, landing, "pass", execution=execution, member=member)
|
|
491
|
-
isolation.retire_member_task(primary, member, task_id)
|
|
492
800
|
return {"task": task_id, "commit": str(landed["commit"]), "mode": "member",
|
|
493
|
-
"landing": landing, "verify": execution, "
|
|
801
|
+
"landing": landing, "verify": execution, "regressions": reruns, **superseded,
|
|
802
|
+
"ledger": ledger}
|
|
494
803
|
landed = isolation.land(primary, worktree, base, task_id, str(state["title"]), task_file,
|
|
495
804
|
list(state["files"]))
|
|
496
805
|
commit = str(landed["commit"])
|
|
@@ -498,9 +807,8 @@ def finish_task(primary: Path, state: Dict[str, object]) -> Dict[str, object]:
|
|
|
498
807
|
ledger = isolation.git_output(primary, "rev-parse", f"{commit}^") == base
|
|
499
808
|
if ledger:
|
|
500
809
|
build_state.verify_record(str(primary), command, commit, "pass", execution=execution)
|
|
501
|
-
isolation.retire(primary, worktree, state.get("task_branch") or None, False)
|
|
502
810
|
return {"task": task_id, "commit": commit, "mode": landed["mode"], "verify": execution,
|
|
503
|
-
"ledger": ledger}
|
|
811
|
+
"regressions": reruns, **superseded, "ledger": ledger}
|
|
504
812
|
|
|
505
813
|
|
|
506
814
|
# --- shared by round and review --------------------------------------------
|
|
@@ -516,6 +824,11 @@ def recover_report(primary: Path, project_dir: str, receipt: Dict[str, object])
|
|
|
516
824
|
report = isolation.recover(primary, Path(project_dir) / "tasks")
|
|
517
825
|
receipt["steps"].append({"script": "isolation.py recover", "result": report})
|
|
518
826
|
if report["verdict"] == "block":
|
|
827
|
+
blocked = [task for task in report["tasks"] if task["verdict"] == "block"]
|
|
828
|
+
if blocked and all(task.get("leftover") for task in blocked):
|
|
829
|
+
# Every blocked task is landed: the stop names only the isolates to remove, and the build stays active.
|
|
830
|
+
raise DriverStop("; ".join(LEFTOVER_ISOLATE.format(task["task_id"], task["leftover"])
|
|
831
|
+
for task in blocked), recover=blocked)
|
|
519
832
|
raise DriverStop(RECOVERY_BLOCKED, recover=report)
|
|
520
833
|
return report
|
|
521
834
|
|
|
@@ -651,7 +964,7 @@ class Round:
|
|
|
651
964
|
self.receipt["steps"].append({"script": "isolation.recover_member_landing", "task": journal.stem,
|
|
652
965
|
"result": result})
|
|
653
966
|
if result["state"] == "landed":
|
|
654
|
-
|
|
967
|
+
self.retire_member(str(result["member"]), journal.stem, str(result["landing"]))
|
|
655
968
|
retired_members.add(journal.stem)
|
|
656
969
|
report = recover_report(self.primary, self.project_dir, self.receipt)
|
|
657
970
|
for task in report["tasks"]:
|
|
@@ -659,26 +972,76 @@ class Round:
|
|
|
659
972
|
verdict = task["verdict"]
|
|
660
973
|
if verdict in ("recovered", "attested"):
|
|
661
974
|
self.proven[str(task.get("task_id"))] = str(task.get("commit"))
|
|
975
|
+
if (verdict == "reconcile" and task.get("status") == "in-progress"
|
|
976
|
+
and task.get("task_branch") is None):
|
|
977
|
+
task_id = str(task.get("task_id"))
|
|
978
|
+
verify_name = f"task-{task_id.lower()}-verify"
|
|
979
|
+
verify_path = isolation.sidecar_root(self.primary, "verify", verify_name)
|
|
980
|
+
verify_branch = isolation.verify_branch_name(verify_name)
|
|
981
|
+
diagnose = shlex.join([
|
|
982
|
+
sys.executable,
|
|
983
|
+
"-B",
|
|
984
|
+
str(Path(__file__).with_name("pipeline_diagnose.py")),
|
|
985
|
+
"diagnose",
|
|
986
|
+
"--repo",
|
|
987
|
+
str(self.primary),
|
|
988
|
+
])
|
|
989
|
+
raise DriverStop(
|
|
990
|
+
f"task {task_id} needs recovery: {task.get('reason')}; inspect Verify "
|
|
991
|
+
f"sidecar {verify_path} ({verify_branch}) before resetting it to pending; "
|
|
992
|
+
f"inventory it with `{diagnose}`",
|
|
993
|
+
recover=task,
|
|
994
|
+
)
|
|
662
995
|
if verdict == "recovered" and task.get("member") and task.get("task_id") not in retired_members:
|
|
663
|
-
|
|
996
|
+
self.retire_member(str(task["member"]), str(task["task_id"]), str(task["landing"]))
|
|
664
997
|
if verdict == "recovered" and worktree and worktree["clean"] and task.get("task_branch"):
|
|
665
998
|
isolation.retire(self.primary, Path(str(worktree["path"])),
|
|
666
999
|
str(task["task_branch"]), False)
|
|
667
|
-
elif
|
|
668
|
-
|
|
669
|
-
|
|
1000
|
+
elif verdict == "recovered" and worktree is None and task.get("task_branch"):
|
|
1001
|
+
# The isolate is gone and the landing is proven: retire proves the branch, then deletes it.
|
|
1002
|
+
isolation.retire(self.primary, None, str(task["task_branch"]), True, str(task["commit"]))
|
|
1003
|
+
elif verdict == "resume" and (task.get("landing_retry") or task.get("dispatch_retry")):
|
|
1004
|
+
# ponytail: interrupted retries stay with the parent's documented
|
|
670
1005
|
# procedure; failed/blocked reconciliation surfaces through ready's typed error.
|
|
671
1006
|
raise DriverStop(f"task {task.get('task_id')} needs recovery: {verdict}", recover=task)
|
|
672
1007
|
|
|
1008
|
+
def retire_member(self, member: str, task_id: str, landing: str) -> None:
|
|
1009
|
+
"""Retire a landed member task's sidecar; a proven leftover of it is a stop that names only its path."""
|
|
1010
|
+
checkout, project, _ = isolation._member_context(self.primary, member)
|
|
1011
|
+
sidecar = isolation.sidecar_root(checkout, "task", f"{project}-{task_id}")
|
|
1012
|
+
branch = f"refs/heads/{isolation.TASK_BRANCH_PREFIX}{project}-{task_id}"
|
|
1013
|
+
tip = isolation.run_git(checkout, "rev-parse", "--verify", "--quiet", branch)
|
|
1014
|
+
commits = [landing, *tip.stdout.split()]
|
|
1015
|
+
try:
|
|
1016
|
+
isolation.retire_member_task(self.primary, member, task_id)
|
|
1017
|
+
except isolation.RemovalError as error:
|
|
1018
|
+
# Missing files are a removal that stopped part way; every other state keeps the refusal.
|
|
1019
|
+
if not (isolation.only_missing_files(sidecar)
|
|
1020
|
+
or isolation.holds_only_landed_files(checkout, sidecar, commits)):
|
|
1021
|
+
raise
|
|
1022
|
+
raise DriverStop(LEFTOVER_ISOLATE.format(task_id, sidecar), cleanup_error=str(error))
|
|
1023
|
+
if isolation.holds_only_landed_files(checkout, sidecar, commits):
|
|
1024
|
+
raise DriverStop(LEFTOVER_ISOLATE.format(task_id, sidecar))
|
|
1025
|
+
|
|
673
1026
|
# settle exited children ---------------------------------------------
|
|
674
1027
|
|
|
675
1028
|
def isolate_live(self, state: Dict[str, object]) -> bool:
|
|
676
|
-
"""
|
|
1029
|
+
"""Keep blocked attempts reportable until the corresponding task is reset."""
|
|
677
1030
|
task_path = Path(str(state["worktree"])) / str(state["task_file"])
|
|
678
1031
|
if not task_path.is_file():
|
|
679
1032
|
return False
|
|
680
1033
|
fields, _ = isolation.task_frontmatter(task_path.read_text(encoding="utf-8"))
|
|
681
|
-
|
|
1034
|
+
if fields is None:
|
|
1035
|
+
return False
|
|
1036
|
+
if fields.get("status") == "in-progress":
|
|
1037
|
+
return True
|
|
1038
|
+
return (
|
|
1039
|
+
state.get("mode") == "serial"
|
|
1040
|
+
and fields.get("status") == "blocked"
|
|
1041
|
+
and fields.get("id") == state.get("task_id")
|
|
1042
|
+
and fields.get("base") == state.get("base")
|
|
1043
|
+
and fields.get("task_branch") == "null"
|
|
1044
|
+
)
|
|
682
1045
|
|
|
683
1046
|
def settle(self) -> bool:
|
|
684
1047
|
"""Classify every exited child. Returns True when a landing or redispatch changed the round."""
|
|
@@ -717,8 +1080,15 @@ class Round:
|
|
|
717
1080
|
elif state.get("finished_at") is None:
|
|
718
1081
|
if child_running(state):
|
|
719
1082
|
self.receipt["in_flight"].append(self.summary(state))
|
|
1083
|
+
elif (state.get("native") and not state.get("command") and not state.get("pid")
|
|
1084
|
+
and self.isolate_live(state)):
|
|
1085
|
+
continue
|
|
720
1086
|
else:
|
|
721
|
-
|
|
1087
|
+
state.update(read_attempt(Path(str(state["_path"]))))
|
|
1088
|
+
if state.get("finished_at") is not None:
|
|
1089
|
+
changed = self.classify(state) or changed
|
|
1090
|
+
else:
|
|
1091
|
+
self.fail(state, ORPHANED)
|
|
722
1092
|
else:
|
|
723
1093
|
changed = self.classify(state) or changed
|
|
724
1094
|
return changed
|
|
@@ -729,6 +1099,35 @@ class Round:
|
|
|
729
1099
|
update_state(state, outcome="blocked", reason=reason)
|
|
730
1100
|
self.receipt["blocked"].append({**self.summary(state), "reason": reason, **details})
|
|
731
1101
|
|
|
1102
|
+
def blocked_result(self, state: Dict[str, object], **diagnostics: object) -> bool:
|
|
1103
|
+
"""Record an explicit serial block and retire only its untouched Verify sidecar."""
|
|
1104
|
+
reason = "child returned blocked"
|
|
1105
|
+
details: Dict[str, object] = dict(diagnostics)
|
|
1106
|
+
if state.get("mode") == "serial":
|
|
1107
|
+
try:
|
|
1108
|
+
task = isolation.block_serial_task(
|
|
1109
|
+
self.primary,
|
|
1110
|
+
str(state["task_file"]),
|
|
1111
|
+
str(state["task_id"]),
|
|
1112
|
+
str(state["base"]),
|
|
1113
|
+
)
|
|
1114
|
+
except STOP_ERRORS as error:
|
|
1115
|
+
self.fail(state, f"{reason}; could not record blocked task: {error}")
|
|
1116
|
+
return False
|
|
1117
|
+
details["task"] = task
|
|
1118
|
+
sidecar = state.get("sidecar")
|
|
1119
|
+
if isinstance(sidecar, dict):
|
|
1120
|
+
worktree = Path(str(sidecar["worktree"]))
|
|
1121
|
+
branch = str(sidecar["branch"])
|
|
1122
|
+
try:
|
|
1123
|
+
details["verify_cleanup"] = isolation.retire_unused_verify(
|
|
1124
|
+
self.primary, worktree, str(state["base"]), branch
|
|
1125
|
+
)
|
|
1126
|
+
except STOP_ERRORS as error:
|
|
1127
|
+
reason += f"; Verify sidecar retained at {worktree}: {error}"
|
|
1128
|
+
self.fail(state, reason, **details)
|
|
1129
|
+
return False
|
|
1130
|
+
|
|
732
1131
|
def classify(self, state: Dict[str, object]) -> bool:
|
|
733
1132
|
attempt_dir = Path(str(state["_path"])).parent
|
|
734
1133
|
if self.budgeted and not state.get("usage_recorded"):
|
|
@@ -756,6 +1155,10 @@ class Round:
|
|
|
756
1155
|
update_state(state, outcome="question", question=unanswered[0], answered=False)
|
|
757
1156
|
self.receipt["questions"].append({**self.summary(state), "question": unanswered[0],
|
|
758
1157
|
"log_delta": delta.strip()})
|
|
1158
|
+
elif verdict == "blocked" and named == str(state["task_id"]).lower():
|
|
1159
|
+
return self.blocked_result(
|
|
1160
|
+
state, stdout_tail=tail(message), log_delta=delta.strip()
|
|
1161
|
+
)
|
|
759
1162
|
elif verdict != "ready" or named != str(state["task_id"]).lower():
|
|
760
1163
|
self.fail(state, "child returned blocked" if verdict == "blocked"
|
|
761
1164
|
else "child returned no RESULT line", stdout_tail=tail(message),
|
|
@@ -790,8 +1193,14 @@ class Round:
|
|
|
790
1193
|
# dispatch -------------------------------------------------------------
|
|
791
1194
|
|
|
792
1195
|
def in_flight_states(self) -> List[Dict[str, object]]:
|
|
793
|
-
|
|
794
|
-
|
|
1196
|
+
states = []
|
|
1197
|
+
for state in latest_states(self.root):
|
|
1198
|
+
if state.get("outcome") is not None or state.get("wave") != self.receipt["wave"]:
|
|
1199
|
+
continue
|
|
1200
|
+
if state.get("native") and not state.get("command") and not state.get("pid"):
|
|
1201
|
+
continue
|
|
1202
|
+
states.append(state)
|
|
1203
|
+
return states
|
|
795
1204
|
|
|
796
1205
|
def checkpoint_bookkeeping(self) -> None:
|
|
797
1206
|
open_ids = {state["task_id"] for state in latest_states(self.root)
|
|
@@ -815,6 +1224,24 @@ class Round:
|
|
|
815
1224
|
ready = build_state.ready(str(self.primary), self.project_dir)
|
|
816
1225
|
self.receipt["steps"].append({"script": "build_state.py ready", "result": ready})
|
|
817
1226
|
wave = ready["current_wave"]
|
|
1227
|
+
blocked_review = ready.get("blocked_review")
|
|
1228
|
+
if blocked_review:
|
|
1229
|
+
waiting_waves = blocked_review["waiting_waves"]
|
|
1230
|
+
if self.receipt["wave"] in waiting_waves:
|
|
1231
|
+
raise DriverStop(
|
|
1232
|
+
f"wave {self.receipt['wave']} has unfinished tasks but wave "
|
|
1233
|
+
f"{blocked_review['wave']} review cycle {blocked_review['cycle']} "
|
|
1234
|
+
"is blocked; it must pass before dispatch")
|
|
1235
|
+
self.checkpoint_bookkeeping()
|
|
1236
|
+
return not in_flight
|
|
1237
|
+
repair_for_wave = ready.get("repair_for_wave")
|
|
1238
|
+
if repair_for_wave is not None and self.receipt["wave"] != wave:
|
|
1239
|
+
if self.receipt["wave"] > repair_for_wave:
|
|
1240
|
+
raise DriverStop(
|
|
1241
|
+
f"wave {self.receipt['wave']} is waiting on blocked wave {repair_for_wave}; "
|
|
1242
|
+
f"dispatch repair wave {wave} first")
|
|
1243
|
+
self.checkpoint_bookkeeping()
|
|
1244
|
+
return not in_flight
|
|
818
1245
|
if wave is not None and wave < self.receipt["wave"]:
|
|
819
1246
|
raise DriverStop(f"wave {wave} is still unfinished; run round --wave {wave} first")
|
|
820
1247
|
self.checkpoint_bookkeeping()
|
|
@@ -881,7 +1308,7 @@ class Round:
|
|
|
881
1308
|
continue
|
|
882
1309
|
prepared = self.runner("prepare-task", head, task["id"], round_size)
|
|
883
1310
|
isolate = prepared[0]["result"]
|
|
884
|
-
sidecar = prepared[1]["result"] if len(prepared) > 1 else None
|
|
1311
|
+
sidecar = prepared[-1]["result"] if len(prepared) > 1 else None # the isolate-verify step is last
|
|
885
1312
|
worktree = Path(str(isolate["worktree"]))
|
|
886
1313
|
isolation.activate_task(worktree, head, task["id"], f"build_{task['id'].lower()}",
|
|
887
1314
|
task["task_file"], isolate["task_branch"])
|
|
@@ -1657,6 +2084,23 @@ def next_task_id(tasks_dir: Path) -> str:
|
|
|
1657
2084
|
return f"T{max(numbers, default=0) + 1:03d}"
|
|
1658
2085
|
|
|
1659
2086
|
|
|
2087
|
+
def replaced_review_base(primary: Path, project_dir: str, wave: int, cycle: int) -> Optional[str]:
|
|
2088
|
+
"""The recovery base when this cycle's review replaces the one a build recovery set aside, else None."""
|
|
2089
|
+
recovery = pipeline_state._build_recovery().context(primary)
|
|
2090
|
+
if not recovery:
|
|
2091
|
+
return None
|
|
2092
|
+
relative = f"{project_dir}/review/wave-{wave}.cycle{cycle}.md"
|
|
2093
|
+
before = isolation.run_git(primary, "show", f"{recovery['base']}:{relative}")
|
|
2094
|
+
current = primary / relative
|
|
2095
|
+
# Plan approval restores the review of an unchanged wave before its checkpoint, so no commit
|
|
2096
|
+
# since the base touches that file. A set-aside review is deleted by that checkpoint.
|
|
2097
|
+
rewritten = isolation.run_git(primary, "log", "-1", "--format=%H", f"{recovery['base']}..HEAD", "--", relative)
|
|
2098
|
+
if (not rewritten.stdout.strip() and before.returncode == 0 and current.is_file()
|
|
2099
|
+
and current.read_text(encoding="utf-8") == before.stdout):
|
|
2100
|
+
return None # the restored review keeps its repairs
|
|
2101
|
+
return recovery["base"]
|
|
2102
|
+
|
|
2103
|
+
|
|
1660
2104
|
def fix_tasks(primary: Path, options: argparse.Namespace) -> Dict[str, object]:
|
|
1661
2105
|
"""Step 7: one fix task per findings batch, carrying failed criteria and observations verbatim."""
|
|
1662
2106
|
receipt: Dict[str, object] = {"status": None, "wave": options.wave, "cycle": options.cycle,
|
|
@@ -1665,14 +2109,32 @@ def fix_tasks(primary: Path, options: argparse.Namespace) -> Dict[str, object]:
|
|
|
1665
2109
|
project = primary / options.project_dir
|
|
1666
2110
|
tasks_dir = project / "tasks"
|
|
1667
2111
|
repairs = []
|
|
2112
|
+
pending = []
|
|
1668
2113
|
for path in sorted(tasks_dir.glob("*-fix-wave-*-cycle-*.md")):
|
|
1669
2114
|
text = path.read_text(encoding="utf-8")
|
|
1670
2115
|
fields, _ = isolation.task_frontmatter(text)
|
|
1671
2116
|
repairs.append({"task": fields["id"], "path": path.relative_to(primary).as_posix(),
|
|
1672
|
-
"wave": int(fields["wave"]), "locators": re.findall(r"(?m)^### (.+)$", _common.section_body(text, "Review findings") or "")
|
|
2117
|
+
"wave": int(fields["wave"]), "locators": re.findall(r"(?m)^### (.+)$", _common.section_body(text, "Review findings") or ""),
|
|
2118
|
+
**({"repo": fields["repo"]} if fields.get("repo") else {})})
|
|
2119
|
+
if fields["status"] != "done":
|
|
2120
|
+
pending.append(repairs[-1])
|
|
1673
2121
|
suffix = f"-fix-wave-{options.wave}-cycle-{options.cycle}.md"
|
|
1674
|
-
|
|
1675
|
-
|
|
2122
|
+
# A build recovery restarts a changed wave at cycle 1, so the repairs and the PLAN heading
|
|
2123
|
+
# of the review it set aside carry the same names. They belong to that review, not this one.
|
|
2124
|
+
replaced = replaced_review_base(primary, options.project_dir, options.wave, options.cycle)
|
|
2125
|
+
|
|
2126
|
+
def of_replaced_review(relative: str) -> bool:
|
|
2127
|
+
if not replaced or isolation.run_git(primary, "show", f"{replaced}:{relative}").returncode != 0:
|
|
2128
|
+
return False
|
|
2129
|
+
return not isolation.run_git(primary, "log", "--diff-filter=AD", "--format=%H",
|
|
2130
|
+
f"{replaced}..HEAD", "--", relative).stdout.strip()
|
|
2131
|
+
|
|
2132
|
+
stale = [item for item in repairs if item["path"].endswith(suffix) and of_replaced_review(item["path"])]
|
|
2133
|
+
existing = [item for item in repairs if item["path"].endswith(suffix) and item not in stale]
|
|
2134
|
+
# A landed repair no longer carries its finding: a repeat proves that fix failed and needs the next one.
|
|
2135
|
+
# Two repos can carry the same locator, so a repair carries a finding only in its own repo.
|
|
2136
|
+
carried = {(item.get("repo"), locator) for item in pending if not item["path"].endswith(suffix)
|
|
2137
|
+
for locator in item["locators"]}
|
|
1676
2138
|
findings = review_findings.compute(primary, options.project_dir, options.wave, options.cycle)
|
|
1677
2139
|
receipt["findings"] = findings
|
|
1678
2140
|
escalation = [key for key in ("structural_blockers", "skeptic_groups", "cap_reached", "all_refuted")
|
|
@@ -1686,15 +2148,17 @@ def fix_tasks(primary: Path, options: argparse.Namespace) -> Dict[str, object]:
|
|
|
1686
2148
|
texts = contracts._task_texts(primary, options.project_dir)
|
|
1687
2149
|
groups = {group["locator"]: group for group in findings["groups"]}
|
|
1688
2150
|
today = now()[:10]
|
|
1689
|
-
#
|
|
2151
|
+
# Unordered same-wave file overlap is rejected, so repairs land in a newly appended wave.
|
|
1690
2152
|
plan_path = project / "plan/PLAN.md"
|
|
1691
2153
|
plan_text = plan_path.read_text(encoding="utf-8")
|
|
1692
2154
|
depths, waves = contracts._plan_waves(plan_text)
|
|
1693
2155
|
repair_waves = {item["wave"] for item in existing}
|
|
1694
2156
|
if len(repair_waves) > 1:
|
|
1695
2157
|
raise DriverStop("repair tasks for this cycle disagree on their wave")
|
|
1696
|
-
|
|
2158
|
+
stale_waves = {item["wave"] for item in stale}
|
|
2159
|
+
repair_heading = next((heading for heading in re.finditer(
|
|
1697
2160
|
rf"(?m)^## Wave (\d+) — fix wave {options.wave} cycle {options.cycle} review findings$", plan_text)
|
|
2161
|
+
if int(heading.group(1)) not in stale_waves), None)
|
|
1698
2162
|
if repair_waves:
|
|
1699
2163
|
fix_wave = next(iter(repair_waves))
|
|
1700
2164
|
elif repair_heading:
|
|
@@ -1705,9 +2169,10 @@ def fix_tasks(primary: Path, options: argparse.Namespace) -> Dict[str, object]:
|
|
|
1705
2169
|
raise DriverStop("repair tasks disagree with the PLAN repair wave")
|
|
1706
2170
|
receipt["existing"] = existing
|
|
1707
2171
|
for batch in findings["fix_batches"]:
|
|
1708
|
-
|
|
2172
|
+
repo = batch.get("repo") # a member name, or None for the coordinator
|
|
2173
|
+
if any(item.get("repo") == repo and set(batch["locators"]) <= set(item["locators"]) for item in existing):
|
|
1709
2174
|
continue
|
|
1710
|
-
if all(locator in carried for locator in batch["locators"]):
|
|
2175
|
+
if all((repo, locator) in carried for locator in batch["locators"]):
|
|
1711
2176
|
receipt["carried"].append(batch)
|
|
1712
2177
|
continue
|
|
1713
2178
|
batch_groups = [groups[locator] for locator in batch["locators"]]
|
|
@@ -1715,9 +2180,15 @@ def fix_tasks(primary: Path, options: argparse.Namespace) -> Dict[str, object]:
|
|
|
1715
2180
|
for source in sources:
|
|
1716
2181
|
if source not in texts:
|
|
1717
2182
|
raise DriverStop(f"fix batch names unknown task {source}")
|
|
2183
|
+
# One task changes one repo: the fix depends on, and runs the Verify of, the source tasks of its repo.
|
|
2184
|
+
sources = [source for source in sources if isolation.task_frontmatter(texts[source])[0].get("repo") == repo]
|
|
2185
|
+
# A repeat builds on the landed repairs of its finding: repair-evidence links them by this dependency.
|
|
2186
|
+
deps = sources + [item["task"] for item in repairs
|
|
2187
|
+
if item not in pending and item.get("repo") == repo
|
|
2188
|
+
and f"-fix-wave-{options.wave}-cycle-" in item["path"] and not item["path"].endswith(suffix)
|
|
2189
|
+
and set(item["locators"]) & set(batch["locators"])]
|
|
1718
2190
|
task_id = next_task_id(tasks_dir)
|
|
1719
|
-
verify =
|
|
1720
|
-
f"(\n{command}\n)" for command in dict.fromkeys(_common.task_verify_command(texts[source]) for source in sources))
|
|
2191
|
+
verify = _common.fix_verify(_common.task_verify_command(texts[source]) for source in sources)
|
|
1721
2192
|
heavy = any(re.search(r"(?m)^Heavy:\s*yes", texts[source]) for source in sources)
|
|
1722
2193
|
criteria = "\n".join(f"{index}. {criterion}" for index, criterion in
|
|
1723
2194
|
enumerate(dict.fromkeys(group["criterion"] for group in batch_groups), 1))
|
|
@@ -1725,18 +2196,19 @@ def fix_tasks(primary: Path, options: argparse.Namespace) -> Dict[str, object]:
|
|
|
1725
2196
|
f"### {group['locator']}\nCriterion: {group['criterion']}\n"
|
|
1726
2197
|
+ "\n".join(f"- {item['text']}" for item in group["observations"]) for group in batch_groups)
|
|
1727
2198
|
files = "\n".join(f" - {path}" for path in batch["files"])
|
|
2199
|
+
member = f"repo: {repo}\n" if repo else "" # the task template puts `repo:` before `files:`
|
|
1728
2200
|
title = f"Fix wave {options.wave} cycle {options.cycle} review findings in {', '.join(sources)}"
|
|
1729
2201
|
text = f"""---
|
|
1730
2202
|
id: {task_id}
|
|
1731
2203
|
title: {title}
|
|
1732
2204
|
wave: {fix_wave}
|
|
1733
|
-
deps: [{', '.join(
|
|
2205
|
+
deps: [{', '.join(deps)}]
|
|
1734
2206
|
status: pending
|
|
1735
2207
|
agent: null
|
|
1736
2208
|
base: null
|
|
1737
2209
|
worktree: null
|
|
1738
2210
|
task_branch: null
|
|
1739
|
-
files:
|
|
2211
|
+
{member}files:
|
|
1740
2212
|
{files}
|
|
1741
2213
|
---
|
|
1742
2214
|
|
|
@@ -1785,8 +2257,8 @@ Heavy: {'yes' if heavy else 'no'}
|
|
|
1785
2257
|
path = tasks_dir / f"{task_id}-fix-wave-{options.wave}-cycle-{options.cycle}.md"
|
|
1786
2258
|
_common.atomic_write(path, text)
|
|
1787
2259
|
receipt["created"].append({"task": task_id, "path": path.relative_to(primary).as_posix(),
|
|
1788
|
-
"wave": fix_wave, "deps":
|
|
1789
|
-
"locators": batch["locators"]})
|
|
2260
|
+
"wave": fix_wave, "deps": deps, "files": batch["files"],
|
|
2261
|
+
"locators": batch["locators"], **({"repo": repo} if repo else {})})
|
|
1790
2262
|
rows = []
|
|
1791
2263
|
for item in [*existing, *receipt["created"]]:
|
|
1792
2264
|
fields, _ = isolation.task_frontmatter((primary / item["path"]).read_text(encoding="utf-8"))
|
|
@@ -1820,7 +2292,8 @@ Heavy: {'yes' if heavy else 'no'}
|
|
|
1820
2292
|
receipt["plan_wave"] = fix_wave
|
|
1821
2293
|
lint = subprocess.run([sys.executable, "-B", str(Path(__file__).resolve().parent / "check_task_briefs.py"),
|
|
1822
2294
|
"--repo", str(primary), "--base", isolation.current_sha(primary),
|
|
1823
|
-
"--tasks-dir", f"{options.project_dir}/tasks"
|
|
2295
|
+
"--tasks-dir", f"{options.project_dir}/tasks",
|
|
2296
|
+
"--project-dir", options.project_dir], capture_output=True, encoding="utf-8", errors="replace")
|
|
1824
2297
|
receipt["steps"].append({"script": "check_task_briefs.py", "exit_code": lint.returncode,
|
|
1825
2298
|
"stdout": lint.stdout, "stderr": lint.stderr})
|
|
1826
2299
|
if lint.returncode:
|
|
@@ -1935,7 +2408,11 @@ def positive_int(value: str) -> int:
|
|
|
1935
2408
|
|
|
1936
2409
|
|
|
1937
2410
|
def default_resource(name: str) -> Optional[Path]:
|
|
1938
|
-
|
|
2411
|
+
script_root = Path(__file__).resolve().parent.parent
|
|
2412
|
+
candidate = script_root / name
|
|
2413
|
+
if candidate.is_file():
|
|
2414
|
+
return candidate
|
|
2415
|
+
candidate = script_root / "skills" / "gsd-path" / name
|
|
1939
2416
|
return candidate if candidate.is_file() else None
|
|
1940
2417
|
|
|
1941
2418
|
|
|
@@ -2026,6 +2503,7 @@ def main(argv=None) -> int:
|
|
|
2026
2503
|
if arguments.action == "_child":
|
|
2027
2504
|
return child_main(arguments.state)
|
|
2028
2505
|
primary = arguments.repo.resolve()
|
|
2506
|
+
_activate_pinned_runtime(primary)
|
|
2029
2507
|
lock = None
|
|
2030
2508
|
try:
|
|
2031
2509
|
if arguments.action != "status":
|
|
@@ -2065,6 +2543,10 @@ def main(argv=None) -> int:
|
|
|
2065
2543
|
if native and native["base"] != records[0].get("base"):
|
|
2066
2544
|
records = []
|
|
2067
2545
|
else:
|
|
2546
|
+
if (native and records[0]["outcome"] == "blocked" and records[0].get("native")
|
|
2547
|
+
and not records[0].get("command")):
|
|
2548
|
+
# A failed finish leaves a host-dispatched task active at its base: finish may run again.
|
|
2549
|
+
update_state(records[0], outcome=None, reason=None)
|
|
2068
2550
|
native = None
|
|
2069
2551
|
if arguments.task_id in current.proven:
|
|
2070
2552
|
commit = current.proven[arguments.task_id]
|
|
@@ -2078,8 +2560,22 @@ def main(argv=None) -> int:
|
|
|
2078
2560
|
raise DriverStop(f"task {arguments.task_id} dispatch record is {record['outcome']}; "
|
|
2079
2561
|
"use round")
|
|
2080
2562
|
if record.get("finished_at") is None:
|
|
2081
|
-
|
|
2082
|
-
|
|
2563
|
+
if child_running(record):
|
|
2564
|
+
raise DriverStop(f"task {arguments.task_id} still has a running child")
|
|
2565
|
+
if record.get("command"):
|
|
2566
|
+
raise DriverStop(f"task {arguments.task_id} still has a running child")
|
|
2567
|
+
live = state_from_task(primary, arguments.task_id)
|
|
2568
|
+
merged = dict(record, **live)
|
|
2569
|
+
merged["_path"] = record["_path"]
|
|
2570
|
+
try:
|
|
2571
|
+
landing = finish_task(primary, merged)
|
|
2572
|
+
except STOP_ERRORS as error:
|
|
2573
|
+
update_state(record, outcome="blocked", reason=str(error))
|
|
2574
|
+
raise
|
|
2575
|
+
update_state(record, outcome="landed", commit=landing["commit"])
|
|
2576
|
+
current.receipt["landed"].append(landing)
|
|
2577
|
+
else:
|
|
2578
|
+
current.classify(record)
|
|
2083
2579
|
else: # a coder dispatched by hand: derive the isolate from the task frontmatter
|
|
2084
2580
|
current.receipt["landed"].append(
|
|
2085
2581
|
finish_task(primary, native or state_from_task(primary, arguments.task_id)))
|