@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
|
@@ -17,12 +17,15 @@ from datetime import date
|
|
|
17
17
|
from pathlib import Path, PurePosixPath
|
|
18
18
|
from typing import Mapping, Optional, Sequence
|
|
19
19
|
|
|
20
|
-
|
|
21
|
-
from isolation import IsolationError, checkpoint as isolation_checkpoint,
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
20
|
+
if __package__:
|
|
21
|
+
from .isolation import IsolationError, checkpoint as isolation_checkpoint, PROJECT_ENTRIES
|
|
22
|
+
else:
|
|
23
|
+
try:
|
|
24
|
+
from isolation import IsolationError, checkpoint as isolation_checkpoint, PROJECT_ENTRIES
|
|
25
|
+
except ModuleNotFoundError as error: # pragma: no cover - package imports used by tests
|
|
26
|
+
if error.name != "isolation":
|
|
27
|
+
raise
|
|
28
|
+
from scripts.isolation import IsolationError, checkpoint as isolation_checkpoint, PROJECT_ENTRIES
|
|
26
29
|
|
|
27
30
|
|
|
28
31
|
if __package__:
|
|
@@ -66,6 +69,11 @@ def _sha256(content: str) -> str:
|
|
|
66
69
|
return hashlib.sha256(content.encode("utf-8")).hexdigest()
|
|
67
70
|
|
|
68
71
|
|
|
72
|
+
def _is_junk_file(path: Path) -> bool:
|
|
73
|
+
# Drift digests skip OS junk by name: Finder rewrites it whatever its ignore status.
|
|
74
|
+
return path.name in pipeline_state._common.OS_JUNK_NAMES and path.is_file() and not path.is_symlink()
|
|
75
|
+
|
|
76
|
+
|
|
69
77
|
def _tree_digest(path: Path, excluded: Sequence[str] = ()) -> str:
|
|
70
78
|
if path.is_symlink() or not path.is_dir():
|
|
71
79
|
raise PipelineStateError(f"promotion path must be a real directory: {path}")
|
|
@@ -74,7 +82,7 @@ def _tree_digest(path: Path, excluded: Sequence[str] = ()) -> str:
|
|
|
74
82
|
if child.is_symlink():
|
|
75
83
|
raise PipelineStateError(f"promotion path contains a symlink: {child}")
|
|
76
84
|
relative = child.relative_to(path).as_posix()
|
|
77
|
-
if PurePosixPath(relative).parts[0] in excluded:
|
|
85
|
+
if PurePosixPath(relative).parts[0] in excluded or _is_junk_file(child):
|
|
78
86
|
continue
|
|
79
87
|
if child.is_dir():
|
|
80
88
|
digest.update(f"d\0{relative}\0".encode())
|
|
@@ -87,15 +95,15 @@ def _tree_digest(path: Path, excluded: Sequence[str] = ()) -> str:
|
|
|
87
95
|
|
|
88
96
|
|
|
89
97
|
def _checkpoint_artifact_digest(project: Path, mutable_paths: set[str]) -> str:
|
|
90
|
-
"""Hash
|
|
98
|
+
"""Hash approval artifacts except helper-owned metadata and OS junk."""
|
|
91
99
|
digest = hashlib.sha256()
|
|
92
100
|
for child in sorted(project.rglob("*")):
|
|
93
101
|
if child.is_symlink():
|
|
94
102
|
raise PipelineStateError(f"checkpoint path contains a symlink: {child}")
|
|
95
103
|
relative = child.relative_to(project).as_posix()
|
|
96
|
-
#
|
|
97
|
-
|
|
98
|
-
|
|
104
|
+
# Finder can rewrite OS junk during approval; it must not cause artifact drift.
|
|
105
|
+
if (relative in mutable_paths or relative.split("/", 1)[0] not in PROJECT_ENTRIES
|
|
106
|
+
or _is_junk_file(child)):
|
|
99
107
|
continue
|
|
100
108
|
if child.is_dir():
|
|
101
109
|
digest.update(f"d\0{relative}\0".encode())
|
|
@@ -333,11 +341,7 @@ def _converge_checkpoint_file(path: Path, before: str, after: str, label: str) -
|
|
|
333
341
|
|
|
334
342
|
def _unlink_checkpoint_journal(path: Path) -> None:
|
|
335
343
|
path.unlink()
|
|
336
|
-
|
|
337
|
-
try:
|
|
338
|
-
os.fsync(descriptor)
|
|
339
|
-
finally:
|
|
340
|
-
os.close(descriptor)
|
|
344
|
+
pipeline_state._common.fsync_directory(path.parent)
|
|
341
345
|
|
|
342
346
|
|
|
343
347
|
def _resume_checkpoint_locked(
|
|
@@ -439,41 +443,85 @@ def _validate_plan_briefs(repo: Path, kind: str, project_dir: str) -> None:
|
|
|
439
443
|
if head.returncode != 0:
|
|
440
444
|
# Pre-Git approval defers base-dependent checks to build.
|
|
441
445
|
return
|
|
446
|
+
if __package__:
|
|
447
|
+
from .check_task_briefs import BriefError, _member_bases, plan_validation_bases, validate_task_briefs
|
|
448
|
+
else:
|
|
449
|
+
try:
|
|
450
|
+
from check_task_briefs import BriefError, _member_bases, plan_validation_bases, validate_task_briefs
|
|
451
|
+
except ModuleNotFoundError as error: # pragma: no cover - package imports
|
|
452
|
+
if error.name != "check_task_briefs":
|
|
453
|
+
raise
|
|
454
|
+
from scripts.check_task_briefs import BriefError, _member_bases, plan_validation_bases, validate_task_briefs
|
|
455
|
+
if __package__:
|
|
456
|
+
from . import check_handoffs
|
|
457
|
+
else:
|
|
458
|
+
try:
|
|
459
|
+
import check_handoffs
|
|
460
|
+
except ModuleNotFoundError as error:
|
|
461
|
+
if error.name != "check_handoffs":
|
|
462
|
+
raise
|
|
463
|
+
from scripts import check_handoffs
|
|
442
464
|
try:
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
from scripts.check_task_briefs import BriefError, validate_task_briefs
|
|
448
|
-
try:
|
|
449
|
-
import check_handoffs
|
|
450
|
-
except ModuleNotFoundError as error:
|
|
451
|
-
if error.name != "check_handoffs":
|
|
452
|
-
raise
|
|
453
|
-
from scripts import check_handoffs
|
|
454
|
-
try:
|
|
455
|
-
tasks, dependency_files = check_handoffs.plan_brief_inputs(repo, project_dir)
|
|
456
|
-
landed_bases = {}
|
|
457
|
-
for task_id, text in tasks.items():
|
|
458
|
-
if check_handoffs._task_scalar(text, task_id, "status") != "done":
|
|
459
|
-
continue
|
|
460
|
-
agent = check_handoffs._task_scalar(text, task_id, "agent")
|
|
461
|
-
if agent in {"", "null"}:
|
|
462
|
-
raise BriefError(f"{task_id} landed task has no recorded agent")
|
|
463
|
-
recorded_base = check_handoffs._task_scalar(text, task_id, "base")
|
|
464
|
-
try:
|
|
465
|
-
landed_bases[task_id] = require_commit(repo, require_full_sha(recorded_base))
|
|
466
|
-
except IsolationError as error:
|
|
467
|
-
raise BriefError(f"{task_id} landed task has invalid historical base: {error}") from error
|
|
468
|
-
dependency_files[task_id] = set()
|
|
465
|
+
member_base = _member_bases(repo)
|
|
466
|
+
tasks, dependency_files, landed_bases = plan_validation_bases(
|
|
467
|
+
repo, project_dir, member_base, initial=True
|
|
468
|
+
)
|
|
469
469
|
validate_task_briefs(
|
|
470
470
|
repo, head.stdout.strip(), f"{project_dir}/tasks",
|
|
471
471
|
dependency_files=dependency_files, landed_bases=landed_bases,
|
|
472
|
+
member_base=member_base,
|
|
472
473
|
)
|
|
474
|
+
if project_dir == ".project/next":
|
|
475
|
+
# Promotion checks lookahead member tasks from the bases their briefs were checked at.
|
|
476
|
+
named = {
|
|
477
|
+
str(check_handoffs._strict_frontmatter(text, task_id).get("repo"))
|
|
478
|
+
for task_id, text in tasks.items()
|
|
479
|
+
if check_handoffs._strict_frontmatter(text, task_id).get("repo")
|
|
480
|
+
and check_handoffs._task_scalar(text, task_id, "status") != "done"
|
|
481
|
+
}
|
|
482
|
+
if named:
|
|
483
|
+
bases = {name: member_base(name)[1] for name in sorted(named)}
|
|
484
|
+
milestone = pipeline_state.load_state(repo, project_dir)[0].milestone
|
|
485
|
+
path = _lookahead_member_bases_path(repo, str(milestone))
|
|
486
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
487
|
+
pipeline_state._common.atomic_write(path, json.dumps(
|
|
488
|
+
{"schema": LOOKAHEAD_MEMBER_BASES_SCHEMA, "milestone": milestone, "bases": bases},
|
|
489
|
+
indent=2, sort_keys=True) + "\n")
|
|
473
490
|
except (BriefError, check_handoffs.HandoffError) as error:
|
|
474
491
|
raise PipelineStateError(f"task brief validation failed: {error}") from error
|
|
475
492
|
|
|
476
493
|
|
|
494
|
+
LOOKAHEAD_MEMBER_BASES_SCHEMA = "gsd-path/lookahead-member-bases/v1"
|
|
495
|
+
|
|
496
|
+
|
|
497
|
+
def _lookahead_member_bases_path(repo: Path, milestone: str) -> Path:
|
|
498
|
+
common = pipeline_state._run_git(repo, "rev-parse", "--path-format=absolute", "--git-common-dir").stdout.strip()
|
|
499
|
+
return Path(common) / "gsd-path" / "lookahead-member-bases" / f"{milestone}.json"
|
|
500
|
+
|
|
501
|
+
|
|
502
|
+
def _lookahead_member_bases(repo: Path, milestone: str) -> Optional[dict]:
|
|
503
|
+
path = _lookahead_member_bases_path(repo, milestone)
|
|
504
|
+
if not path.is_file():
|
|
505
|
+
return None
|
|
506
|
+
value = json.loads(path.read_text(encoding="utf-8"))
|
|
507
|
+
if value.get("schema") != LOOKAHEAD_MEMBER_BASES_SCHEMA or value.get("milestone") != milestone:
|
|
508
|
+
return None
|
|
509
|
+
return value.get("bases")
|
|
510
|
+
|
|
511
|
+
|
|
512
|
+
def _shipped_member_merges(repo: Path, landing: str) -> dict:
|
|
513
|
+
"""Member merges named by the ship commit the integration landing merged."""
|
|
514
|
+
if __package__:
|
|
515
|
+
from .pipeline_git import ship_member_rows
|
|
516
|
+
else:
|
|
517
|
+
from pipeline_git import ship_member_rows
|
|
518
|
+
ship = pipeline_state._run_git(repo, "rev-parse", "--verify", "--quiet", f"{landing}^2", check=False)
|
|
519
|
+
if ship.returncode != 0:
|
|
520
|
+
return {}
|
|
521
|
+
message = pipeline_state._run_git(repo, "show", "-s", "--format=%B", ship.stdout.strip()).stdout
|
|
522
|
+
return {row["name"]: row["merge"] for row in ship_member_rows(message)}
|
|
523
|
+
|
|
524
|
+
|
|
477
525
|
def checkpoint_approval(
|
|
478
526
|
repo: Path,
|
|
479
527
|
kind: str,
|
|
@@ -1027,6 +1075,46 @@ def _classify_plan_drift(
|
|
|
1027
1075
|
}
|
|
1028
1076
|
approved = _approved_task_contracts(repo, checkpoint)
|
|
1029
1077
|
current = _task_contracts_at(repo, revision)
|
|
1078
|
+
if __package__:
|
|
1079
|
+
from . import check_handoffs
|
|
1080
|
+
else:
|
|
1081
|
+
import check_handoffs
|
|
1082
|
+
# Member task paths live in member repos: compare each member from the base its brief
|
|
1083
|
+
# was checked at to the member merge the shipped milestone made (both fixed commits).
|
|
1084
|
+
task_repos = {
|
|
1085
|
+
task_id: str(check_handoffs._strict_frontmatter(contract.text, contract.path).get("repo") or "")
|
|
1086
|
+
for task_id, contract in approved.items()
|
|
1087
|
+
}
|
|
1088
|
+
active_member_tasks = {
|
|
1089
|
+
task_id for task_id, name in task_repos.items()
|
|
1090
|
+
if name and check_handoffs._task_scalar(approved[task_id].text, task_id, "status") != "done"
|
|
1091
|
+
}
|
|
1092
|
+
active_members = {task_repos[task_id] for task_id in active_member_tasks}
|
|
1093
|
+
member_changes: dict[str, list] = {}
|
|
1094
|
+
if active_members:
|
|
1095
|
+
bases = _lookahead_member_bases(repo, str(state.milestone)) or {}
|
|
1096
|
+
if any(name not in bases for name in active_members):
|
|
1097
|
+
return {
|
|
1098
|
+
"class": "unverifiable",
|
|
1099
|
+
"checkpoint": checkpoint,
|
|
1100
|
+
"task_ids": [],
|
|
1101
|
+
"changed_paths": [],
|
|
1102
|
+
"contract_paths": [],
|
|
1103
|
+
"member_tasks": True,
|
|
1104
|
+
"reason": "member bases were not recorded at plan approval",
|
|
1105
|
+
}
|
|
1106
|
+
if __package__:
|
|
1107
|
+
from . import members
|
|
1108
|
+
else:
|
|
1109
|
+
import members
|
|
1110
|
+
checkouts = {item["name"]: Path(item["checkout"]) for item in members.read_members(repo)}
|
|
1111
|
+
merges = _shipped_member_merges(repo, landing or revision)
|
|
1112
|
+
for name in active_members:
|
|
1113
|
+
member_changes[name] = [
|
|
1114
|
+
item for item in pipeline_state._run_git(
|
|
1115
|
+
checkouts[name], "diff", "--no-renames", "--name-only", "-z", bases[name], merges[name]
|
|
1116
|
+
).stdout.split("\0") if item
|
|
1117
|
+
] if name in merges else []
|
|
1030
1118
|
|
|
1031
1119
|
contract_paths: set[str] = set()
|
|
1032
1120
|
flagged_ids: set[str] = set()
|
|
@@ -1050,9 +1138,12 @@ def _classify_plan_drift(
|
|
|
1050
1138
|
|
|
1051
1139
|
product_paths: set[str] = set()
|
|
1052
1140
|
for task_id, contract in approved.items():
|
|
1141
|
+
member = task_repos.get(task_id)
|
|
1142
|
+
if member and task_id not in active_member_tasks:
|
|
1143
|
+
continue
|
|
1053
1144
|
matches = sorted(
|
|
1054
|
-
changed_path
|
|
1055
|
-
for changed_path in changed
|
|
1145
|
+
(f"{member}:{changed_path}" if member else changed_path)
|
|
1146
|
+
for changed_path in (member_changes[member] if member else changed)
|
|
1056
1147
|
if any(
|
|
1057
1148
|
changed_path == path
|
|
1058
1149
|
or changed_path.startswith(f"{path.rstrip('/')}/")
|
|
@@ -82,6 +82,8 @@ def _promotion_event(
|
|
|
82
82
|
if isinstance(task_ids, list) and task_ids:
|
|
83
83
|
return f"{prefix}; plan drift flagged tasks {', '.join(task_ids)}"
|
|
84
84
|
return f"{prefix}; plan contract changed after approval"
|
|
85
|
+
if drift_class == "unverifiable" and drift.get("member_tasks"):
|
|
86
|
+
return f"{prefix}; plan drift unverifiable because member bases were not recorded at plan approval"
|
|
85
87
|
if drift_class == "unverifiable":
|
|
86
88
|
return f"{prefix}; plan drift unverifiable because approval checkpoint is missing"
|
|
87
89
|
return prefix
|
|
@@ -5,27 +5,45 @@ import argparse
|
|
|
5
5
|
import hashlib
|
|
6
6
|
import json
|
|
7
7
|
import os
|
|
8
|
+
import re
|
|
8
9
|
import subprocess
|
|
9
10
|
import sys
|
|
10
11
|
import tempfile
|
|
11
12
|
from pathlib import Path
|
|
12
13
|
|
|
13
14
|
try:
|
|
14
|
-
from scripts import _common
|
|
15
|
+
from scripts import _common, isolation
|
|
15
16
|
except ImportError: # bundled copy inside a skill's scripts directory
|
|
16
17
|
import _common
|
|
18
|
+
import isolation
|
|
17
19
|
|
|
18
20
|
|
|
19
21
|
class StepFailed(RuntimeError):
|
|
20
22
|
pass
|
|
21
23
|
|
|
24
|
+
|
|
25
|
+
def _table_rows(body):
|
|
26
|
+
"""Cells of a Markdown table's data rows (header and separator skipped)."""
|
|
27
|
+
lines = [line.strip() for line in (body or "").splitlines() if line.strip().startswith("|")]
|
|
28
|
+
return [[cell.strip() for cell in re.split(r"(?<!\\)\|", line.strip("|"))] for line in lines[2:]]
|
|
29
|
+
|
|
22
30
|
INSPECTION_SPECS = (("inspect_codebase", "codebase-mapper", "codebase", "evidence-codebase.md"),
|
|
23
31
|
("inspect_docs", "docs-auditor", "docs-audit", "DOCS-AUDIT.md"))
|
|
24
32
|
|
|
25
33
|
|
|
34
|
+
def _task_member(repo: Path, project_dir: str, task_id: str):
|
|
35
|
+
"""The `repo:` member a task names, or None for a coordinator task."""
|
|
36
|
+
for path in sorted((repo / project_dir / "tasks").glob(f"{task_id}-*.md")):
|
|
37
|
+
fields, error = isolation.task_frontmatter(path.read_text(encoding="utf-8"))
|
|
38
|
+
if error:
|
|
39
|
+
raise StepFailed(error)
|
|
40
|
+
return fields.get("repo")
|
|
41
|
+
return None
|
|
42
|
+
|
|
43
|
+
|
|
26
44
|
def run_workflow(repo: Path, action: str, project_dir: str, expected_head: str = None,
|
|
27
45
|
task_id: str = None, round_size: int = None, inspection: Path = None,
|
|
28
|
-
mapper_reviewed: bool = False) -> dict:
|
|
46
|
+
mapper_reviewed: bool = False, kind: str = None) -> dict:
|
|
29
47
|
steps = []
|
|
30
48
|
outputs = {}
|
|
31
49
|
scripts = Path(__file__).resolve().parent
|
|
@@ -33,7 +51,7 @@ def run_workflow(repo: Path, action: str, project_dir: str, expected_head: str =
|
|
|
33
51
|
|
|
34
52
|
def step(script, *arguments, raw=False):
|
|
35
53
|
command = [sys.executable, "-B", str(helpers / script), *arguments]
|
|
36
|
-
completed = subprocess.run(command, cwd=repo, capture_output=True,
|
|
54
|
+
completed = subprocess.run(command, cwd=repo, capture_output=True, encoding="utf-8", errors="replace")
|
|
37
55
|
receipt = {"script": script, "command": command, "exit_code": completed.returncode,
|
|
38
56
|
"stdout": completed.stdout, "stderr": completed.stderr}
|
|
39
57
|
steps.append(receipt)
|
|
@@ -50,12 +68,29 @@ def run_workflow(repo: Path, action: str, project_dir: str, expected_head: str =
|
|
|
50
68
|
return receipt["result"]
|
|
51
69
|
|
|
52
70
|
common = ["--repo", str(repo), "--project-dir", project_dir]
|
|
71
|
+
next_step = "$gsd-path-forensics"
|
|
72
|
+
|
|
73
|
+
def gate_plan():
|
|
74
|
+
if step("discussion_records.py", "pending", "--repo", str(repo))["pending"]:
|
|
75
|
+
raise StepFailed("pending discussion requires its owner disposition")
|
|
76
|
+
step("check_handoffs.py", "plan", *common)
|
|
77
|
+
head = subprocess.run(["git", "rev-parse", "--verify", "HEAD"],
|
|
78
|
+
cwd=repo, capture_output=True, encoding="utf-8", errors="replace", check=True).stdout.strip()
|
|
79
|
+
step("check_task_briefs.py", "--repo", str(repo), "--base", head,
|
|
80
|
+
"--tasks-dir", f"{project_dir}/tasks", "--project-dir", project_dir)
|
|
81
|
+
panel = ["validate-plan", "--plan", str(repo / project_dir / "plan/PLAN.md"),
|
|
82
|
+
"--intent", str(repo / project_dir / "intent/INTENT.md")]
|
|
83
|
+
charter = repo / ".project/CHARTER.md"
|
|
84
|
+
if charter.exists():
|
|
85
|
+
panel += ["--charter", str(charter)]
|
|
86
|
+
return step("review_panel.py", *panel)
|
|
87
|
+
|
|
53
88
|
try:
|
|
54
89
|
if os.path.lexists(repo / ".gsd-path/runtime.json"):
|
|
55
90
|
resolved = subprocess.run(
|
|
56
91
|
[sys.executable, "-B", str(repo / ".gsd-path/status_runtime.py"),
|
|
57
92
|
"--repo", str(repo), "--runtime-path"],
|
|
58
|
-
cwd=repo, capture_output=True,
|
|
93
|
+
cwd=repo, capture_output=True, encoding="utf-8", errors="replace")
|
|
59
94
|
if resolved.returncode:
|
|
60
95
|
sys.stderr.write(resolved.stderr)
|
|
61
96
|
raise StepFailed(resolved.stderr.strip())
|
|
@@ -71,7 +106,7 @@ def run_workflow(repo: Path, action: str, project_dir: str, expected_head: str =
|
|
|
71
106
|
(repo / project_dir / "research" / spec[3]).is_symlink() for spec in specs):
|
|
72
107
|
raise StepFailed("prior inspection evidence requires the re-inspection contract")
|
|
73
108
|
branch = subprocess.run(["git", "branch", "--show-current"], cwd=repo,
|
|
74
|
-
capture_output=True,
|
|
109
|
+
capture_output=True, encoding="utf-8", errors="replace", check=True).stdout.strip()
|
|
75
110
|
if branch != state["branch"]:
|
|
76
111
|
raise StepFailed("current branch differs from the recorded inspection branch")
|
|
77
112
|
pending = step("discussion_records.py", "pending", "--repo", str(repo))
|
|
@@ -87,7 +122,7 @@ def run_workflow(repo: Path, action: str, project_dir: str, expected_head: str =
|
|
|
87
122
|
raise StepFailed(f"missing inspection resource: {path}")
|
|
88
123
|
staging = Path(tempfile.mkdtemp(prefix="gsd-path-inspect-"))
|
|
89
124
|
inventory_file = staging / "inventory.txt"
|
|
90
|
-
inventory_file.
|
|
125
|
+
inventory_file.write_bytes(inventory.encode("utf-8"))
|
|
91
126
|
assignments = []
|
|
92
127
|
for task_name, role, template, filename in specs:
|
|
93
128
|
isolated = step("isolation.py", "isolate-verify", "--repo", str(repo),
|
|
@@ -100,13 +135,14 @@ def run_workflow(repo: Path, action: str, project_dir: str, expected_head: str =
|
|
|
100
135
|
f"Template: {template_path}\nRepository and command cwd: {isolated['worktree']}\n"
|
|
101
136
|
f"Recorded baseline and Audited HEAD: {isolated['base']}\n"
|
|
102
137
|
f"Write only: {Path(isolated['worktree']) / output}\n"
|
|
138
|
+
f"Write header Repo root: {repo}\n"
|
|
103
139
|
"Use only this supplied sidecar for project commands; do not create worktrees.\n"
|
|
104
140
|
"The parent owns gates, collection, retirement, and phase state.\n"
|
|
105
141
|
"Exclude .git, node_modules, .project/archive, and installed GSD Path and path skill bundles.\n")
|
|
106
142
|
if task_name == "inspect_docs":
|
|
107
143
|
brief += ("Alignment mode: false. No prior audit or changed set.\n"
|
|
108
144
|
"Frozen inventory follows; use it exactly, without rediscovery:\n" + inventory)
|
|
109
|
-
brief_file.
|
|
145
|
+
brief_file.write_bytes(brief.encode("utf-8"))
|
|
110
146
|
assignments.append({**isolated, "task_name": task_name, "role": str(role_path),
|
|
111
147
|
"template": str(template_path), "output": output,
|
|
112
148
|
"brief_file": str(brief_file)})
|
|
@@ -136,16 +172,16 @@ def run_workflow(repo: Path, action: str, project_dir: str, expected_head: str =
|
|
|
136
172
|
if state["phase"] != "inspect" or state["status"] != "active":
|
|
137
173
|
raise StepFailed("inspection completion requires inspect/active")
|
|
138
174
|
branch = subprocess.run(["git", "branch", "--show-current"], cwd=repo,
|
|
139
|
-
capture_output=True,
|
|
175
|
+
capture_output=True, encoding="utf-8", errors="replace", check=True).stdout.strip()
|
|
140
176
|
if state["branch"] != branch:
|
|
141
177
|
raise StepFailed("current branch differs from the recorded inspection branch")
|
|
142
178
|
pending = step("discussion_records.py", "pending", "--repo", str(repo))
|
|
143
179
|
if pending["pending"]:
|
|
144
180
|
raise StepFailed("pending discussion requires its owner disposition")
|
|
145
181
|
docs = assignments["inspect_docs"]
|
|
146
|
-
step("check_docs_audit.py", "--repo", docs["worktree"], "--
|
|
147
|
-
"--inventory", str(inventory_file))
|
|
148
|
-
if f"Audited HEAD: {expected_head}" not in (Path(docs["worktree"]) / docs["output"]).read_text().splitlines():
|
|
182
|
+
step("check_docs_audit.py", "--repo", docs["worktree"], "--primary", str(repo),
|
|
183
|
+
"--audit", docs["output"], "--inventory", str(inventory_file))
|
|
184
|
+
if f"Audited HEAD: {expected_head}" not in (Path(docs["worktree"]) / docs["output"]).read_text(encoding="utf-8").splitlines():
|
|
149
185
|
raise StepFailed("audit does not name the supplied inspection baseline")
|
|
150
186
|
for task_name, _, _, _ in INSPECTION_SPECS:
|
|
151
187
|
assignment = assignments[task_name]
|
|
@@ -161,10 +197,14 @@ def run_workflow(repo: Path, action: str, project_dir: str, expected_head: str =
|
|
|
161
197
|
"--set-phase", "inspect", "--set-status", "done", "--event", "inspection artifacts passed")
|
|
162
198
|
elif action == "lint-round":
|
|
163
199
|
head = subprocess.run(["git", "rev-parse", "--verify", "HEAD"],
|
|
164
|
-
cwd=repo, capture_output=True,
|
|
200
|
+
cwd=repo, capture_output=True, encoding="utf-8", errors="replace", check=True).stdout.strip()
|
|
165
201
|
step("check_task_briefs.py", "--repo", str(repo), "--base", head,
|
|
166
|
-
"--tasks-dir", f"{project_dir}/tasks")
|
|
202
|
+
"--tasks-dir", f"{project_dir}/tasks", "--project-dir", project_dir)
|
|
167
203
|
step("check_handoffs.py", "plan", *common)
|
|
204
|
+
elif action == "prepare-task" and _task_member(repo, project_dir, task_id):
|
|
205
|
+
# A member task works in a sidecar of its member; its Verify runs there at finish.
|
|
206
|
+
step("isolation.py", "isolate-member-task", "--repo", str(repo),
|
|
207
|
+
"--member", _task_member(repo, project_dir, task_id), "--task-id", task_id)
|
|
168
208
|
elif action == "prepare-task":
|
|
169
209
|
task = step("isolation.py", "isolate-task", "--repo", str(repo),
|
|
170
210
|
"--base", expected_head, "--task-id", task_id,
|
|
@@ -186,33 +226,67 @@ def run_workflow(repo: Path, action: str, project_dir: str, expected_head: str =
|
|
|
186
226
|
raise StepFailed("pending discussion requires its owner disposition")
|
|
187
227
|
step("build_state.py", "verify-landed", *common, "--head", expected_head)
|
|
188
228
|
step("lean_verification.py", "--repo", str(repo), "--expected-head", expected_head)
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
229
|
+
elif action == "preauthorize":
|
|
230
|
+
# A block here is not a failure: the gate asks its owner as usual.
|
|
231
|
+
next_step = "ask the owner at this gate"
|
|
232
|
+
if project_dir != ".project" or kind not in {"intent", "plan"}:
|
|
233
|
+
raise StepFailed("pre-approval covers only intent or plan on the active track")
|
|
234
|
+
routed = step("pipeline_state.py", "route", *common)
|
|
235
|
+
if routed["route"].get("action") != "run-phase" or "recovery" in routed:
|
|
236
|
+
raise StepFailed("pre-approval needs an ordinary phase route")
|
|
237
|
+
if step("discussion_records.py", "pending", "--repo", str(repo))["pending"]:
|
|
192
238
|
raise StepFailed("pending discussion requires its owner disposition")
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
239
|
+
track = repo / project_dir
|
|
240
|
+
intent = (track / "intent/INTENT.md").read_text(encoding="utf-8")
|
|
241
|
+
lane = re.search(r"(?m)^Lane:\s*(.+)$", intent)
|
|
242
|
+
if not lane or lane.group(1).split("<!--", 1)[0].strip().strip("`") != "quick":
|
|
243
|
+
raise StepFailed("pre-approval covers only the quick lane")
|
|
244
|
+
intent = re.sub(r"(?s)<!--.*?-->", "", intent)
|
|
245
|
+
if re.search(r"(?m)^\s*-\s*\[(RESEARCH|NEEDS-USER)\]", _common.section_body(intent, "Open questions") or ""):
|
|
246
|
+
raise StepFailed("INTENT.md has open RESEARCH or NEEDS-USER questions")
|
|
247
|
+
audit_path = track / "research/DOCS-AUDIT.md"
|
|
248
|
+
if audit_path.exists():
|
|
249
|
+
audit = audit_path.read_text(encoding="utf-8")
|
|
250
|
+
queue = _table_rows(_common.section_body(audit, "Remediation queue"))
|
|
251
|
+
ruled = set()
|
|
252
|
+
for row in _table_rows(_common.section_body(audit, "User rulings")):
|
|
253
|
+
if len(row) != 4 or row[1] not in {"fix-code", "fix-doc", "accept-drift"}:
|
|
254
|
+
continue
|
|
255
|
+
words = row[2].strip().strip('`"').strip()
|
|
256
|
+
if words and words.casefold() != "none" and not re.fullmatch(r"<[^>]*>", words):
|
|
257
|
+
ruled.add(row[0])
|
|
258
|
+
if any(len(row) == 6 and row[4] == "NEEDS-USER" and row[0] not in ruled for row in queue):
|
|
259
|
+
raise StepFailed("DOCS-AUDIT.md has NEEDS-USER rows without a user ruling")
|
|
260
|
+
if kind == "intent":
|
|
261
|
+
# Malformed or incomplete spec-probe tables are not a pre-approvable draft.
|
|
262
|
+
step("check_handoffs.py", "intent", *common)
|
|
263
|
+
if kind == "plan":
|
|
264
|
+
if gate_plan()["mode"] != "off":
|
|
265
|
+
raise StepFailed("pre-approval requires review_panel off")
|
|
266
|
+
plan = re.sub(r"(?s)<!--.*?-->", "", (track / "plan/PLAN.md").read_text(encoding="utf-8"))
|
|
267
|
+
config = _common.section_body(plan, "Config") or ""
|
|
268
|
+
skeptics = re.search(r"(?m)^[ \t]*-[ \t]*finding_skeptics:[ \t]*(\S+)", config)
|
|
269
|
+
if skeptics and skeptics.group(1) != "off":
|
|
270
|
+
raise StepFailed("pre-approval requires finding_skeptics off")
|
|
271
|
+
# Quick-lane limit from project policy (gsd-path-plan Quick mode): one wave, at most two tasks.
|
|
272
|
+
if (len(re.findall(r"(?m)^## Wave \d+ — ", plan)) != 1
|
|
273
|
+
or len(list((track / "tasks").glob("T[0-9][0-9][0-9]-*.md"))) > 2):
|
|
274
|
+
raise StepFailed("pre-approval requires one wave and at most two tasks")
|
|
275
|
+
step("pipeline_state.py", "pre-approve", "--repo", str(repo), "--use", kind)
|
|
276
|
+
else:
|
|
277
|
+
gate_plan()
|
|
204
278
|
if action == "approve-plan":
|
|
205
279
|
step("pipeline_state.py", "approve", *common, "--kind", "plan",
|
|
206
280
|
"--expected-head", expected_head)
|
|
207
281
|
except (StepFailed, OSError, subprocess.CalledProcessError, ValueError, KeyError, TypeError) as error:
|
|
208
282
|
return {"status": "blocked", "reason": str(error), "steps": steps,
|
|
209
|
-
"next":
|
|
283
|
+
"next": next_step}
|
|
210
284
|
return {"status": "complete", "steps": steps, **outputs}
|
|
211
285
|
|
|
212
286
|
|
|
213
287
|
def main(argv=None):
|
|
214
288
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
215
|
-
parser.add_argument("action", choices=("route", "gate-plan", "approve-plan", "build-evidence", "lint-round", "prepare-task", "prepare-final", "prepare-inspect", "finish-inspect"))
|
|
289
|
+
parser.add_argument("action", choices=("route", "gate-plan", "approve-plan", "build-evidence", "lint-round", "prepare-task", "prepare-final", "prepare-inspect", "finish-inspect", "preauthorize"))
|
|
216
290
|
parser.add_argument("--repo", type=Path, required=True)
|
|
217
291
|
parser.add_argument("--project-dir", choices=(".project", ".project/next"), default=".project")
|
|
218
292
|
parser.add_argument("--expected-head")
|
|
@@ -220,15 +294,18 @@ def main(argv=None):
|
|
|
220
294
|
parser.add_argument("--round-size", type=int)
|
|
221
295
|
parser.add_argument("--inspection", type=Path)
|
|
222
296
|
parser.add_argument("--mapper-reviewed", action="store_true")
|
|
297
|
+
parser.add_argument("--kind", choices=("intent", "plan"))
|
|
223
298
|
arguments = parser.parse_args(argv)
|
|
224
299
|
if arguments.action in {"approve-plan", "build-evidence", "prepare-task", "prepare-final", "prepare-inspect", "finish-inspect"} and not arguments.expected_head:
|
|
225
300
|
parser.error(f"{arguments.action} requires --expected-head")
|
|
226
301
|
if arguments.action == "prepare-task" and (not arguments.task_id or arguments.round_size is None):
|
|
227
302
|
parser.error("prepare-task requires --task-id and --round-size")
|
|
303
|
+
if arguments.action == "preauthorize" and not arguments.kind:
|
|
304
|
+
parser.error("preauthorize requires --kind")
|
|
228
305
|
result = run_workflow(arguments.repo.resolve(), arguments.action,
|
|
229
306
|
arguments.project_dir, arguments.expected_head,
|
|
230
307
|
arguments.task_id, arguments.round_size, arguments.inspection,
|
|
231
|
-
arguments.mapper_reviewed)
|
|
308
|
+
arguments.mapper_reviewed, arguments.kind)
|
|
232
309
|
print(json.dumps(result, sort_keys=True))
|
|
233
310
|
return 0 if result["status"] == "complete" else 1
|
|
234
311
|
|
|
@@ -13,10 +13,34 @@ import os
|
|
|
13
13
|
import tempfile
|
|
14
14
|
from pathlib import Path
|
|
15
15
|
|
|
16
|
-
|
|
17
|
-
import _common
|
|
18
|
-
|
|
19
|
-
|
|
16
|
+
if __package__:
|
|
17
|
+
from . import _common
|
|
18
|
+
else:
|
|
19
|
+
try:
|
|
20
|
+
import _common
|
|
21
|
+
except ImportError:
|
|
22
|
+
from scripts import _common
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _worktree_list_porcelain(primary: Path) -> tuple[str, bool]:
|
|
26
|
+
try:
|
|
27
|
+
if __package__:
|
|
28
|
+
from . import pipeline_git as pg
|
|
29
|
+
else:
|
|
30
|
+
from scripts import pipeline_git as pg
|
|
31
|
+
except ImportError:
|
|
32
|
+
try:
|
|
33
|
+
import pipeline_git as pg
|
|
34
|
+
except ImportError:
|
|
35
|
+
pg = None
|
|
36
|
+
if pg is not None:
|
|
37
|
+
return pg.worktree_list_porcelain(primary)
|
|
38
|
+
try:
|
|
39
|
+
output = _git(primary, "worktree", "list", "--porcelain", "-z")
|
|
40
|
+
return output, True
|
|
41
|
+
except ValueError:
|
|
42
|
+
output = _git(primary, "worktree", "list", "--porcelain")
|
|
43
|
+
return output, False
|
|
20
44
|
|
|
21
45
|
|
|
22
46
|
def _git(primary: Path, *args: str) -> str:
|
|
@@ -27,7 +51,10 @@ def _git(primary: Path, *args: str) -> str:
|
|
|
27
51
|
|
|
28
52
|
|
|
29
53
|
def _identity(path: Path) -> str:
|
|
30
|
-
|
|
54
|
+
digest = hashlib.sha256(os.fsencode(path)).hexdigest()
|
|
55
|
+
# Two identities nest in every managed path; full digests would use about
|
|
56
|
+
# 130 of Windows' 260-character MAX_PATH before any repository content.
|
|
57
|
+
return digest[:16] if os.name == "nt" else digest
|
|
31
58
|
|
|
32
59
|
|
|
33
60
|
def _workspace(primary: Path, pin: bool) -> Path:
|
|
@@ -65,7 +92,7 @@ def _workspace(primary: Path, pin: bool) -> Path:
|
|
|
65
92
|
receipt.parent.mkdir(parents=True, exist_ok=True)
|
|
66
93
|
fd, temporary = tempfile.mkstemp(dir=receipt.parent)
|
|
67
94
|
try:
|
|
68
|
-
with os.fdopen(fd, "w", encoding="utf-8") as handle:
|
|
95
|
+
with os.fdopen(fd, "w", encoding="utf-8", newline="\n") as handle:
|
|
69
96
|
json.dump({"primary": str(primary), "root": str(workspace)}, handle)
|
|
70
97
|
try:
|
|
71
98
|
os.link(temporary, receipt)
|
|
@@ -76,17 +103,52 @@ def _workspace(primary: Path, pin: bool) -> Path:
|
|
|
76
103
|
return workspace
|
|
77
104
|
|
|
78
105
|
|
|
106
|
+
def release_workspace(primary: Path) -> bool:
|
|
107
|
+
"""Forget the pinned placement receipt once no linked worktrees remain.
|
|
108
|
+
|
|
109
|
+
Lets GSD_PATH_WORKTREE_ROOT (or a new default) apply again at the next
|
|
110
|
+
milestone's first allocation. Returns True when a receipt was removed;
|
|
111
|
+
any remaining linked worktree (including one only still registered) keeps
|
|
112
|
+
the pin.
|
|
113
|
+
"""
|
|
114
|
+
primary = primary.resolve()
|
|
115
|
+
common = Path(_git(primary, "rev-parse", "--git-common-dir"))
|
|
116
|
+
common = (primary / common).resolve()
|
|
117
|
+
receipt = common / "gsd-path" / "workspaces" / f"{_identity(primary)}.json"
|
|
118
|
+
if not receipt.is_file():
|
|
119
|
+
return False
|
|
120
|
+
output, nul_separated = _worktree_list_porcelain(primary)
|
|
121
|
+
records = output.split("\0\0") if nul_separated else [
|
|
122
|
+
block for block in output.strip().split("\n\n") if block.strip()
|
|
123
|
+
]
|
|
124
|
+
for record in records:
|
|
125
|
+
fields = record.split("\0") if nul_separated else record.splitlines()
|
|
126
|
+
for field in fields:
|
|
127
|
+
if field.startswith("worktree ") and Path(field[len("worktree "):]).resolve() != primary:
|
|
128
|
+
return False
|
|
129
|
+
receipt.unlink()
|
|
130
|
+
return True
|
|
131
|
+
|
|
132
|
+
|
|
79
133
|
def worktree_path(primary: Path, kind: str, name: str, *, pin: bool = False) -> Path:
|
|
80
134
|
primary = primary.resolve()
|
|
81
|
-
if kind not in {"task", "verify", "integrate"} or not name or Path(name).name != name or name in {".", ".."}:
|
|
135
|
+
if kind not in {"task", "verify", "integrate", "bound"} or not name or Path(name).name != name or name in {".", ".."}:
|
|
82
136
|
raise ValueError("invalid managed worktree kind or name")
|
|
137
|
+
if kind == "bound":
|
|
138
|
+
return _workspace(primary, pin) / kind / name
|
|
83
139
|
legacy = (primary.parent / f".{primary.name}-gsd-path-integrate-{name}"
|
|
84
140
|
if kind == "integrate" else primary.parent / f"{primary.name}.gsd-path" / kind / name)
|
|
85
141
|
branch = f"refs/heads/gsd-path-{kind}/{name}"
|
|
86
|
-
|
|
142
|
+
output, nul_separated = _worktree_list_porcelain(primary)
|
|
143
|
+
if nul_separated:
|
|
144
|
+
records = output.split("\0\0")
|
|
145
|
+
else:
|
|
146
|
+
records = [block for block in output.strip().split("\n\n") if block.strip()]
|
|
87
147
|
for record in records:
|
|
88
|
-
fields = record.split("\0")
|
|
89
|
-
|
|
148
|
+
fields = record.split("\0") if nul_separated else record.splitlines()
|
|
149
|
+
# Compare as paths: git prints forward slashes on Windows.
|
|
150
|
+
worktrees = [Path(field[len("worktree "):]) for field in fields if field.startswith("worktree ")]
|
|
151
|
+
if legacy in worktrees and f"branch {branch}" in fields:
|
|
90
152
|
return legacy
|
|
91
153
|
# Keep collision checks and missing-directory recovery at the old location.
|
|
92
154
|
if os.path.lexists(legacy):
|