@opengsd/gsd-path 1.3.1 → 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/AGENTS.md +2 -207
- package/CHANGELOG.md +331 -69
- package/DOCS.md +25 -9
- package/FULL.md +12 -5
- package/GUIDE.md +1 -1
- package/HOOKS.md +104 -14
- package/QUICK.md +5 -2
- package/README.md +131 -111
- package/RUNTIME.md +2 -2
- package/UPDATE.md +61 -11
- package/WORKFLOW.md +44 -7
- package/package.json +13 -9
- package/platforms/shared-agents/dispatch.md +13 -0
- package/scripts/_common.py +619 -18
- package/scripts/archive_milestone.py +160 -86
- package/scripts/bootstrap_repository.py +8 -5
- package/scripts/build_recovery.py +100 -32
- package/scripts/build_state.py +143 -24
- package/scripts/check_docs_audit.py +91 -7
- package/scripts/check_handoffs.py +662 -50
- package/scripts/check_task_briefs.py +338 -30
- package/scripts/check_update.py +2 -2
- package/scripts/core_hook_gate.py +10 -1
- package/scripts/core_hook_settings.py +11 -4
- package/scripts/detect_project.py +587 -32
- package/scripts/discussion_records.py +19 -13
- package/scripts/discussion_validate.py +152 -50
- package/scripts/dispatch_driver.py +501 -46
- package/scripts/git_guard.py +118 -31
- package/scripts/guard_hook.py +1567 -194
- package/scripts/install.mjs +77 -46
- package/scripts/install.py +611 -50
- package/scripts/integration.py +580 -25
- package/scripts/isolation.py +1438 -57
- package/scripts/lean_verification.py +99 -19
- package/scripts/loop_run.py +31 -29
- package/scripts/members.py +706 -0
- package/scripts/migrate_core.py +2 -2
- package/scripts/path_config.py +5 -5
- package/scripts/pipeline_diagnose.py +82 -31
- package/scripts/pipeline_git.py +124 -34
- package/scripts/pipeline_state.py +287 -87
- package/scripts/pipeline_undo.py +218 -30
- package/scripts/promote_lookahead.py +1 -1
- package/scripts/review_panel.py +1 -1
- package/scripts/runtime_store.py +55 -11
- package/scripts/skill-resources.json +37 -5
- package/scripts/state_checkpoint.py +135 -44
- package/scripts/state_promote.py +2 -0
- package/scripts/status_runtime.py +58 -10
- package/scripts/sync_skill_resources.py +8 -7
- package/scripts/task_context.py +12 -3
- package/scripts/token_budget.py +12 -8
- package/scripts/wizard.mjs +1 -0
- package/scripts/workflow_run.py +107 -30
- package/scripts/worktree_paths.py +72 -10
- package/skills/gsd-path/BUILD.md +7 -1
- package/skills/gsd-path/DECIDE.md +11 -4
- package/skills/gsd-path/DEFINE.md +36 -4
- package/skills/gsd-path/DOCS-AUDIT.md +6 -4
- package/skills/gsd-path/INSPECT.md +5 -4
- package/skills/gsd-path/PLAN.md +18 -4
- package/skills/gsd-path/RESEARCH.md +13 -1
- package/skills/gsd-path/ROADMAP.md +2 -1
- package/skills/gsd-path/SHIP.md +33 -11
- package/skills/gsd-path/SKILL.md +45 -1
- package/skills/gsd-path/references/build-native.md +63 -15
- package/skills/gsd-path/references/codebase-mapper.md +2 -1
- package/skills/gsd-path/references/dispatch.md +13 -0
- package/skills/gsd-path/references/docs-auditor.md +7 -1
- package/skills/gsd-path/references/operating-rules.md +214 -0
- package/skills/gsd-path/references/planner.md +22 -4
- package/skills/gsd-path/references/reviewer-final.md +20 -2
- package/skills/gsd-path/references/reviewer-wave.md +18 -5
- 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 +619 -18
- package/skills/gsd-path/scripts/archive_milestone.py +160 -86
- package/skills/gsd-path/scripts/bootstrap_repository.py +8 -5
- package/skills/gsd-path/scripts/build_recovery.py +100 -32
- package/skills/gsd-path/scripts/build_state.py +143 -24
- package/skills/gsd-path/scripts/check_docs_audit.py +91 -7
- package/skills/gsd-path/scripts/check_handoffs.py +662 -50
- package/skills/gsd-path/scripts/check_task_briefs.py +338 -30
- package/skills/gsd-path/scripts/check_update.py +2 -2
- package/skills/gsd-path/scripts/detect_project.py +587 -32
- package/skills/gsd-path/scripts/discussion_records.py +19 -13
- package/skills/gsd-path/scripts/discussion_validate.py +152 -50
- package/skills/gsd-path/scripts/dispatch_driver.py +501 -46
- package/skills/gsd-path/scripts/git_guard.py +118 -31
- package/skills/gsd-path/scripts/integration.py +580 -25
- package/skills/gsd-path/scripts/isolation.py +1438 -57
- package/skills/gsd-path/scripts/lean_verification.py +99 -19
- package/skills/gsd-path/scripts/loop_run.py +31 -29
- package/skills/gsd-path/scripts/members.py +706 -0
- package/skills/gsd-path/scripts/path_config.py +5 -5
- package/skills/gsd-path/scripts/pipeline_git.py +124 -34
- package/skills/gsd-path/scripts/pipeline_state.py +287 -87
- package/skills/gsd-path/scripts/promote_lookahead.py +1 -1
- package/skills/gsd-path/scripts/review_panel.py +1 -1
- package/skills/gsd-path/scripts/state_checkpoint.py +135 -44
- package/skills/gsd-path/scripts/state_promote.py +2 -0
- package/skills/gsd-path/scripts/status_runtime.py +58 -10
- package/skills/gsd-path/scripts/task_context.py +12 -3
- package/skills/gsd-path/scripts/token_budget.py +12 -8
- package/skills/gsd-path/scripts/workflow_run.py +107 -30
- package/skills/gsd-path/scripts/worktree_paths.py +72 -10
- package/skills/gsd-path/templates/codebase.md +1 -1
- package/skills/gsd-path/templates/docs-audit.md +6 -2
- package/skills/gsd-path/templates/final-review.md +16 -4
- package/skills/gsd-path/templates/gap-review.md +2 -1
- 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/task.md +1 -0
- package/skills/gsd-path/templates/wave-review.md +16 -4
- package/skills/gsd-path-build/SKILL.md +7 -1
- package/skills/gsd-path-build/references/build-native.md +63 -15
- package/skills/gsd-path-build/references/dispatch.md +13 -0
- package/skills/gsd-path-build/references/operating-rules.md +214 -0
- package/skills/gsd-path-build/references/reviewer-final.md +20 -2
- package/skills/gsd-path-build/references/reviewer-wave.md +18 -5
- 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 +619 -18
- package/skills/gsd-path-build/scripts/archive_milestone.py +160 -86
- package/skills/gsd-path-build/scripts/build_recovery.py +100 -32
- package/skills/gsd-path-build/scripts/build_state.py +143 -24
- package/skills/gsd-path-build/scripts/check_handoffs.py +662 -50
- package/skills/gsd-path-build/scripts/check_task_briefs.py +338 -30
- package/skills/gsd-path-build/scripts/detect_project.py +587 -32
- package/skills/gsd-path-build/scripts/discussion_records.py +19 -13
- package/skills/gsd-path-build/scripts/discussion_validate.py +152 -50
- package/skills/gsd-path-build/scripts/dispatch_driver.py +501 -46
- package/skills/gsd-path-build/scripts/integration.py +580 -25
- package/skills/gsd-path-build/scripts/isolation.py +1438 -57
- package/skills/gsd-path-build/scripts/loop_run.py +31 -29
- package/skills/gsd-path-build/scripts/members.py +706 -0
- package/skills/gsd-path-build/scripts/path_config.py +5 -5
- package/skills/gsd-path-build/scripts/pipeline_git.py +124 -34
- package/skills/gsd-path-build/scripts/pipeline_state.py +287 -87
- package/skills/gsd-path-build/scripts/promote_lookahead.py +1 -1
- package/skills/gsd-path-build/scripts/review_panel.py +1 -1
- package/skills/gsd-path-build/scripts/state_checkpoint.py +135 -44
- package/skills/gsd-path-build/scripts/state_promote.py +2 -0
- package/skills/gsd-path-build/scripts/task_context.py +12 -3
- package/skills/gsd-path-build/scripts/token_budget.py +12 -8
- package/skills/gsd-path-build/scripts/workflow_run.py +107 -30
- package/skills/gsd-path-build/scripts/worktree_paths.py +72 -10
- package/skills/gsd-path-build/templates/task.md +1 -0
- package/skills/gsd-path-build/templates/wave-review.md +16 -4
- package/skills/gsd-path-decide/SKILL.md +11 -4
- package/skills/gsd-path-decide/references/dispatch.md +13 -0
- package/skills/gsd-path-decide/references/operating-rules.md +214 -0
- package/skills/gsd-path-decide/scripts/_common.py +619 -18
- package/skills/gsd-path-decide/scripts/archive_milestone.py +160 -86
- package/skills/gsd-path-decide/scripts/build_recovery.py +100 -32
- package/skills/gsd-path-decide/scripts/check_handoffs.py +662 -50
- package/skills/gsd-path-decide/scripts/check_task_briefs.py +338 -30
- package/skills/gsd-path-decide/scripts/detect_project.py +587 -32
- package/skills/gsd-path-decide/scripts/discussion_records.py +19 -13
- package/skills/gsd-path-decide/scripts/discussion_validate.py +152 -50
- package/skills/gsd-path-decide/scripts/integration.py +580 -25
- package/skills/gsd-path-decide/scripts/isolation.py +1438 -57
- package/skills/gsd-path-decide/scripts/loop_run.py +31 -29
- package/skills/gsd-path-decide/scripts/members.py +706 -0
- package/skills/gsd-path-decide/scripts/path_config.py +5 -5
- package/skills/gsd-path-decide/scripts/pipeline_git.py +124 -34
- package/skills/gsd-path-decide/scripts/pipeline_state.py +287 -87
- package/skills/gsd-path-decide/scripts/promote_lookahead.py +1 -1
- package/skills/gsd-path-decide/scripts/review_panel.py +1 -1
- package/skills/gsd-path-decide/scripts/state_checkpoint.py +135 -44
- package/skills/gsd-path-decide/scripts/state_promote.py +2 -0
- package/skills/gsd-path-decide/scripts/worktree_paths.py +72 -10
- package/skills/gsd-path-define/SKILL.md +36 -4
- package/skills/gsd-path-define/references/operating-rules.md +214 -0
- package/skills/gsd-path-define/references/spec-probes.md +150 -0
- package/skills/gsd-path-define/scripts/_common.py +619 -18
- package/skills/gsd-path-define/scripts/archive_milestone.py +160 -86
- package/skills/gsd-path-define/scripts/build_recovery.py +100 -32
- package/skills/gsd-path-define/scripts/check_handoffs.py +662 -50
- package/skills/gsd-path-define/scripts/check_task_briefs.py +338 -30
- package/skills/gsd-path-define/scripts/detect_project.py +587 -32
- package/skills/gsd-path-define/scripts/discussion_records.py +19 -13
- package/skills/gsd-path-define/scripts/discussion_validate.py +152 -50
- package/skills/gsd-path-define/scripts/integration.py +580 -25
- package/skills/gsd-path-define/scripts/isolation.py +1438 -57
- package/skills/gsd-path-define/scripts/loop_run.py +31 -29
- package/skills/gsd-path-define/scripts/members.py +706 -0
- package/skills/gsd-path-define/scripts/path_config.py +5 -5
- package/skills/gsd-path-define/scripts/pipeline_git.py +124 -34
- package/skills/gsd-path-define/scripts/pipeline_state.py +287 -87
- package/skills/gsd-path-define/scripts/promote_lookahead.py +1 -1
- package/skills/gsd-path-define/scripts/review_panel.py +1 -1
- package/skills/gsd-path-define/scripts/state_checkpoint.py +135 -44
- package/skills/gsd-path-define/scripts/state_promote.py +2 -0
- package/skills/gsd-path-define/scripts/worktree_paths.py +72 -10
- 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/SKILL.md +2 -1
- package/skills/gsd-path-discuss/references/operating-rules.md +214 -0
- package/skills/gsd-path-discuss/scripts/_common.py +619 -18
- package/skills/gsd-path-discuss/scripts/discussion_records.py +19 -13
- package/skills/gsd-path-discuss/scripts/loop_run.py +31 -29
- package/skills/gsd-path-discuss/scripts/path_config.py +5 -5
- package/skills/gsd-path-discuss/scripts/review_panel.py +1 -1
- package/skills/gsd-path-discuss/scripts/worktree_paths.py +72 -10
- package/skills/gsd-path-docs-audit/SKILL.md +6 -4
- package/skills/gsd-path-docs-audit/references/dispatch.md +13 -0
- package/skills/gsd-path-docs-audit/references/docs-auditor.md +7 -1
- package/skills/gsd-path-docs-audit/references/operating-rules.md +214 -0
- package/skills/gsd-path-docs-audit/scripts/_common.py +619 -18
- package/skills/gsd-path-docs-audit/scripts/archive_milestone.py +160 -86
- package/skills/gsd-path-docs-audit/scripts/build_recovery.py +100 -32
- package/skills/gsd-path-docs-audit/scripts/check_docs_audit.py +91 -7
- package/skills/gsd-path-docs-audit/scripts/check_handoffs.py +662 -50
- package/skills/gsd-path-docs-audit/scripts/check_task_briefs.py +338 -30
- package/skills/gsd-path-docs-audit/scripts/detect_project.py +587 -32
- package/skills/gsd-path-docs-audit/scripts/discussion_records.py +19 -13
- package/skills/gsd-path-docs-audit/scripts/discussion_validate.py +152 -50
- package/skills/gsd-path-docs-audit/scripts/integration.py +580 -25
- package/skills/gsd-path-docs-audit/scripts/isolation.py +1438 -57
- package/skills/gsd-path-docs-audit/scripts/loop_run.py +31 -29
- package/skills/gsd-path-docs-audit/scripts/members.py +706 -0
- package/skills/gsd-path-docs-audit/scripts/path_config.py +5 -5
- package/skills/gsd-path-docs-audit/scripts/pipeline_git.py +124 -34
- package/skills/gsd-path-docs-audit/scripts/pipeline_state.py +287 -87
- package/skills/gsd-path-docs-audit/scripts/promote_lookahead.py +1 -1
- package/skills/gsd-path-docs-audit/scripts/review_panel.py +1 -1
- package/skills/gsd-path-docs-audit/scripts/state_checkpoint.py +135 -44
- package/skills/gsd-path-docs-audit/scripts/state_promote.py +2 -0
- package/skills/gsd-path-docs-audit/scripts/worktree_paths.py +72 -10
- package/skills/gsd-path-docs-audit/templates/docs-audit.md +6 -2
- package/skills/gsd-path-forensics/SKILL.md +2 -1
- package/skills/gsd-path-forensics/references/operating-rules.md +214 -0
- package/skills/gsd-path-forensics/scripts/_common.py +619 -18
- package/skills/gsd-path-forensics/scripts/discussion_records.py +19 -13
- package/skills/gsd-path-forensics/scripts/loop_run.py +31 -29
- package/skills/gsd-path-forensics/scripts/path_config.py +5 -5
- package/skills/gsd-path-forensics/scripts/pipeline_diagnose.py +82 -31
- package/skills/gsd-path-forensics/scripts/pipeline_undo.py +218 -30
- package/skills/gsd-path-forensics/scripts/review_panel.py +1 -1
- package/skills/gsd-path-forensics/scripts/worktree_paths.py +72 -10
- package/skills/gsd-path-inspect/SKILL.md +5 -4
- package/skills/gsd-path-inspect/references/codebase-mapper.md +2 -1
- package/skills/gsd-path-inspect/references/dispatch.md +13 -0
- package/skills/gsd-path-inspect/references/docs-auditor.md +7 -1
- package/skills/gsd-path-inspect/references/operating-rules.md +214 -0
- package/skills/gsd-path-inspect/scripts/_common.py +619 -18
- package/skills/gsd-path-inspect/scripts/archive_milestone.py +160 -86
- package/skills/gsd-path-inspect/scripts/build_recovery.py +100 -32
- package/skills/gsd-path-inspect/scripts/check_docs_audit.py +91 -7
- package/skills/gsd-path-inspect/scripts/check_handoffs.py +662 -50
- package/skills/gsd-path-inspect/scripts/check_task_briefs.py +338 -30
- package/skills/gsd-path-inspect/scripts/detect_project.py +587 -32
- package/skills/gsd-path-inspect/scripts/discussion_records.py +19 -13
- package/skills/gsd-path-inspect/scripts/discussion_validate.py +152 -50
- package/skills/gsd-path-inspect/scripts/integration.py +580 -25
- package/skills/gsd-path-inspect/scripts/isolation.py +1438 -57
- package/skills/gsd-path-inspect/scripts/loop_run.py +31 -29
- package/skills/gsd-path-inspect/scripts/members.py +706 -0
- package/skills/gsd-path-inspect/scripts/path_config.py +5 -5
- package/skills/gsd-path-inspect/scripts/pipeline_git.py +124 -34
- package/skills/gsd-path-inspect/scripts/pipeline_state.py +287 -87
- package/skills/gsd-path-inspect/scripts/promote_lookahead.py +1 -1
- package/skills/gsd-path-inspect/scripts/review_panel.py +1 -1
- package/skills/gsd-path-inspect/scripts/state_checkpoint.py +135 -44
- package/skills/gsd-path-inspect/scripts/state_promote.py +2 -0
- package/skills/gsd-path-inspect/scripts/workflow_run.py +107 -30
- package/skills/gsd-path-inspect/scripts/worktree_paths.py +72 -10
- package/skills/gsd-path-inspect/templates/codebase.md +1 -1
- package/skills/gsd-path-inspect/templates/docs-audit.md +6 -2
- package/skills/gsd-path-inspect/templates/state.md +1 -1
- package/skills/gsd-path-loop/SKILL.md +2 -1
- package/skills/gsd-path-loop/references/operating-rules.md +214 -0
- package/skills/gsd-path-loop/scripts/_common.py +619 -18
- package/skills/gsd-path-loop/scripts/discussion_records.py +19 -13
- package/skills/gsd-path-loop/scripts/loop_run.py +31 -29
- package/skills/gsd-path-loop/scripts/path_config.py +5 -5
- package/skills/gsd-path-loop/scripts/review_panel.py +1 -1
- package/skills/gsd-path-loop/scripts/worktree_paths.py +72 -10
- package/skills/gsd-path-migrate/SKILL.md +2 -1
- package/skills/gsd-path-migrate/references/operating-rules.md +214 -0
- package/skills/gsd-path-migrate/scripts/_common.py +619 -18
- package/skills/gsd-path-migrate/scripts/core_hook_gate.py +10 -1
- package/skills/gsd-path-migrate/scripts/core_hook_settings.py +11 -4
- package/skills/gsd-path-migrate/scripts/discussion_records.py +19 -13
- package/skills/gsd-path-migrate/scripts/loop_run.py +31 -29
- package/skills/gsd-path-migrate/scripts/migrate_core.py +2 -2
- package/skills/gsd-path-migrate/scripts/path_config.py +5 -5
- package/skills/gsd-path-migrate/scripts/review_panel.py +1 -1
- package/skills/gsd-path-plan/SKILL.md +18 -4
- package/skills/gsd-path-plan/references/dispatch.md +13 -0
- package/skills/gsd-path-plan/references/operating-rules.md +214 -0
- package/skills/gsd-path-plan/references/planner.md +22 -4
- package/skills/gsd-path-plan/references/spec-probes.md +150 -0
- package/skills/gsd-path-plan/scripts/_common.py +619 -18
- package/skills/gsd-path-plan/scripts/archive_milestone.py +160 -86
- package/skills/gsd-path-plan/scripts/build_recovery.py +100 -32
- package/skills/gsd-path-plan/scripts/check_handoffs.py +662 -50
- package/skills/gsd-path-plan/scripts/check_task_briefs.py +338 -30
- package/skills/gsd-path-plan/scripts/detect_project.py +587 -32
- package/skills/gsd-path-plan/scripts/discussion_records.py +19 -13
- package/skills/gsd-path-plan/scripts/discussion_validate.py +152 -50
- package/skills/gsd-path-plan/scripts/integration.py +580 -25
- package/skills/gsd-path-plan/scripts/isolation.py +1438 -57
- package/skills/gsd-path-plan/scripts/loop_run.py +31 -29
- package/skills/gsd-path-plan/scripts/members.py +706 -0
- package/skills/gsd-path-plan/scripts/path_config.py +5 -5
- package/skills/gsd-path-plan/scripts/pipeline_git.py +124 -34
- package/skills/gsd-path-plan/scripts/pipeline_state.py +287 -87
- package/skills/gsd-path-plan/scripts/promote_lookahead.py +1 -1
- package/skills/gsd-path-plan/scripts/review_panel.py +1 -1
- package/skills/gsd-path-plan/scripts/state_checkpoint.py +135 -44
- package/skills/gsd-path-plan/scripts/state_promote.py +2 -0
- package/skills/gsd-path-plan/scripts/workflow_run.py +107 -30
- package/skills/gsd-path-plan/scripts/worktree_paths.py +72 -10
- 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 +13 -1
- package/skills/gsd-path-research/references/dispatch.md +13 -0
- package/skills/gsd-path-research/references/operating-rules.md +214 -0
- package/skills/gsd-path-research/scripts/_common.py +619 -18
- package/skills/gsd-path-research/scripts/archive_milestone.py +160 -86
- package/skills/gsd-path-research/scripts/build_recovery.py +100 -32
- package/skills/gsd-path-research/scripts/check_handoffs.py +662 -50
- package/skills/gsd-path-research/scripts/check_task_briefs.py +338 -30
- package/skills/gsd-path-research/scripts/detect_project.py +587 -32
- package/skills/gsd-path-research/scripts/discussion_records.py +19 -13
- package/skills/gsd-path-research/scripts/discussion_validate.py +152 -50
- package/skills/gsd-path-research/scripts/integration.py +580 -25
- package/skills/gsd-path-research/scripts/isolation.py +1438 -57
- package/skills/gsd-path-research/scripts/loop_run.py +31 -29
- package/skills/gsd-path-research/scripts/members.py +706 -0
- package/skills/gsd-path-research/scripts/path_config.py +5 -5
- package/skills/gsd-path-research/scripts/pipeline_git.py +124 -34
- package/skills/gsd-path-research/scripts/pipeline_state.py +287 -87
- package/skills/gsd-path-research/scripts/promote_lookahead.py +1 -1
- package/skills/gsd-path-research/scripts/review_panel.py +1 -1
- package/skills/gsd-path-research/scripts/state_checkpoint.py +135 -44
- package/skills/gsd-path-research/scripts/state_promote.py +2 -0
- package/skills/gsd-path-research/scripts/worktree_paths.py +72 -10
- package/skills/gsd-path-research/templates/research-handoff.md +14 -3
- package/skills/gsd-path-roadmap/SKILL.md +2 -1
- package/skills/gsd-path-roadmap/references/dispatch.md +13 -0
- package/skills/gsd-path-roadmap/references/operating-rules.md +214 -0
- package/skills/gsd-path-roadmap/scripts/_common.py +619 -18
- package/skills/gsd-path-roadmap/scripts/archive_milestone.py +160 -86
- package/skills/gsd-path-roadmap/scripts/build_recovery.py +100 -32
- package/skills/gsd-path-roadmap/scripts/check_handoffs.py +662 -50
- package/skills/gsd-path-roadmap/scripts/check_task_briefs.py +338 -30
- package/skills/gsd-path-roadmap/scripts/detect_project.py +587 -32
- package/skills/gsd-path-roadmap/scripts/discussion_records.py +19 -13
- package/skills/gsd-path-roadmap/scripts/discussion_validate.py +152 -50
- package/skills/gsd-path-roadmap/scripts/integration.py +580 -25
- package/skills/gsd-path-roadmap/scripts/isolation.py +1438 -57
- package/skills/gsd-path-roadmap/scripts/loop_run.py +31 -29
- package/skills/gsd-path-roadmap/scripts/members.py +706 -0
- package/skills/gsd-path-roadmap/scripts/path_config.py +5 -5
- package/skills/gsd-path-roadmap/scripts/pipeline_git.py +124 -34
- package/skills/gsd-path-roadmap/scripts/pipeline_state.py +287 -87
- package/skills/gsd-path-roadmap/scripts/promote_lookahead.py +1 -1
- package/skills/gsd-path-roadmap/scripts/review_panel.py +1 -1
- package/skills/gsd-path-roadmap/scripts/state_checkpoint.py +135 -44
- package/skills/gsd-path-roadmap/scripts/state_promote.py +2 -0
- package/skills/gsd-path-roadmap/scripts/worktree_paths.py +72 -10
- package/skills/gsd-path-ship/SKILL.md +33 -11
- package/skills/gsd-path-ship/references/dispatch.md +13 -0
- package/skills/gsd-path-ship/references/operating-rules.md +214 -0
- package/skills/gsd-path-ship/references/reviewer-final.md +20 -2
- package/skills/gsd-path-ship/references/reviewer-wave.md +18 -5
- 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 +619 -18
- package/skills/gsd-path-ship/scripts/archive_milestone.py +160 -86
- package/skills/gsd-path-ship/scripts/build_recovery.py +100 -32
- package/skills/gsd-path-ship/scripts/build_state.py +143 -24
- package/skills/gsd-path-ship/scripts/check_handoffs.py +662 -50
- package/skills/gsd-path-ship/scripts/check_task_briefs.py +338 -30
- package/skills/gsd-path-ship/scripts/detect_project.py +587 -32
- package/skills/gsd-path-ship/scripts/discussion_records.py +19 -13
- package/skills/gsd-path-ship/scripts/discussion_validate.py +152 -50
- package/skills/gsd-path-ship/scripts/integration.py +580 -25
- package/skills/gsd-path-ship/scripts/isolation.py +1438 -57
- package/skills/gsd-path-ship/scripts/lean_verification.py +99 -19
- package/skills/gsd-path-ship/scripts/loop_run.py +31 -29
- package/skills/gsd-path-ship/scripts/members.py +706 -0
- package/skills/gsd-path-ship/scripts/path_config.py +5 -5
- package/skills/gsd-path-ship/scripts/pipeline_git.py +124 -34
- package/skills/gsd-path-ship/scripts/pipeline_state.py +287 -87
- package/skills/gsd-path-ship/scripts/promote_lookahead.py +1 -1
- package/skills/gsd-path-ship/scripts/review_panel.py +1 -1
- package/skills/gsd-path-ship/scripts/state_checkpoint.py +135 -44
- package/skills/gsd-path-ship/scripts/state_promote.py +2 -0
- package/skills/gsd-path-ship/scripts/workflow_run.py +107 -30
- package/skills/gsd-path-ship/scripts/worktree_paths.py +72 -10
- package/skills/gsd-path-ship/templates/final-review.md +16 -4
- package/skills/gsd-path-ship/templates/gap-review.md +2 -1
- package/skills/gsd-path-ship/templates/patch-findings.md +3 -3
- package/skills/gsd-path-ship/templates/wave-review.md +16 -4
- package/skills/gsd-path-undo/SKILL.md +2 -1
- package/skills/gsd-path-undo/references/operating-rules.md +214 -0
- package/skills/gsd-path-undo/scripts/_common.py +619 -18
- package/skills/gsd-path-undo/scripts/discussion_records.py +19 -13
- package/skills/gsd-path-undo/scripts/loop_run.py +31 -29
- package/skills/gsd-path-undo/scripts/path_config.py +5 -5
- package/skills/gsd-path-undo/scripts/pipeline_undo.py +218 -30
- package/skills/gsd-path-undo/scripts/review_panel.py +1 -1
- package/skills/gsd-path-undo/scripts/worktree_paths.py +72 -10
- package/skills/path/BUILD.md +7 -1
- package/skills/path/DECIDE.md +11 -4
- package/skills/path/DEFINE.md +36 -4
- package/skills/path/DOCS-AUDIT.md +6 -4
- package/skills/path/INSPECT.md +5 -4
- package/skills/path/PLAN.md +18 -4
- package/skills/path/RESEARCH.md +13 -1
- package/skills/path/ROADMAP.md +2 -1
- package/skills/path/SHIP.md +33 -11
- package/skills/path/SKILL.md +45 -1
- package/skills/path/references/build-native.md +63 -15
- package/skills/path/references/codebase-mapper.md +2 -1
- package/skills/path/references/dispatch.md +13 -0
- package/skills/path/references/docs-auditor.md +7 -1
- package/skills/path/references/operating-rules.md +214 -0
- package/skills/path/references/planner.md +22 -4
- package/skills/path/references/reviewer-final.md +20 -2
- package/skills/path/references/reviewer-wave.md +18 -5
- package/skills/path/references/reviewer.md +3 -1
- package/skills/path/references/spec-probes.md +150 -0
- package/skills/path/scripts/_common.py +619 -18
- package/skills/path/scripts/archive_milestone.py +160 -86
- package/skills/path/scripts/bootstrap_repository.py +8 -5
- package/skills/path/scripts/build_recovery.py +100 -32
- package/skills/path/scripts/build_state.py +143 -24
- package/skills/path/scripts/check_docs_audit.py +91 -7
- package/skills/path/scripts/check_handoffs.py +662 -50
- package/skills/path/scripts/check_task_briefs.py +338 -30
- package/skills/path/scripts/check_update.py +2 -2
- package/skills/path/scripts/detect_project.py +587 -32
- package/skills/path/scripts/discussion_records.py +19 -13
- package/skills/path/scripts/discussion_validate.py +152 -50
- package/skills/path/scripts/dispatch_driver.py +501 -46
- package/skills/path/scripts/git_guard.py +118 -31
- package/skills/path/scripts/integration.py +580 -25
- package/skills/path/scripts/isolation.py +1438 -57
- package/skills/path/scripts/lean_verification.py +99 -19
- package/skills/path/scripts/loop_run.py +31 -29
- package/skills/path/scripts/members.py +706 -0
- package/skills/path/scripts/path_config.py +5 -5
- package/skills/path/scripts/pipeline_git.py +124 -34
- package/skills/path/scripts/pipeline_state.py +287 -87
- package/skills/path/scripts/promote_lookahead.py +1 -1
- package/skills/path/scripts/review_panel.py +1 -1
- package/skills/path/scripts/state_checkpoint.py +135 -44
- package/skills/path/scripts/state_promote.py +2 -0
- package/skills/path/scripts/status_runtime.py +58 -10
- package/skills/path/scripts/task_context.py +12 -3
- package/skills/path/scripts/token_budget.py +12 -8
- package/skills/path/scripts/workflow_run.py +107 -30
- package/skills/path/scripts/worktree_paths.py +72 -10
- package/skills/path/templates/codebase.md +1 -1
- package/skills/path/templates/docs-audit.md +6 -2
- package/skills/path/templates/final-review.md +16 -4
- package/skills/path/templates/gap-review.md +2 -1
- 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/task.md +1 -0
- package/skills/path/templates/wave-review.md +16 -4
- 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,11 +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
|
|
22
23
|
import shlex
|
|
23
|
-
import signal
|
|
24
24
|
import subprocess
|
|
25
25
|
import sys
|
|
26
26
|
import tempfile
|
|
@@ -33,6 +33,7 @@ try:
|
|
|
33
33
|
from scripts import (_common, archive_milestone, build_state, discussion_validate, isolation,
|
|
34
34
|
pipeline_state, review_findings, review_panel, workflow_run, task_context, model_policy)
|
|
35
35
|
from scripts import check_handoffs as contracts
|
|
36
|
+
from scripts.pipeline_git import is_bound_branch
|
|
36
37
|
except ImportError: # bundled copy inside a skill's scripts directory
|
|
37
38
|
import _common
|
|
38
39
|
import archive_milestone
|
|
@@ -46,6 +47,7 @@ except ImportError: # bundled copy inside a skill's scripts directory
|
|
|
46
47
|
import review_panel
|
|
47
48
|
import workflow_run
|
|
48
49
|
import model_policy
|
|
50
|
+
from pipeline_git import is_bound_branch
|
|
49
51
|
|
|
50
52
|
if os.name == "nt":
|
|
51
53
|
import msvcrt
|
|
@@ -73,12 +75,91 @@ class DriverStop(RuntimeError):
|
|
|
73
75
|
STOP_ERRORS = (DriverStop, isolation.IsolationError, build_state.BuildStateError,
|
|
74
76
|
contracts.HandoffError, review_findings.ReviewFindingsError,
|
|
75
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
|
+
)
|
|
76
94
|
RECOVERY_BLOCKED = "recovery blocked"
|
|
77
95
|
# The stops the build contract answers with build/blocked when the round raises them (steps 1 and 2);
|
|
78
96
|
# the same stops from review, panel, or skeptics stay with the parent.
|
|
79
97
|
BLOCKING_STOPS = {"dependency-deadlock": "dependency deadlock", RECOVERY_BLOCKED: "blocked recovery"}
|
|
80
98
|
|
|
81
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
|
+
|
|
82
163
|
def now() -> str:
|
|
83
164
|
return datetime.now(timezone.utc).isoformat(timespec="seconds")
|
|
84
165
|
|
|
@@ -109,6 +190,169 @@ def attempts_used(root: Path, task_id: str) -> int:
|
|
|
109
190
|
if load_state(path).get("origin") == "dispatch")
|
|
110
191
|
|
|
111
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
|
+
|
|
112
356
|
def acquire_lock(primary: Path) -> BinaryIO:
|
|
113
357
|
root = records_root(primary)
|
|
114
358
|
root.mkdir(parents=True, exist_ok=True)
|
|
@@ -170,12 +414,7 @@ def child_running(state: Dict[str, object]) -> bool:
|
|
|
170
414
|
return state.get("finished_at") is None and bool(state.get("pid")) and process_alive(int(state["pid"]))
|
|
171
415
|
|
|
172
416
|
|
|
173
|
-
|
|
174
|
-
try:
|
|
175
|
-
os.kill(pid, 0)
|
|
176
|
-
except ProcessLookupError:
|
|
177
|
-
return False
|
|
178
|
-
return True
|
|
417
|
+
process_alive = _common.process_alive
|
|
179
418
|
|
|
180
419
|
|
|
181
420
|
# --- child process ---------------------------------------------------------
|
|
@@ -208,13 +447,13 @@ def child_main(state_path: Path) -> int:
|
|
|
208
447
|
attempt_dir = state_path.parent
|
|
209
448
|
with open(attempt_dir / "brief.md", "rb") as brief, \
|
|
210
449
|
open(attempt_dir / "stdout", "wb") as out, open(attempt_dir / "stderr", "wb") as err:
|
|
211
|
-
process =
|
|
212
|
-
|
|
450
|
+
process = _common.popen_group(_common.resolve_argv(state["command"]), cwd=state["worktree"],
|
|
451
|
+
stdin=brief, stdout=out, stderr=err)
|
|
213
452
|
try:
|
|
214
453
|
exit_code: Optional[int] = process.wait(timeout=state.get("child_timeout"))
|
|
215
454
|
timed_out = False
|
|
216
455
|
except subprocess.TimeoutExpired:
|
|
217
|
-
|
|
456
|
+
_common.kill_tree(process)
|
|
218
457
|
process.wait()
|
|
219
458
|
exit_code, timed_out = None, True
|
|
220
459
|
save_state(attempt_dir / "exit.json",
|
|
@@ -224,6 +463,7 @@ def child_main(state_path: Path) -> int:
|
|
|
224
463
|
|
|
225
464
|
def brief_text(state: Dict[str, object], role_brief: Path, task_template: Path) -> str:
|
|
226
465
|
worktree = Path(str(state["worktree"]))
|
|
466
|
+
project_root = Path(str(state.get("project_root") or worktree))
|
|
227
467
|
task_id = state["task_id"]
|
|
228
468
|
agents = worktree / "AGENTS.md"
|
|
229
469
|
workflow = worktree / "WORKFLOW.md"
|
|
@@ -234,12 +474,14 @@ def brief_text(state: Dict[str, object], role_brief: Path, task_template: Path)
|
|
|
234
474
|
f"Work only in this worktree root: {worktree}",
|
|
235
475
|
f"Task file: {worktree / str(state['task_file'])}",
|
|
236
476
|
f"Task template: {task_template}",
|
|
237
|
-
f"INTENT.md: {
|
|
477
|
+
f"INTENT.md: {project_root / '.project/intent/INTENT.md'}",
|
|
238
478
|
f"AGENTS.md: {agents if agents.exists() else 'absent'}",
|
|
239
479
|
f"WORKFLOW.md: {workflow if workflow.exists() else 'absent'}",
|
|
240
480
|
f"Recorded base: {state['base']}",
|
|
241
481
|
f"Dispatch mode: {state['mode']} ("
|
|
242
482
|
+ ("the primary worktree on the bound branch" if state["mode"] == "serial"
|
|
483
|
+
else f"an isolated worktree of member {state['member']}; never touch the member team's "
|
|
484
|
+
"checkout or the coordinator" if state["mode"] == "member"
|
|
243
485
|
else "an isolated task worktree; never touch the primary") + ").",
|
|
244
486
|
"Next consumer: the build orchestrator reruns Verify in an isolated checkout and lands "
|
|
245
487
|
"the task. Gate: Verify passes and only the task's declared files plus its Log changed.",
|
|
@@ -248,7 +490,7 @@ def brief_text(state: Dict[str, object], role_brief: Path, task_template: Path)
|
|
|
248
490
|
if state.get("answered"):
|
|
249
491
|
lines.append(f"The task Log now records an `{ANSWER_MARK}` to your earlier question; "
|
|
250
492
|
"continue from it.")
|
|
251
|
-
lines.append(task_context.render(
|
|
493
|
+
lines.append(task_context.render(project_root, worktree / str(state['task_file'])))
|
|
252
494
|
return "\n".join(lines) + "\n"
|
|
253
495
|
|
|
254
496
|
|
|
@@ -282,7 +524,7 @@ def selected_command(state: Dict[str, object], options: argparse.Namespace, comm
|
|
|
282
524
|
if caps_path is None:
|
|
283
525
|
if overrides or previous or model_policy.policy_path(primary, project_dir).exists():
|
|
284
526
|
raise model_policy.PolicyError('model policy requires --model-capabilities for this dispatch')
|
|
285
|
-
return
|
|
527
|
+
return _common.split_command(command), None
|
|
286
528
|
caps = model_policy.read_json(caps_path)
|
|
287
529
|
action = getattr(options, 'action', 'round')
|
|
288
530
|
role = {'round': 'coder', 'review': 'reviewer', 'panel': 'review_panel',
|
|
@@ -297,7 +539,7 @@ def selected_command(state: Dict[str, object], options: argparse.Namespace, comm
|
|
|
297
539
|
dict(overrides, model=state['slug']), previous)
|
|
298
540
|
model_policy.validate_panel(selection, state['family'], state.get('excluded_families', []))
|
|
299
541
|
state['slug'] = selection['selected']['model']
|
|
300
|
-
return model_policy.command_args(
|
|
542
|
+
return model_policy.command_args(_common.split_command(command), selection), selection
|
|
301
543
|
|
|
302
544
|
|
|
303
545
|
def retain_selection(root: Path, state: Dict[str, object]) -> Dict[str, object]:
|
|
@@ -311,7 +553,7 @@ def retain_selection(root: Path, state: Dict[str, object]) -> Dict[str, object]:
|
|
|
311
553
|
raise DriverStop('cannot redispatch while the previous child is active')
|
|
312
554
|
if prior.get('model_selection'):
|
|
313
555
|
state['model_selection'] = prior['model_selection']
|
|
314
|
-
|
|
556
|
+
elif prior.get('command'):
|
|
315
557
|
state['command'] = prior['command']
|
|
316
558
|
reassignment = task_dir / 'model-reassignment.json'
|
|
317
559
|
if reassignment.exists():
|
|
@@ -327,25 +569,35 @@ def spawn(root: Path, state: Dict[str, object], options: argparse.Namespace,
|
|
|
327
569
|
state = retain_selection(root, state)
|
|
328
570
|
task_dir = root / str(state["task_id"])
|
|
329
571
|
argv, selection = selected_command(state, options, command or options.child_command)
|
|
330
|
-
|
|
331
|
-
attempt_dir
|
|
332
|
-
|
|
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)
|
|
333
579
|
stale = ("pid", "finished_at", "exit_code", "timed_out", "reason", "question", "commit", "answer",
|
|
334
580
|
"usage_recorded")
|
|
335
|
-
|
|
336
|
-
|
|
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
|
|
337
589
|
if selection is not None:
|
|
338
590
|
state['model_selection'] = selection
|
|
339
591
|
state.update({"attempt": attempt, "command": argv,
|
|
340
592
|
"child_timeout": options.child_timeout, "outcome": None,
|
|
341
593
|
"dispatched_at": now(), "origin": "question" if state.get("answered") else "dispatch"})
|
|
342
594
|
state_path = attempt_dir / "state.json"
|
|
343
|
-
(attempt_dir / "brief.md").
|
|
595
|
+
(attempt_dir / "brief.md").write_bytes(brief(state).encode("utf-8"))
|
|
344
596
|
save_state(state_path, state)
|
|
345
|
-
wrapper =
|
|
597
|
+
wrapper = _common.popen_detached(
|
|
346
598
|
[sys.executable, "-B", str(Path(__file__).resolve()), "_child", "--state", str(state_path)],
|
|
347
599
|
cwd=state["worktree"], stdin=subprocess.DEVNULL, stdout=subprocess.DEVNULL,
|
|
348
|
-
stderr=subprocess.DEVNULL,
|
|
600
|
+
stderr=subprocess.DEVNULL,
|
|
349
601
|
)
|
|
350
602
|
state["_path"] = str(state_path)
|
|
351
603
|
update_state(state, pid=wrapper.pid)
|
|
@@ -356,8 +608,11 @@ def spawn(root: Path, state: Dict[str, object], options: argparse.Namespace,
|
|
|
356
608
|
|
|
357
609
|
|
|
358
610
|
def log_delta(primary: Path, state: Dict[str, object]) -> str:
|
|
359
|
-
|
|
611
|
+
contract_file = state.get("contract_file") or state["task_file"]
|
|
612
|
+
base_text = isolation.git_text(primary, "show", f"{state['base']}:{contract_file}")
|
|
360
613
|
text = (Path(str(state["worktree"])) / str(state["task_file"])).read_text(encoding="utf-8")
|
|
614
|
+
if state["mode"] == "member":
|
|
615
|
+
return isolation.member_log_delta(base_text, text)
|
|
361
616
|
return isolation.task_log_delta(base_text, text)
|
|
362
617
|
|
|
363
618
|
|
|
@@ -374,14 +629,14 @@ def snapshot_tree(primary: Path) -> str:
|
|
|
374
629
|
env = dict(os.environ, GIT_INDEX_FILE=str(Path(temporary) / "index"))
|
|
375
630
|
for arguments in (("read-tree", "HEAD"), ("add", "-A"), ("write-tree",)):
|
|
376
631
|
completed = subprocess.run(("git", "-C", str(primary), *arguments), env=env,
|
|
377
|
-
|
|
632
|
+
encoding="utf-8", errors="replace", capture_output=True)
|
|
378
633
|
if completed.returncode:
|
|
379
634
|
raise DriverStop(f"git {' '.join(arguments)} failed: {completed.stderr.strip()}")
|
|
380
635
|
return completed.stdout.strip()
|
|
381
636
|
|
|
382
637
|
|
|
383
638
|
def run_verify(command: str, cwd: Path) -> Dict[str, object]:
|
|
384
|
-
completed = _common.run_command(
|
|
639
|
+
completed = _common.run_command(*_common.bash_argv(command), cwd=cwd)
|
|
385
640
|
return {"exit_code": completed.returncode, "stdout": completed.stdout, "stderr": completed.stderr}
|
|
386
641
|
|
|
387
642
|
|
|
@@ -394,6 +649,30 @@ def state_from_task(primary: Path, task_id: str) -> Dict[str, object]:
|
|
|
394
649
|
break
|
|
395
650
|
else:
|
|
396
651
|
raise DriverStop(f"task {task_id} has no task file")
|
|
652
|
+
member = fields.get("repo")
|
|
653
|
+
if member:
|
|
654
|
+
# A member activation lives in the live copy inside its member sidecar.
|
|
655
|
+
try:
|
|
656
|
+
checkout, project, _ = isolation._member_context(primary, str(member))
|
|
657
|
+
except isolation.IsolationError as error:
|
|
658
|
+
raise DriverStop(str(error), task=task_id) from error
|
|
659
|
+
task_file = task_path.relative_to(primary).as_posix()
|
|
660
|
+
sidecar = isolation.sidecar_root(checkout, "task", f"{project}-{task_id}").resolve()
|
|
661
|
+
copy = isolation.member_task_copy(sidecar, task_file)
|
|
662
|
+
live, _ = isolation.task_frontmatter(copy.read_text(encoding="utf-8")) if copy.is_file() else ({}, None)
|
|
663
|
+
if not live or live.get("status") != "in-progress" or not live.get("base") or not live.get("member_base"):
|
|
664
|
+
raise DriverStop(f"member task {task_id} has no activated live copy in {sidecar}", task=task_id)
|
|
665
|
+
return {"task_id": task_id, "title": str(live.get("title")), "task_file": copy.relative_to(sidecar).as_posix(),
|
|
666
|
+
"contract_file": task_file, "files": list(live.get("files") or []), "base": str(live["base"]),
|
|
667
|
+
"worktree": str(sidecar), "task_branch": f"{isolation.TASK_BRANCH_PREFIX}{project}-{task_id}",
|
|
668
|
+
"mode": "member", "member": str(member), "member_base": str(live["member_base"]),
|
|
669
|
+
"project_root": str(primary), "sidecar": None}
|
|
670
|
+
if fields.get("status") != "in-progress" or not fields.get("base"):
|
|
671
|
+
# A parallel activation lives only in its isolate; the primary copy stays pending.
|
|
672
|
+
isolate = isolation.sidecar_root(primary, "task", task_id).resolve()
|
|
673
|
+
if isolation.authorized_task_worktree(isolate, isolation.require_bound(primary)):
|
|
674
|
+
fields, _ = isolation.task_frontmatter(
|
|
675
|
+
(isolate / task_path.relative_to(primary)).read_text(encoding="utf-8"))
|
|
397
676
|
if fields.get("status") != "in-progress" or not fields.get("base"):
|
|
398
677
|
raise DriverStop(f"task {task_id} is not an in-progress task with a recorded base")
|
|
399
678
|
def value(key: str) -> Optional[str]:
|
|
@@ -423,14 +702,14 @@ def finish_task(primary: Path, state: Dict[str, object]) -> Dict[str, object]:
|
|
|
423
702
|
if not command:
|
|
424
703
|
raise DriverStop("task has no Verify command", task=task_id)
|
|
425
704
|
if state["mode"] == "serial":
|
|
426
|
-
# ponytail: sidecar reproduction is git plumbing that belongs in isolation.py as the
|
|
427
|
-
# inverse of clean_verify; lift it there when a second caller appears.
|
|
428
705
|
sidecar = state["sidecar"]
|
|
429
|
-
|
|
706
|
+
ensured = isolation.ensure_verify_sidecar(primary, base, str(sidecar["branch"]))
|
|
707
|
+
sidecar_path = Path(str(ensured["worktree"]))
|
|
430
708
|
tree = snapshot_tree(primary)
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
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
|
|
434
713
|
execution = run_verify(command, sidecar_path)
|
|
435
714
|
location = f"sidecar {sidecar['branch']}"
|
|
436
715
|
isolation.clean_verify(primary, sidecar_path, base, str(sidecar["branch"]))
|
|
@@ -453,7 +732,21 @@ def finish_task(primary: Path, state: Dict[str, object]) -> Dict[str, object]:
|
|
|
453
732
|
if not passed:
|
|
454
733
|
if state["mode"] == "parallel":
|
|
455
734
|
isolation.deactivate_task(worktree, task_id, str(state["task_branch"]))
|
|
735
|
+
elif state["mode"] == "member":
|
|
736
|
+
isolation.deactivate_member_task(primary, str(state["member"]), task_id)
|
|
456
737
|
raise DriverStop("Verify failed in the isolate", task=task_id, execution=execution)
|
|
738
|
+
if state["mode"] == "member":
|
|
739
|
+
member = str(state["member"])
|
|
740
|
+
landed = isolation.land_member(primary, member, task_id, str(state["title"]),
|
|
741
|
+
str(state["contract_file"]), base, str(state["member_base"]))
|
|
742
|
+
landing = str(landed["landing"])
|
|
743
|
+
# Only a landing whose parent is the member base proves the verified member tree.
|
|
744
|
+
ledger = isolation.git_output(worktree, "rev-parse", f"{landing}^") == str(state["member_base"])
|
|
745
|
+
if ledger:
|
|
746
|
+
build_state.verify_record(str(primary), command, landing, "pass", execution=execution, member=member)
|
|
747
|
+
isolation.retire_member_task(primary, member, task_id)
|
|
748
|
+
return {"task": task_id, "commit": str(landed["commit"]), "mode": "member",
|
|
749
|
+
"landing": landing, "verify": execution, "ledger": ledger}
|
|
457
750
|
landed = isolation.land(primary, worktree, base, task_id, str(state["title"]), task_file,
|
|
458
751
|
list(state["files"]))
|
|
459
752
|
commit = str(landed["commit"])
|
|
@@ -485,7 +778,8 @@ def recover_report(primary: Path, project_dir: str, receipt: Dict[str, object])
|
|
|
485
778
|
|
|
486
779
|
def checkpoint_project(primary: Path, receipt: Dict[str, object], subject: str, body: str) -> Optional[str]:
|
|
487
780
|
"""Commit .project bookkeeping through the canonical checkpoint; None when nothing is dirty."""
|
|
488
|
-
if not any(path.startswith(".project/") for path in isolation.uncommitted_paths(primary))
|
|
781
|
+
if not any(path.startswith(".project/") for path in isolation.uncommitted_paths(primary)) \
|
|
782
|
+
and not isolation.ignored_legacy_ledger(primary):
|
|
489
783
|
return None
|
|
490
784
|
result = isolation.checkpoint(primary, isolation.current_sha(primary), subject, body, [".project"])
|
|
491
785
|
receipt["steps"].append({"script": "isolation.py checkpoint", "result": result})
|
|
@@ -582,7 +876,7 @@ class Round:
|
|
|
582
876
|
"""Run the bundled token_budget.py against this milestone's ledger; blocked is a stop."""
|
|
583
877
|
command = [sys.executable, "-B", str(Path(__file__).resolve().parent / "token_budget.py"),
|
|
584
878
|
action, "--ledger", str(budget_ledger(self.primary)), *arguments]
|
|
585
|
-
completed = subprocess.run(command, cwd=self.primary, capture_output=True,
|
|
879
|
+
completed = subprocess.run(command, cwd=self.primary, capture_output=True, encoding="utf-8", errors="replace")
|
|
586
880
|
result = json.loads(completed.stdout) if completed.stdout.strip() else {}
|
|
587
881
|
self.receipt["steps"].append({"script": f"token_budget.py {action}", "command": command,
|
|
588
882
|
"exit_code": completed.returncode, "result": result,
|
|
@@ -605,12 +899,44 @@ class Round:
|
|
|
605
899
|
# recovery -------------------------------------------------------------
|
|
606
900
|
|
|
607
901
|
def recover(self) -> None:
|
|
902
|
+
# A journaled member landing is safe to finish; it never lands the same work twice.
|
|
903
|
+
journals = isolation.common_git_dir(self.primary).joinpath(*isolation.MEMBER_LANDING_DIR)
|
|
904
|
+
retired_members = set()
|
|
905
|
+
for journal in sorted(journals.glob("*.json")) if journals.is_dir() else []:
|
|
906
|
+
result = isolation.recover_member_landing(self.primary, journal.stem)
|
|
907
|
+
self.receipt["steps"].append({"script": "isolation.recover_member_landing", "task": journal.stem,
|
|
908
|
+
"result": result})
|
|
909
|
+
if result["state"] == "landed":
|
|
910
|
+
isolation.retire_member_task(self.primary, str(result["member"]), journal.stem)
|
|
911
|
+
retired_members.add(journal.stem)
|
|
608
912
|
report = recover_report(self.primary, self.project_dir, self.receipt)
|
|
609
913
|
for task in report["tasks"]:
|
|
610
914
|
worktree = task.get("worktree")
|
|
611
915
|
verdict = task["verdict"]
|
|
612
916
|
if verdict in ("recovered", "attested"):
|
|
613
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
|
+
)
|
|
938
|
+
if verdict == "recovered" and task.get("member") and task.get("task_id") not in retired_members:
|
|
939
|
+
isolation.retire_member_task(self.primary, str(task["member"]), str(task["task_id"]))
|
|
614
940
|
if verdict == "recovered" and worktree and worktree["clean"] and task.get("task_branch"):
|
|
615
941
|
isolation.retire(self.primary, Path(str(worktree["path"])),
|
|
616
942
|
str(task["task_branch"]), False)
|
|
@@ -623,12 +949,22 @@ class Round:
|
|
|
623
949
|
# settle exited children ---------------------------------------------
|
|
624
950
|
|
|
625
951
|
def isolate_live(self, state: Dict[str, object]) -> bool:
|
|
626
|
-
"""
|
|
952
|
+
"""Keep blocked attempts reportable until the corresponding task is reset."""
|
|
627
953
|
task_path = Path(str(state["worktree"])) / str(state["task_file"])
|
|
628
954
|
if not task_path.is_file():
|
|
629
955
|
return False
|
|
630
956
|
fields, _ = isolation.task_frontmatter(task_path.read_text(encoding="utf-8"))
|
|
631
|
-
|
|
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
|
+
)
|
|
632
968
|
|
|
633
969
|
def settle(self) -> bool:
|
|
634
970
|
"""Classify every exited child. Returns True when a landing or redispatch changed the round."""
|
|
@@ -667,8 +1003,15 @@ class Round:
|
|
|
667
1003
|
elif state.get("finished_at") is None:
|
|
668
1004
|
if child_running(state):
|
|
669
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
|
|
670
1009
|
else:
|
|
671
|
-
|
|
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)
|
|
672
1015
|
else:
|
|
673
1016
|
changed = self.classify(state) or changed
|
|
674
1017
|
return changed
|
|
@@ -679,6 +1022,35 @@ class Round:
|
|
|
679
1022
|
update_state(state, outcome="blocked", reason=reason)
|
|
680
1023
|
self.receipt["blocked"].append({**self.summary(state), "reason": reason, **details})
|
|
681
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
|
+
|
|
682
1054
|
def classify(self, state: Dict[str, object]) -> bool:
|
|
683
1055
|
attempt_dir = Path(str(state["_path"])).parent
|
|
684
1056
|
if self.budgeted and not state.get("usage_recorded"):
|
|
@@ -706,6 +1078,10 @@ class Round:
|
|
|
706
1078
|
update_state(state, outcome="question", question=unanswered[0], answered=False)
|
|
707
1079
|
self.receipt["questions"].append({**self.summary(state), "question": unanswered[0],
|
|
708
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
|
+
)
|
|
709
1085
|
elif verdict != "ready" or named != str(state["task_id"]).lower():
|
|
710
1086
|
self.fail(state, "child returned blocked" if verdict == "blocked"
|
|
711
1087
|
else "child returned no RESULT line", stdout_tail=tail(message),
|
|
@@ -730,6 +1106,8 @@ class Round:
|
|
|
730
1106
|
if state.get("wave") != self.receipt["wave"]:
|
|
731
1107
|
raise DriverStop("task is outside the requested wave", task=state["task_id"],
|
|
732
1108
|
wave=state.get("wave"), requested_wave=self.receipt["wave"])
|
|
1109
|
+
if state.get("mode") == "member":
|
|
1110
|
+
isolation.ensure_member_hooks(self.primary, str(state["member"]), str(state["task_id"]))
|
|
733
1111
|
fresh = spawn(self.root, state, self.options,
|
|
734
1112
|
lambda fresh: brief_text(fresh, self.options.role_brief, self.options.task_template))
|
|
735
1113
|
self.receipt["dispatched"].append(self.summary(fresh))
|
|
@@ -738,8 +1116,14 @@ class Round:
|
|
|
738
1116
|
# dispatch -------------------------------------------------------------
|
|
739
1117
|
|
|
740
1118
|
def in_flight_states(self) -> List[Dict[str, object]]:
|
|
741
|
-
|
|
742
|
-
|
|
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
|
|
743
1127
|
|
|
744
1128
|
def checkpoint_bookkeeping(self) -> None:
|
|
745
1129
|
open_ids = {state["task_id"] for state in latest_states(self.root)
|
|
@@ -763,6 +1147,24 @@ class Round:
|
|
|
763
1147
|
ready = build_state.ready(str(self.primary), self.project_dir)
|
|
764
1148
|
self.receipt["steps"].append({"script": "build_state.py ready", "result": ready})
|
|
765
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
|
|
766
1168
|
if wave is not None and wave < self.receipt["wave"]:
|
|
767
1169
|
raise DriverStop(f"wave {wave} is still unfinished; run round --wave {wave} first")
|
|
768
1170
|
self.checkpoint_bookkeeping()
|
|
@@ -805,6 +1207,28 @@ class Round:
|
|
|
805
1207
|
self.runner("lint-round", head)
|
|
806
1208
|
round_size = len(selected) + len(in_flight)
|
|
807
1209
|
for task in selected:
|
|
1210
|
+
if task.get("repo"):
|
|
1211
|
+
member = str(task["repo"])
|
|
1212
|
+
retained = isolation.retained_member_task(self.primary, member, task["id"],
|
|
1213
|
+
task["task_file"], head)
|
|
1214
|
+
if retained and retained["state"] == "unused":
|
|
1215
|
+
isolation.retire_member_task(self.primary, member, task["id"])
|
|
1216
|
+
retained = None
|
|
1217
|
+
if retained:
|
|
1218
|
+
isolate, active = retained["isolate"], retained["active"]
|
|
1219
|
+
else:
|
|
1220
|
+
isolate = isolation.isolate_member_task(self.primary, member, task["id"])
|
|
1221
|
+
active = isolation.activate_member_task(self.primary, member, task["id"],
|
|
1222
|
+
f"build_{task['id'].lower()}", task["task_file"], head)
|
|
1223
|
+
sidecar_path = Path(str(active["worktree"]))
|
|
1224
|
+
self.launch({"task_id": task["id"], "title": task["title"],
|
|
1225
|
+
"task_file": Path(str(active["copy"])).relative_to(sidecar_path).as_posix(),
|
|
1226
|
+
"contract_file": task["task_file"], "files": task["files"], "wave": task["wave"],
|
|
1227
|
+
"verify_heavy": task["verify_heavy"], "base": head, "worktree": str(sidecar_path),
|
|
1228
|
+
"task_branch": isolate["task_branch"], "mode": "member", "member": member,
|
|
1229
|
+
"member_base": isolate["member_base"], "project_root": str(self.primary),
|
|
1230
|
+
"sidecar": None, "answered": False, "model_selection": task['model_selection']})
|
|
1231
|
+
continue
|
|
808
1232
|
prepared = self.runner("prepare-task", head, task["id"], round_size)
|
|
809
1233
|
isolate = prepared[0]["result"]
|
|
810
1234
|
sidecar = prepared[1]["result"] if len(prepared) > 1 else None
|
|
@@ -1178,7 +1602,7 @@ class Review:
|
|
|
1178
1602
|
def helper_json(receipt: Dict[str, object], script: str, *arguments: str, cwd: Path) -> Dict[str, object]:
|
|
1179
1603
|
"""Run a bundled helper and return its JSON stdout; a non-zero exit is a typed stop."""
|
|
1180
1604
|
command = [sys.executable, "-B", str(Path(__file__).resolve().parent / script), *arguments]
|
|
1181
|
-
completed = subprocess.run(command, cwd=cwd, capture_output=True,
|
|
1605
|
+
completed = subprocess.run(command, cwd=cwd, capture_output=True, encoding="utf-8", errors="replace")
|
|
1182
1606
|
step = {"script": script, "command": command, "exit_code": completed.returncode,
|
|
1183
1607
|
"stdout": completed.stdout, "stderr": completed.stderr}
|
|
1184
1608
|
receipt["steps"].append(step)
|
|
@@ -1612,7 +2036,7 @@ def fix_tasks(primary: Path, options: argparse.Namespace) -> Dict[str, object]:
|
|
|
1612
2036
|
texts = contracts._task_texts(primary, options.project_dir)
|
|
1613
2037
|
groups = {group["locator"]: group for group in findings["groups"]}
|
|
1614
2038
|
today = now()[:10]
|
|
1615
|
-
#
|
|
2039
|
+
# Unordered same-wave file overlap is rejected, so repairs land in a newly appended wave.
|
|
1616
2040
|
plan_path = project / "plan/PLAN.md"
|
|
1617
2041
|
plan_text = plan_path.read_text(encoding="utf-8")
|
|
1618
2042
|
depths, waves = contracts._plan_waves(plan_text)
|
|
@@ -1746,7 +2170,8 @@ Heavy: {'yes' if heavy else 'no'}
|
|
|
1746
2170
|
receipt["plan_wave"] = fix_wave
|
|
1747
2171
|
lint = subprocess.run([sys.executable, "-B", str(Path(__file__).resolve().parent / "check_task_briefs.py"),
|
|
1748
2172
|
"--repo", str(primary), "--base", isolation.current_sha(primary),
|
|
1749
|
-
"--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")
|
|
1750
2175
|
receipt["steps"].append({"script": "check_task_briefs.py", "exit_code": lint.returncode,
|
|
1751
2176
|
"stdout": lint.stdout, "stderr": lint.stderr})
|
|
1752
2177
|
if lint.returncode:
|
|
@@ -1861,7 +2286,11 @@ def positive_int(value: str) -> int:
|
|
|
1861
2286
|
|
|
1862
2287
|
|
|
1863
2288
|
def default_resource(name: str) -> Optional[Path]:
|
|
1864
|
-
|
|
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
|
|
1865
2294
|
return candidate if candidate.is_file() else None
|
|
1866
2295
|
|
|
1867
2296
|
|
|
@@ -1952,6 +2381,7 @@ def main(argv=None) -> int:
|
|
|
1952
2381
|
if arguments.action == "_child":
|
|
1953
2382
|
return child_main(arguments.state)
|
|
1954
2383
|
primary = arguments.repo.resolve()
|
|
2384
|
+
_activate_pinned_runtime(primary)
|
|
1955
2385
|
lock = None
|
|
1956
2386
|
try:
|
|
1957
2387
|
if arguments.action != "status":
|
|
@@ -1981,6 +2411,17 @@ def main(argv=None) -> int:
|
|
|
1981
2411
|
current.recover()
|
|
1982
2412
|
records = [state for state in latest_states(current.root)
|
|
1983
2413
|
if state["task_id"] == arguments.task_id]
|
|
2414
|
+
native = None
|
|
2415
|
+
if records and records[0].get("outcome") is not None:
|
|
2416
|
+
# A settled driver record at an older base is superseded by a native retry.
|
|
2417
|
+
try:
|
|
2418
|
+
native = state_from_task(primary, arguments.task_id)
|
|
2419
|
+
except DriverStop:
|
|
2420
|
+
pass
|
|
2421
|
+
if native and native["base"] != records[0].get("base"):
|
|
2422
|
+
records = []
|
|
2423
|
+
else:
|
|
2424
|
+
native = None
|
|
1984
2425
|
if arguments.task_id in current.proven:
|
|
1985
2426
|
commit = current.proven[arguments.task_id]
|
|
1986
2427
|
if records and records[0].get("outcome") is None:
|
|
@@ -1993,11 +2434,25 @@ def main(argv=None) -> int:
|
|
|
1993
2434
|
raise DriverStop(f"task {arguments.task_id} dispatch record is {record['outcome']}; "
|
|
1994
2435
|
"use round")
|
|
1995
2436
|
if record.get("finished_at") is None:
|
|
1996
|
-
|
|
1997
|
-
|
|
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)
|
|
1998
2453
|
else: # a coder dispatched by hand: derive the isolate from the task frontmatter
|
|
1999
2454
|
current.receipt["landed"].append(
|
|
2000
|
-
finish_task(primary, state_from_task(primary, arguments.task_id)))
|
|
2455
|
+
finish_task(primary, native or state_from_task(primary, arguments.task_id)))
|
|
2001
2456
|
result = {**current.receipt, "status": "landed" if current.receipt["landed"] else "blocked"}
|
|
2002
2457
|
elif arguments.action == "answer":
|
|
2003
2458
|
result = answer(primary, arguments.task_id, arguments.answer)
|