@opengsd/gsd-path 1.1.0 → 1.3.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 +5 -2
- package/CHANGELOG.md +87 -0
- package/DOCS.md +73 -1
- package/FULL.md +3 -2
- package/HOOKS.md +18 -9
- package/QUICK.md +3 -4
- package/README.md +72 -5
- package/RUNTIME.md +9 -4
- package/UPDATE.md +56 -11
- package/WORKFLOW.md +5 -2
- package/package.json +9 -1
- package/platforms/claude/dispatch.md +4 -4
- package/platforms/copilot/dispatch.md +4 -4
- package/platforms/cursor/dispatch.md +5 -10
- package/platforms/grok/dispatch.md +4 -6
- package/platforms/kimi/dispatch.md +4 -4
- package/platforms/kiro/dispatch.md +4 -6
- package/platforms/opencode/dispatch.md +4 -4
- package/platforms/qwen/dispatch.md +4 -4
- package/platforms/shared-agents/dispatch.md +6 -19
- package/scripts/_common.py +5 -0
- package/scripts/archive_milestone.py +20 -6
- package/scripts/bootstrap_repository.py +8 -1
- package/scripts/build_recovery.py +5 -0
- package/scripts/build_state.py +2 -0
- package/scripts/check_docs_audit.py +2 -0
- package/scripts/check_handoffs.py +6 -0
- package/scripts/check_task_briefs.py +3 -0
- package/scripts/detect_project.py +11 -1
- package/scripts/discussion_records.py +3 -0
- package/scripts/discussion_validate.py +8 -3
- package/scripts/dispatch_driver.py +230 -21
- package/scripts/git_guard.py +12 -4
- package/scripts/guard_hook.py +11 -1
- package/scripts/install.mjs +74 -1448
- package/scripts/install.py +165 -201
- package/scripts/integration.py +28 -8
- package/scripts/isolation.py +15 -5
- package/scripts/jev_review.py +138 -0
- package/scripts/lean_verification.py +2 -0
- package/scripts/loop_run.py +2 -0
- package/scripts/model_policy.py +279 -0
- package/scripts/path_config.py +248 -0
- package/scripts/pipeline_diagnose.py +3 -0
- package/scripts/pipeline_git.py +3 -0
- package/scripts/pipeline_state.py +4 -1
- package/scripts/pipeline_undo.py +3 -0
- package/scripts/promote_lookahead.py +3 -0
- package/scripts/review_panel.py +11 -0
- package/scripts/roadmap.py +5 -0
- package/scripts/runtime_store.py +281 -0
- package/scripts/skill-resources.json +104 -4
- package/scripts/state_checkpoint.py +5 -0
- package/scripts/state_promote.py +5 -0
- package/scripts/status_runtime.py +92 -6
- package/scripts/wizard.mjs +8 -2
- package/scripts/workflow_run.py +12 -1
- package/scripts/worktree_paths.py +94 -0
- package/skills/gsd-path/BUILD.md +7 -0
- package/skills/gsd-path/DECIDE.md +2 -0
- package/skills/gsd-path/DEFINE.md +13 -7
- package/skills/gsd-path/DOCS-AUDIT.md +2 -0
- package/skills/gsd-path/INSPECT.md +2 -0
- package/skills/gsd-path/PLAN.md +4 -2
- package/skills/gsd-path/RESEARCH.md +2 -0
- package/skills/gsd-path/ROADMAP.md +2 -0
- package/skills/gsd-path/SHIP.md +24 -13
- package/skills/gsd-path/SKILL.md +23 -9
- package/skills/gsd-path/references/build-native.md +15 -7
- package/skills/gsd-path/references/config.md +57 -0
- package/skills/gsd-path/references/dispatch.md +6 -19
- package/skills/gsd-path/references/jev-review.md +82 -0
- package/skills/gsd-path/references/model-policy.md +139 -0
- package/skills/gsd-path/references/reviewer-panel.md +1 -1
- package/skills/gsd-path/references/reviewer.md +2 -0
- package/skills/gsd-path/references/runtime-selection.md +25 -0
- 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/scripts/_common.py +5 -0
- package/skills/gsd-path/scripts/archive_milestone.py +20 -6
- package/skills/gsd-path/scripts/bootstrap_repository.py +8 -1
- package/skills/gsd-path/scripts/build_recovery.py +5 -0
- package/skills/gsd-path/scripts/build_state.py +2 -0
- package/skills/gsd-path/scripts/check_docs_audit.py +2 -0
- package/skills/gsd-path/scripts/check_handoffs.py +6 -0
- package/skills/gsd-path/scripts/check_task_briefs.py +3 -0
- package/skills/gsd-path/scripts/detect_project.py +11 -1
- package/skills/gsd-path/scripts/discussion_records.py +3 -0
- package/skills/gsd-path/scripts/discussion_validate.py +8 -3
- package/skills/gsd-path/scripts/dispatch_driver.py +230 -21
- package/skills/gsd-path/scripts/git_guard.py +768 -0
- package/skills/gsd-path/scripts/integration.py +28 -8
- package/skills/gsd-path/scripts/isolation.py +15 -5
- package/skills/gsd-path/scripts/jev_review.py +138 -0
- package/skills/gsd-path/scripts/lean_verification.py +2 -0
- package/skills/gsd-path/scripts/loop_run.py +2 -0
- package/skills/gsd-path/scripts/model_policy.py +279 -0
- package/skills/gsd-path/scripts/path_config.py +248 -0
- package/skills/gsd-path/scripts/pipeline_git.py +3 -0
- package/skills/gsd-path/scripts/pipeline_state.py +4 -1
- package/skills/gsd-path/scripts/promote_lookahead.py +3 -0
- package/skills/gsd-path/scripts/review_panel.py +11 -0
- package/skills/gsd-path/scripts/roadmap.py +5 -0
- package/skills/gsd-path/scripts/state_checkpoint.py +5 -0
- package/skills/gsd-path/scripts/state_promote.py +5 -0
- package/skills/gsd-path/scripts/status_runtime.py +208 -0
- package/skills/gsd-path/scripts/workflow_run.py +12 -1
- package/skills/gsd-path/scripts/worktree_paths.py +94 -0
- package/skills/gsd-path/templates/skeptic.md +1 -1
- package/skills/gsd-path/templates/task.md +2 -0
- package/skills/gsd-path/templates/wave-panel.md +1 -1
- package/skills/gsd-path-build/SKILL.md +7 -0
- package/skills/gsd-path-build/references/build-native.md +15 -7
- package/skills/gsd-path-build/references/dispatch.md +6 -19
- package/skills/gsd-path-build/references/jev-review.md +82 -0
- package/skills/gsd-path-build/references/model-policy.md +139 -0
- package/skills/gsd-path-build/references/reviewer-panel.md +1 -1
- package/skills/gsd-path-build/references/reviewer.md +2 -0
- package/skills/gsd-path-build/references/runtime-selection.md +25 -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-build/scripts/_common.py +5 -0
- package/skills/gsd-path-build/scripts/archive_milestone.py +20 -6
- package/skills/gsd-path-build/scripts/build_recovery.py +5 -0
- package/skills/gsd-path-build/scripts/build_state.py +2 -0
- package/skills/gsd-path-build/scripts/check_handoffs.py +6 -0
- package/skills/gsd-path-build/scripts/check_task_briefs.py +3 -0
- package/skills/gsd-path-build/scripts/detect_project.py +11 -1
- package/skills/gsd-path-build/scripts/discussion_records.py +3 -0
- package/skills/gsd-path-build/scripts/discussion_validate.py +8 -3
- package/skills/gsd-path-build/scripts/dispatch_driver.py +230 -21
- package/skills/gsd-path-build/scripts/integration.py +28 -8
- package/skills/gsd-path-build/scripts/isolation.py +15 -5
- package/skills/gsd-path-build/scripts/jev_review.py +138 -0
- package/skills/gsd-path-build/scripts/loop_run.py +2 -0
- package/skills/gsd-path-build/scripts/model_policy.py +279 -0
- package/skills/gsd-path-build/scripts/path_config.py +248 -0
- package/skills/gsd-path-build/scripts/pipeline_git.py +3 -0
- package/skills/gsd-path-build/scripts/pipeline_state.py +4 -1
- package/skills/gsd-path-build/scripts/promote_lookahead.py +3 -0
- package/skills/gsd-path-build/scripts/review_panel.py +11 -0
- package/skills/gsd-path-build/scripts/roadmap.py +5 -0
- package/skills/gsd-path-build/scripts/state_checkpoint.py +5 -0
- package/skills/gsd-path-build/scripts/state_promote.py +5 -0
- package/skills/gsd-path-build/scripts/workflow_run.py +12 -1
- package/skills/gsd-path-build/scripts/worktree_paths.py +94 -0
- package/skills/gsd-path-build/templates/skeptic.md +1 -1
- package/skills/gsd-path-build/templates/task.md +2 -0
- package/skills/gsd-path-build/templates/wave-panel.md +1 -1
- package/skills/gsd-path-decide/SKILL.md +2 -0
- package/skills/gsd-path-decide/references/dispatch.md +6 -19
- package/skills/gsd-path-decide/references/model-policy.md +139 -0
- package/skills/gsd-path-decide/references/runtime-selection.md +25 -0
- package/skills/gsd-path-decide/scripts/_common.py +5 -0
- package/skills/gsd-path-decide/scripts/archive_milestone.py +20 -6
- package/skills/gsd-path-decide/scripts/build_recovery.py +5 -0
- package/skills/gsd-path-decide/scripts/check_handoffs.py +6 -0
- package/skills/gsd-path-decide/scripts/check_task_briefs.py +3 -0
- package/skills/gsd-path-decide/scripts/detect_project.py +11 -1
- package/skills/gsd-path-decide/scripts/discussion_records.py +3 -0
- package/skills/gsd-path-decide/scripts/discussion_validate.py +8 -3
- package/skills/gsd-path-decide/scripts/integration.py +28 -8
- package/skills/gsd-path-decide/scripts/isolation.py +15 -5
- package/skills/gsd-path-decide/scripts/loop_run.py +839 -0
- package/skills/gsd-path-decide/scripts/model_policy.py +279 -0
- package/skills/gsd-path-decide/scripts/path_config.py +248 -0
- package/skills/gsd-path-decide/scripts/pipeline_git.py +3 -0
- package/skills/gsd-path-decide/scripts/pipeline_state.py +4 -1
- package/skills/gsd-path-decide/scripts/promote_lookahead.py +3 -0
- package/skills/gsd-path-decide/scripts/review_panel.py +11 -0
- package/skills/gsd-path-decide/scripts/roadmap.py +5 -0
- package/skills/gsd-path-decide/scripts/state_checkpoint.py +5 -0
- package/skills/gsd-path-decide/scripts/state_promote.py +5 -0
- package/skills/gsd-path-decide/scripts/worktree_paths.py +94 -0
- package/skills/gsd-path-define/SKILL.md +13 -7
- package/skills/gsd-path-define/references/runtime-selection.md +25 -0
- package/skills/gsd-path-define/scripts/_common.py +5 -0
- package/skills/gsd-path-define/scripts/archive_milestone.py +20 -6
- package/skills/gsd-path-define/scripts/build_recovery.py +5 -0
- package/skills/gsd-path-define/scripts/check_handoffs.py +6 -0
- package/skills/gsd-path-define/scripts/check_task_briefs.py +3 -0
- package/skills/gsd-path-define/scripts/detect_project.py +11 -1
- package/skills/gsd-path-define/scripts/discussion_records.py +3 -0
- package/skills/gsd-path-define/scripts/discussion_validate.py +8 -3
- package/skills/gsd-path-define/scripts/integration.py +28 -8
- package/skills/gsd-path-define/scripts/isolation.py +15 -5
- package/skills/gsd-path-define/scripts/loop_run.py +839 -0
- package/skills/gsd-path-define/scripts/model_policy.py +279 -0
- package/skills/gsd-path-define/scripts/path_config.py +248 -0
- package/skills/gsd-path-define/scripts/pipeline_git.py +3 -0
- package/skills/gsd-path-define/scripts/pipeline_state.py +4 -1
- package/skills/gsd-path-define/scripts/promote_lookahead.py +3 -0
- package/skills/gsd-path-define/scripts/review_panel.py +11 -0
- package/skills/gsd-path-define/scripts/roadmap.py +5 -0
- package/skills/gsd-path-define/scripts/state_checkpoint.py +5 -0
- package/skills/gsd-path-define/scripts/state_promote.py +5 -0
- package/skills/gsd-path-define/scripts/worktree_paths.py +94 -0
- package/skills/gsd-path-discuss/SKILL.md +2 -0
- package/skills/gsd-path-discuss/references/runtime-selection.md +25 -0
- package/skills/gsd-path-discuss/scripts/_common.py +5 -0
- package/skills/gsd-path-discuss/scripts/discussion_records.py +3 -0
- package/skills/gsd-path-discuss/scripts/loop_run.py +839 -0
- package/skills/gsd-path-discuss/scripts/model_policy.py +279 -0
- package/skills/gsd-path-discuss/scripts/path_config.py +248 -0
- package/skills/gsd-path-discuss/scripts/review_panel.py +564 -0
- package/skills/gsd-path-discuss/scripts/worktree_paths.py +94 -0
- package/skills/gsd-path-docs-audit/SKILL.md +2 -0
- package/skills/gsd-path-docs-audit/references/dispatch.md +6 -19
- package/skills/gsd-path-docs-audit/references/model-policy.md +139 -0
- package/skills/gsd-path-docs-audit/references/runtime-selection.md +25 -0
- package/skills/gsd-path-docs-audit/scripts/_common.py +5 -0
- package/skills/gsd-path-docs-audit/scripts/archive_milestone.py +20 -6
- package/skills/gsd-path-docs-audit/scripts/build_recovery.py +5 -0
- package/skills/gsd-path-docs-audit/scripts/check_docs_audit.py +2 -0
- package/skills/gsd-path-docs-audit/scripts/check_handoffs.py +6 -0
- package/skills/gsd-path-docs-audit/scripts/check_task_briefs.py +3 -0
- package/skills/gsd-path-docs-audit/scripts/detect_project.py +11 -1
- package/skills/gsd-path-docs-audit/scripts/discussion_records.py +3 -0
- package/skills/gsd-path-docs-audit/scripts/discussion_validate.py +8 -3
- package/skills/gsd-path-docs-audit/scripts/integration.py +28 -8
- package/skills/gsd-path-docs-audit/scripts/isolation.py +15 -5
- package/skills/gsd-path-docs-audit/scripts/loop_run.py +839 -0
- package/skills/gsd-path-docs-audit/scripts/model_policy.py +279 -0
- package/skills/gsd-path-docs-audit/scripts/path_config.py +248 -0
- package/skills/gsd-path-docs-audit/scripts/pipeline_git.py +3 -0
- package/skills/gsd-path-docs-audit/scripts/pipeline_state.py +4 -1
- package/skills/gsd-path-docs-audit/scripts/promote_lookahead.py +3 -0
- package/skills/gsd-path-docs-audit/scripts/review_panel.py +11 -0
- package/skills/gsd-path-docs-audit/scripts/roadmap.py +5 -0
- package/skills/gsd-path-docs-audit/scripts/state_checkpoint.py +5 -0
- package/skills/gsd-path-docs-audit/scripts/state_promote.py +5 -0
- package/skills/gsd-path-docs-audit/scripts/worktree_paths.py +94 -0
- package/skills/gsd-path-forensics/SKILL.md +9 -5
- package/skills/gsd-path-forensics/references/runtime-selection.md +25 -0
- package/skills/gsd-path-forensics/scripts/_common.py +5 -0
- package/skills/gsd-path-forensics/scripts/discussion_records.py +3 -0
- package/skills/gsd-path-forensics/scripts/loop_run.py +839 -0
- package/skills/gsd-path-forensics/scripts/model_policy.py +279 -0
- package/skills/gsd-path-forensics/scripts/path_config.py +248 -0
- package/skills/gsd-path-forensics/scripts/pipeline_diagnose.py +3 -0
- package/skills/gsd-path-forensics/scripts/pipeline_undo.py +3 -0
- package/skills/gsd-path-forensics/scripts/review_panel.py +564 -0
- package/skills/gsd-path-forensics/scripts/worktree_paths.py +94 -0
- package/skills/gsd-path-inspect/SKILL.md +2 -0
- package/skills/gsd-path-inspect/references/dispatch.md +6 -19
- package/skills/gsd-path-inspect/references/model-policy.md +139 -0
- package/skills/gsd-path-inspect/references/runtime-selection.md +25 -0
- package/skills/gsd-path-inspect/scripts/__pycache__/_common.cpython-312.pyc +0 -0
- package/skills/gsd-path-inspect/scripts/_common.py +5 -0
- package/skills/gsd-path-inspect/scripts/archive_milestone.py +20 -6
- package/skills/gsd-path-inspect/scripts/build_recovery.py +5 -0
- package/skills/gsd-path-inspect/scripts/check_docs_audit.py +2 -0
- package/skills/gsd-path-inspect/scripts/check_handoffs.py +6 -0
- package/skills/gsd-path-inspect/scripts/check_task_briefs.py +3 -0
- package/skills/gsd-path-inspect/scripts/detect_project.py +11 -1
- package/skills/gsd-path-inspect/scripts/discussion_records.py +3 -0
- package/skills/gsd-path-inspect/scripts/discussion_validate.py +8 -3
- package/skills/gsd-path-inspect/scripts/integration.py +28 -8
- package/skills/gsd-path-inspect/scripts/isolation.py +15 -5
- package/skills/gsd-path-inspect/scripts/loop_run.py +839 -0
- package/skills/gsd-path-inspect/scripts/model_policy.py +279 -0
- package/skills/gsd-path-inspect/scripts/path_config.py +248 -0
- package/skills/gsd-path-inspect/scripts/pipeline_git.py +3 -0
- package/skills/gsd-path-inspect/scripts/pipeline_state.py +4 -1
- package/skills/gsd-path-inspect/scripts/promote_lookahead.py +3 -0
- package/skills/gsd-path-inspect/scripts/review_panel.py +11 -0
- package/skills/gsd-path-inspect/scripts/roadmap.py +5 -0
- package/skills/gsd-path-inspect/scripts/state_checkpoint.py +5 -0
- package/skills/gsd-path-inspect/scripts/state_promote.py +5 -0
- package/skills/gsd-path-inspect/scripts/workflow_run.py +12 -1
- package/skills/gsd-path-inspect/scripts/worktree_paths.py +94 -0
- package/skills/gsd-path-loop/SKILL.md +2 -0
- package/skills/gsd-path-loop/references/runtime-selection.md +25 -0
- package/skills/gsd-path-loop/scripts/_common.py +5 -0
- package/skills/gsd-path-loop/scripts/discussion_records.py +3 -0
- package/skills/gsd-path-loop/scripts/loop_run.py +2 -0
- package/skills/gsd-path-loop/scripts/model_policy.py +279 -0
- package/skills/gsd-path-loop/scripts/path_config.py +248 -0
- package/skills/gsd-path-loop/scripts/review_panel.py +564 -0
- package/skills/gsd-path-loop/scripts/worktree_paths.py +94 -0
- package/skills/gsd-path-migrate/SKILL.md +2 -0
- package/skills/gsd-path-migrate/references/runtime-selection.md +25 -0
- package/skills/gsd-path-migrate/scripts/_common.py +185 -0
- package/skills/gsd-path-migrate/scripts/discussion_records.py +3 -0
- package/skills/gsd-path-migrate/scripts/loop_run.py +839 -0
- package/skills/gsd-path-migrate/scripts/model_policy.py +279 -0
- package/skills/gsd-path-migrate/scripts/path_config.py +248 -0
- package/skills/gsd-path-migrate/scripts/review_panel.py +564 -0
- package/skills/gsd-path-plan/SKILL.md +4 -2
- package/skills/gsd-path-plan/references/dispatch.md +6 -19
- package/skills/gsd-path-plan/references/model-policy.md +139 -0
- package/skills/gsd-path-plan/references/runtime-selection.md +25 -0
- package/skills/gsd-path-plan/scripts/__pycache__/_common.cpython-312.pyc +0 -0
- package/skills/gsd-path-plan/scripts/_common.py +5 -0
- package/skills/gsd-path-plan/scripts/archive_milestone.py +20 -6
- package/skills/gsd-path-plan/scripts/build_recovery.py +5 -0
- package/skills/gsd-path-plan/scripts/check_handoffs.py +6 -0
- package/skills/gsd-path-plan/scripts/check_task_briefs.py +3 -0
- package/skills/gsd-path-plan/scripts/detect_project.py +11 -1
- package/skills/gsd-path-plan/scripts/discussion_records.py +3 -0
- package/skills/gsd-path-plan/scripts/discussion_validate.py +8 -3
- package/skills/gsd-path-plan/scripts/integration.py +28 -8
- package/skills/gsd-path-plan/scripts/isolation.py +15 -5
- package/skills/gsd-path-plan/scripts/loop_run.py +839 -0
- package/skills/gsd-path-plan/scripts/model_policy.py +279 -0
- package/skills/gsd-path-plan/scripts/path_config.py +248 -0
- package/skills/gsd-path-plan/scripts/pipeline_git.py +3 -0
- package/skills/gsd-path-plan/scripts/pipeline_state.py +4 -1
- package/skills/gsd-path-plan/scripts/promote_lookahead.py +3 -0
- package/skills/gsd-path-plan/scripts/review_panel.py +11 -0
- package/skills/gsd-path-plan/scripts/roadmap.py +5 -0
- package/skills/gsd-path-plan/scripts/state_checkpoint.py +5 -0
- package/skills/gsd-path-plan/scripts/state_promote.py +5 -0
- package/skills/gsd-path-plan/scripts/workflow_run.py +12 -1
- package/skills/gsd-path-plan/scripts/worktree_paths.py +94 -0
- package/skills/gsd-path-plan/templates/task.md +2 -0
- package/skills/gsd-path-research/SKILL.md +2 -0
- package/skills/gsd-path-research/references/dispatch.md +6 -19
- package/skills/gsd-path-research/references/model-policy.md +139 -0
- package/skills/gsd-path-research/references/runtime-selection.md +25 -0
- package/skills/gsd-path-research/scripts/_common.py +5 -0
- package/skills/gsd-path-research/scripts/archive_milestone.py +20 -6
- package/skills/gsd-path-research/scripts/build_recovery.py +5 -0
- package/skills/gsd-path-research/scripts/check_handoffs.py +6 -0
- package/skills/gsd-path-research/scripts/check_task_briefs.py +3 -0
- package/skills/gsd-path-research/scripts/detect_project.py +11 -1
- package/skills/gsd-path-research/scripts/discussion_records.py +3 -0
- package/skills/gsd-path-research/scripts/discussion_validate.py +8 -3
- package/skills/gsd-path-research/scripts/integration.py +28 -8
- package/skills/gsd-path-research/scripts/isolation.py +15 -5
- package/skills/gsd-path-research/scripts/loop_run.py +839 -0
- package/skills/gsd-path-research/scripts/model_policy.py +279 -0
- package/skills/gsd-path-research/scripts/path_config.py +248 -0
- package/skills/gsd-path-research/scripts/pipeline_git.py +3 -0
- package/skills/gsd-path-research/scripts/pipeline_state.py +4 -1
- package/skills/gsd-path-research/scripts/promote_lookahead.py +3 -0
- package/skills/gsd-path-research/scripts/review_panel.py +11 -0
- package/skills/gsd-path-research/scripts/roadmap.py +5 -0
- package/skills/gsd-path-research/scripts/state_checkpoint.py +5 -0
- package/skills/gsd-path-research/scripts/state_promote.py +5 -0
- package/skills/gsd-path-research/scripts/worktree_paths.py +94 -0
- package/skills/gsd-path-roadmap/SKILL.md +2 -0
- package/skills/gsd-path-roadmap/references/dispatch.md +6 -19
- package/skills/gsd-path-roadmap/references/model-policy.md +139 -0
- package/skills/gsd-path-roadmap/references/runtime-selection.md +25 -0
- package/skills/gsd-path-roadmap/scripts/_common.py +5 -0
- package/skills/gsd-path-roadmap/scripts/archive_milestone.py +20 -6
- package/skills/gsd-path-roadmap/scripts/build_recovery.py +5 -0
- package/skills/gsd-path-roadmap/scripts/check_handoffs.py +6 -0
- package/skills/gsd-path-roadmap/scripts/check_task_briefs.py +3 -0
- package/skills/gsd-path-roadmap/scripts/detect_project.py +11 -1
- package/skills/gsd-path-roadmap/scripts/discussion_records.py +3 -0
- package/skills/gsd-path-roadmap/scripts/discussion_validate.py +8 -3
- package/skills/gsd-path-roadmap/scripts/integration.py +28 -8
- package/skills/gsd-path-roadmap/scripts/isolation.py +15 -5
- package/skills/gsd-path-roadmap/scripts/loop_run.py +839 -0
- package/skills/gsd-path-roadmap/scripts/model_policy.py +279 -0
- package/skills/gsd-path-roadmap/scripts/path_config.py +248 -0
- package/skills/gsd-path-roadmap/scripts/pipeline_git.py +3 -0
- package/skills/gsd-path-roadmap/scripts/pipeline_state.py +4 -1
- package/skills/gsd-path-roadmap/scripts/promote_lookahead.py +3 -0
- package/skills/gsd-path-roadmap/scripts/review_panel.py +11 -0
- package/skills/gsd-path-roadmap/scripts/roadmap.py +5 -0
- package/skills/gsd-path-roadmap/scripts/state_checkpoint.py +5 -0
- package/skills/gsd-path-roadmap/scripts/state_promote.py +5 -0
- package/skills/gsd-path-roadmap/scripts/worktree_paths.py +94 -0
- package/skills/gsd-path-ship/SKILL.md +24 -13
- package/skills/gsd-path-ship/references/dispatch.md +6 -19
- package/skills/gsd-path-ship/references/jev-review.md +82 -0
- package/skills/gsd-path-ship/references/model-policy.md +139 -0
- package/skills/gsd-path-ship/references/reviewer-panel.md +1 -1
- package/skills/gsd-path-ship/references/reviewer.md +2 -0
- package/skills/gsd-path-ship/references/runtime-selection.md +25 -0
- package/skills/gsd-path-ship/scripts/__pycache__/_common.cpython-312.pyc +0 -0
- package/skills/gsd-path-ship/scripts/_common.py +5 -0
- package/skills/gsd-path-ship/scripts/archive_milestone.py +20 -6
- package/skills/gsd-path-ship/scripts/build_recovery.py +5 -0
- package/skills/gsd-path-ship/scripts/build_state.py +2 -0
- package/skills/gsd-path-ship/scripts/check_handoffs.py +6 -0
- package/skills/gsd-path-ship/scripts/check_task_briefs.py +3 -0
- package/skills/gsd-path-ship/scripts/detect_project.py +11 -1
- package/skills/gsd-path-ship/scripts/discussion_records.py +3 -0
- package/skills/gsd-path-ship/scripts/discussion_validate.py +8 -3
- package/skills/gsd-path-ship/scripts/integration.py +28 -8
- package/skills/gsd-path-ship/scripts/isolation.py +15 -5
- package/skills/gsd-path-ship/scripts/jev_review.py +138 -0
- package/skills/gsd-path-ship/scripts/lean_verification.py +2 -0
- package/skills/gsd-path-ship/scripts/loop_run.py +839 -0
- package/skills/gsd-path-ship/scripts/model_policy.py +279 -0
- package/skills/gsd-path-ship/scripts/path_config.py +248 -0
- package/skills/gsd-path-ship/scripts/pipeline_git.py +3 -0
- package/skills/gsd-path-ship/scripts/pipeline_state.py +4 -1
- package/skills/gsd-path-ship/scripts/promote_lookahead.py +3 -0
- package/skills/gsd-path-ship/scripts/review_panel.py +11 -0
- package/skills/gsd-path-ship/scripts/roadmap.py +5 -0
- package/skills/gsd-path-ship/scripts/state_checkpoint.py +5 -0
- package/skills/gsd-path-ship/scripts/state_promote.py +5 -0
- package/skills/gsd-path-ship/scripts/workflow_run.py +12 -1
- package/skills/gsd-path-ship/scripts/worktree_paths.py +94 -0
- package/skills/gsd-path-undo/SKILL.md +2 -0
- package/skills/gsd-path-undo/references/runtime-selection.md +25 -0
- package/skills/gsd-path-undo/scripts/_common.py +5 -0
- package/skills/gsd-path-undo/scripts/discussion_records.py +3 -0
- package/skills/gsd-path-undo/scripts/loop_run.py +839 -0
- package/skills/gsd-path-undo/scripts/model_policy.py +279 -0
- package/skills/gsd-path-undo/scripts/path_config.py +248 -0
- package/skills/gsd-path-undo/scripts/pipeline_undo.py +3 -0
- package/skills/gsd-path-undo/scripts/review_panel.py +564 -0
- package/skills/gsd-path-undo/scripts/worktree_paths.py +94 -0
- package/skills/path/BUILD.md +7 -0
- package/skills/path/DECIDE.md +2 -0
- package/skills/path/DEFINE.md +13 -7
- package/skills/path/DOCS-AUDIT.md +2 -0
- package/skills/path/INSPECT.md +2 -0
- package/skills/path/PLAN.md +4 -2
- package/skills/path/RESEARCH.md +2 -0
- package/skills/path/ROADMAP.md +2 -0
- package/skills/path/SHIP.md +24 -13
- package/skills/path/SKILL.md +23 -9
- package/skills/path/references/build-native.md +15 -7
- package/skills/path/references/config.md +57 -0
- package/skills/path/references/dispatch.md +6 -19
- package/skills/path/references/jev-review.md +82 -0
- package/skills/path/references/model-policy.md +139 -0
- package/skills/path/references/reviewer-panel.md +1 -1
- package/skills/path/references/reviewer.md +2 -0
- package/skills/path/references/runtime-selection.md +25 -0
- package/skills/path/scripts/_common.py +5 -0
- package/skills/path/scripts/archive_milestone.py +20 -6
- package/skills/path/scripts/bootstrap_repository.py +8 -1
- package/skills/path/scripts/build_recovery.py +5 -0
- package/skills/path/scripts/build_state.py +2 -0
- package/skills/path/scripts/check_docs_audit.py +2 -0
- package/skills/path/scripts/check_handoffs.py +6 -0
- package/skills/path/scripts/check_task_briefs.py +3 -0
- package/skills/path/scripts/detect_project.py +11 -1
- package/skills/path/scripts/discussion_records.py +3 -0
- package/skills/path/scripts/discussion_validate.py +8 -3
- package/skills/path/scripts/dispatch_driver.py +230 -21
- package/skills/path/scripts/git_guard.py +768 -0
- package/skills/path/scripts/integration.py +28 -8
- package/skills/path/scripts/isolation.py +15 -5
- package/skills/path/scripts/jev_review.py +138 -0
- package/skills/path/scripts/lean_verification.py +2 -0
- package/skills/path/scripts/loop_run.py +2 -0
- package/skills/path/scripts/model_policy.py +279 -0
- package/skills/path/scripts/path_config.py +248 -0
- package/skills/path/scripts/pipeline_git.py +3 -0
- package/skills/path/scripts/pipeline_state.py +4 -1
- package/skills/path/scripts/promote_lookahead.py +3 -0
- package/skills/path/scripts/review_panel.py +11 -0
- package/skills/path/scripts/roadmap.py +5 -0
- package/skills/path/scripts/state_checkpoint.py +5 -0
- package/skills/path/scripts/state_promote.py +5 -0
- package/skills/path/scripts/status_runtime.py +208 -0
- package/skills/path/scripts/workflow_run.py +12 -1
- package/skills/path/scripts/worktree_paths.py +94 -0
- package/skills/path/templates/skeptic.md +1 -1
- package/skills/path/templates/task.md +2 -0
- package/skills/path/templates/wave-panel.md +1 -1
- package/skills/gsd-path/scripts/__pycache__/archive_milestone.cpython-312.pyc +0 -0
- package/skills/gsd-path/scripts/__pycache__/build_recovery.cpython-312.pyc +0 -0
- package/skills/gsd-path/scripts/__pycache__/build_state.cpython-312.pyc +0 -0
- package/skills/gsd-path/scripts/__pycache__/check_task_briefs.cpython-312.pyc +0 -0
- package/skills/gsd-path/scripts/__pycache__/detect_project.cpython-312.pyc +0 -0
- package/skills/gsd-path/scripts/__pycache__/discussion_records.cpython-312.pyc +0 -0
- package/skills/gsd-path/scripts/__pycache__/discussion_validate.cpython-312.pyc +0 -0
- package/skills/gsd-path/scripts/__pycache__/integration.cpython-312.pyc +0 -0
- package/skills/gsd-path/scripts/__pycache__/isolation.cpython-312.pyc +0 -0
- package/skills/gsd-path/scripts/__pycache__/loop_run.cpython-312.pyc +0 -0
- package/skills/gsd-path/scripts/__pycache__/pipeline_git.cpython-312.pyc +0 -0
- package/skills/gsd-path/scripts/__pycache__/pipeline_state.cpython-312.pyc +0 -0
- package/skills/gsd-path/scripts/__pycache__/review_findings.cpython-312.pyc +0 -0
- package/skills/gsd-path/scripts/__pycache__/review_panel.cpython-312.pyc +0 -0
- package/skills/gsd-path/scripts/__pycache__/roadmap.cpython-312.pyc +0 -0
- package/skills/gsd-path/scripts/__pycache__/state_checkpoint.cpython-312.pyc +0 -0
- package/skills/gsd-path/scripts/__pycache__/task_context.cpython-312.pyc +0 -0
- package/skills/gsd-path/scripts/__pycache__/workflow_run.cpython-312.pyc +0 -0
- package/skills/gsd-path-build/scripts/__pycache__/archive_milestone.cpython-312.pyc +0 -0
- package/skills/gsd-path-build/scripts/__pycache__/build_recovery.cpython-312.pyc +0 -0
- package/skills/gsd-path-build/scripts/__pycache__/build_state.cpython-312.pyc +0 -0
- package/skills/gsd-path-build/scripts/__pycache__/check_task_briefs.cpython-312.pyc +0 -0
- package/skills/gsd-path-build/scripts/__pycache__/detect_project.cpython-312.pyc +0 -0
- package/skills/gsd-path-build/scripts/__pycache__/discussion_validate.cpython-312.pyc +0 -0
- package/skills/gsd-path-build/scripts/__pycache__/isolation.cpython-312.pyc +0 -0
- package/skills/gsd-path-build/scripts/__pycache__/loop_run.cpython-312.pyc +0 -0
- package/skills/gsd-path-build/scripts/__pycache__/pipeline_git.cpython-312.pyc +0 -0
- package/skills/gsd-path-build/scripts/__pycache__/pipeline_state.cpython-312.pyc +0 -0
- package/skills/gsd-path-build/scripts/__pycache__/review_findings.cpython-312.pyc +0 -0
- package/skills/gsd-path-build/scripts/__pycache__/review_panel.cpython-312.pyc +0 -0
- package/skills/gsd-path-build/scripts/__pycache__/roadmap.cpython-312.pyc +0 -0
- package/skills/gsd-path-build/scripts/__pycache__/state_checkpoint.cpython-312.pyc +0 -0
- package/skills/gsd-path-build/scripts/__pycache__/task_context.cpython-312.pyc +0 -0
- package/skills/gsd-path-build/scripts/__pycache__/workflow_run.cpython-312.pyc +0 -0
- package/skills/gsd-path-decide/scripts/__pycache__/_common.cpython-312.pyc +0 -0
- package/skills/gsd-path-decide/scripts/__pycache__/archive_milestone.cpython-312.pyc +0 -0
- package/skills/gsd-path-decide/scripts/__pycache__/detect_project.cpython-312.pyc +0 -0
- package/skills/gsd-path-decide/scripts/__pycache__/discussion_validate.cpython-312.pyc +0 -0
- package/skills/gsd-path-decide/scripts/__pycache__/isolation.cpython-312.pyc +0 -0
- package/skills/gsd-path-decide/scripts/__pycache__/pipeline_git.cpython-312.pyc +0 -0
- package/skills/gsd-path-decide/scripts/__pycache__/pipeline_state.cpython-312.pyc +0 -0
- package/skills/gsd-path-decide/scripts/__pycache__/review_panel.cpython-312.pyc +0 -0
- package/skills/gsd-path-decide/scripts/__pycache__/roadmap.cpython-312.pyc +0 -0
- package/skills/gsd-path-decide/scripts/__pycache__/state_checkpoint.cpython-312.pyc +0 -0
- package/skills/gsd-path-define/scripts/__pycache__/_common.cpython-312.pyc +0 -0
- package/skills/gsd-path-define/scripts/__pycache__/archive_milestone.cpython-312.pyc +0 -0
- package/skills/gsd-path-define/scripts/__pycache__/detect_project.cpython-312.pyc +0 -0
- package/skills/gsd-path-define/scripts/__pycache__/discussion_validate.cpython-312.pyc +0 -0
- package/skills/gsd-path-define/scripts/__pycache__/isolation.cpython-312.pyc +0 -0
- package/skills/gsd-path-define/scripts/__pycache__/pipeline_git.cpython-312.pyc +0 -0
- package/skills/gsd-path-define/scripts/__pycache__/pipeline_state.cpython-312.pyc +0 -0
- package/skills/gsd-path-define/scripts/__pycache__/review_panel.cpython-312.pyc +0 -0
- package/skills/gsd-path-define/scripts/__pycache__/roadmap.cpython-312.pyc +0 -0
- package/skills/gsd-path-define/scripts/__pycache__/state_checkpoint.cpython-312.pyc +0 -0
- package/skills/gsd-path-docs-audit/scripts/__pycache__/_common.cpython-312.pyc +0 -0
- package/skills/gsd-path-docs-audit/scripts/__pycache__/archive_milestone.cpython-312.pyc +0 -0
- package/skills/gsd-path-docs-audit/scripts/__pycache__/build_recovery.cpython-312.pyc +0 -0
- package/skills/gsd-path-docs-audit/scripts/__pycache__/detect_project.cpython-312.pyc +0 -0
- package/skills/gsd-path-docs-audit/scripts/__pycache__/discussion_validate.cpython-312.pyc +0 -0
- package/skills/gsd-path-docs-audit/scripts/__pycache__/isolation.cpython-312.pyc +0 -0
- package/skills/gsd-path-docs-audit/scripts/__pycache__/pipeline_git.cpython-312.pyc +0 -0
- package/skills/gsd-path-docs-audit/scripts/__pycache__/pipeline_state.cpython-312.pyc +0 -0
- package/skills/gsd-path-docs-audit/scripts/__pycache__/review_panel.cpython-312.pyc +0 -0
- package/skills/gsd-path-docs-audit/scripts/__pycache__/roadmap.cpython-312.pyc +0 -0
- package/skills/gsd-path-docs-audit/scripts/__pycache__/state_checkpoint.cpython-312.pyc +0 -0
- package/skills/gsd-path-forensics/scripts/__pycache__/pipeline_undo.cpython-312.pyc +0 -0
- package/skills/gsd-path-inspect/scripts/__pycache__/archive_milestone.cpython-312.pyc +0 -0
- package/skills/gsd-path-inspect/scripts/__pycache__/build_recovery.cpython-312.pyc +0 -0
- package/skills/gsd-path-inspect/scripts/__pycache__/detect_project.cpython-312.pyc +0 -0
- package/skills/gsd-path-inspect/scripts/__pycache__/discussion_validate.cpython-312.pyc +0 -0
- package/skills/gsd-path-inspect/scripts/__pycache__/isolation.cpython-312.pyc +0 -0
- package/skills/gsd-path-inspect/scripts/__pycache__/pipeline_git.cpython-312.pyc +0 -0
- package/skills/gsd-path-inspect/scripts/__pycache__/pipeline_state.cpython-312.pyc +0 -0
- package/skills/gsd-path-inspect/scripts/__pycache__/review_panel.cpython-312.pyc +0 -0
- package/skills/gsd-path-inspect/scripts/__pycache__/roadmap.cpython-312.pyc +0 -0
- package/skills/gsd-path-inspect/scripts/__pycache__/state_checkpoint.cpython-312.pyc +0 -0
- package/skills/gsd-path-plan/scripts/__pycache__/archive_milestone.cpython-312.pyc +0 -0
- package/skills/gsd-path-plan/scripts/__pycache__/detect_project.cpython-312.pyc +0 -0
- package/skills/gsd-path-plan/scripts/__pycache__/discussion_validate.cpython-312.pyc +0 -0
- package/skills/gsd-path-plan/scripts/__pycache__/isolation.cpython-312.pyc +0 -0
- package/skills/gsd-path-plan/scripts/__pycache__/pipeline_git.cpython-312.pyc +0 -0
- package/skills/gsd-path-plan/scripts/__pycache__/pipeline_state.cpython-312.pyc +0 -0
- package/skills/gsd-path-plan/scripts/__pycache__/review_panel.cpython-312.pyc +0 -0
- package/skills/gsd-path-plan/scripts/__pycache__/roadmap.cpython-312.pyc +0 -0
- package/skills/gsd-path-plan/scripts/__pycache__/state_checkpoint.cpython-312.pyc +0 -0
- package/skills/gsd-path-research/scripts/__pycache__/_common.cpython-312.pyc +0 -0
- package/skills/gsd-path-research/scripts/__pycache__/archive_milestone.cpython-312.pyc +0 -0
- package/skills/gsd-path-research/scripts/__pycache__/detect_project.cpython-312.pyc +0 -0
- package/skills/gsd-path-research/scripts/__pycache__/discussion_validate.cpython-312.pyc +0 -0
- package/skills/gsd-path-research/scripts/__pycache__/isolation.cpython-312.pyc +0 -0
- package/skills/gsd-path-research/scripts/__pycache__/pipeline_git.cpython-312.pyc +0 -0
- package/skills/gsd-path-research/scripts/__pycache__/pipeline_state.cpython-312.pyc +0 -0
- package/skills/gsd-path-research/scripts/__pycache__/review_panel.cpython-312.pyc +0 -0
- package/skills/gsd-path-research/scripts/__pycache__/roadmap.cpython-312.pyc +0 -0
- package/skills/gsd-path-research/scripts/__pycache__/state_checkpoint.cpython-312.pyc +0 -0
- package/skills/gsd-path-roadmap/scripts/__pycache__/_common.cpython-312.pyc +0 -0
- package/skills/gsd-path-roadmap/scripts/__pycache__/archive_milestone.cpython-312.pyc +0 -0
- package/skills/gsd-path-roadmap/scripts/__pycache__/detect_project.cpython-312.pyc +0 -0
- package/skills/gsd-path-roadmap/scripts/__pycache__/discussion_validate.cpython-312.pyc +0 -0
- package/skills/gsd-path-roadmap/scripts/__pycache__/isolation.cpython-312.pyc +0 -0
- package/skills/gsd-path-roadmap/scripts/__pycache__/pipeline_git.cpython-312.pyc +0 -0
- package/skills/gsd-path-roadmap/scripts/__pycache__/pipeline_state.cpython-312.pyc +0 -0
- package/skills/gsd-path-roadmap/scripts/__pycache__/review_panel.cpython-312.pyc +0 -0
- package/skills/gsd-path-roadmap/scripts/__pycache__/roadmap.cpython-312.pyc +0 -0
- package/skills/gsd-path-roadmap/scripts/__pycache__/state_checkpoint.cpython-312.pyc +0 -0
- package/skills/gsd-path-ship/scripts/__pycache__/archive_milestone.cpython-312.pyc +0 -0
- package/skills/gsd-path-ship/scripts/__pycache__/build_recovery.cpython-312.pyc +0 -0
- package/skills/gsd-path-ship/scripts/__pycache__/build_state.cpython-312.pyc +0 -0
- package/skills/gsd-path-ship/scripts/__pycache__/check_handoffs.cpython-312.pyc +0 -0
- package/skills/gsd-path-ship/scripts/__pycache__/check_task_briefs.cpython-312.pyc +0 -0
- package/skills/gsd-path-ship/scripts/__pycache__/detect_project.cpython-312.pyc +0 -0
- package/skills/gsd-path-ship/scripts/__pycache__/discussion_validate.cpython-312.pyc +0 -0
- package/skills/gsd-path-ship/scripts/__pycache__/isolation.cpython-312.pyc +0 -0
- package/skills/gsd-path-ship/scripts/__pycache__/pipeline_git.cpython-312.pyc +0 -0
- package/skills/gsd-path-ship/scripts/__pycache__/pipeline_state.cpython-312.pyc +0 -0
- package/skills/gsd-path-ship/scripts/__pycache__/review_panel.cpython-312.pyc +0 -0
- package/skills/gsd-path-ship/scripts/__pycache__/roadmap.cpython-312.pyc +0 -0
- package/skills/gsd-path-ship/scripts/__pycache__/state_checkpoint.cpython-312.pyc +0 -0
package/scripts/install.mjs
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
// Dependency-free; requires Node >= 18.17.
|
|
4
4
|
|
|
5
5
|
import { spawnSync } from "node:child_process";
|
|
6
|
+
import { createHash } from "node:crypto";
|
|
6
7
|
import fs from "node:fs";
|
|
7
8
|
import os from "node:os";
|
|
8
9
|
import path from "node:path";
|
|
@@ -28,6 +29,7 @@ export const PROJECT_RUNTIME_SCRIPTS = [
|
|
|
28
29
|
"archive_milestone.py",
|
|
29
30
|
"review_panel.py",
|
|
30
31
|
"_common.py",
|
|
32
|
+
"worktree_paths.py",
|
|
31
33
|
"build_recovery.py",
|
|
32
34
|
"state_checkpoint.py",
|
|
33
35
|
"state_promote.py",
|
|
@@ -40,52 +42,6 @@ export const PROJECT_STATUS_MARKER = "gsd-path project status launcher";
|
|
|
40
42
|
const INSTALL_LOCK_NAME = ".gsd-path-install-lock";
|
|
41
43
|
const INSTALL_LOCK_OWNER = "owner.json";
|
|
42
44
|
const INSTALL_LOCK_SCHEMA = "gsd-path/install-lock/v2";
|
|
43
|
-
const PROJECT_CONTRACTS = [
|
|
44
|
-
["AGENTS.md", "## Plain-prompt re-entry"],
|
|
45
|
-
["WORKFLOW.md", "### Plain-prompt re-entry"],
|
|
46
|
-
];
|
|
47
|
-
const STATUS_ACTIONS = new Set([
|
|
48
|
-
"bind-initial",
|
|
49
|
-
"block",
|
|
50
|
-
"resume-checkpoint",
|
|
51
|
-
"resume-next-handoff",
|
|
52
|
-
"resume-promotion",
|
|
53
|
-
"resume-shipment",
|
|
54
|
-
"resume-undo",
|
|
55
|
-
"run-phase",
|
|
56
|
-
"validate-integrated",
|
|
57
|
-
"wait",
|
|
58
|
-
]);
|
|
59
|
-
const STATUS_PHASES = new Set(["inspect", "define", "research", "decide", "roadmap", "plan", "build", "ship", "shipped"]);
|
|
60
|
-
const STATUS_VALUES = new Set(["active", "done", "blocked"]);
|
|
61
|
-
const STATUS_STATE_FIELDS = [
|
|
62
|
-
"archive",
|
|
63
|
-
"branch",
|
|
64
|
-
"integration",
|
|
65
|
-
"integration_default",
|
|
66
|
-
"integration_source",
|
|
67
|
-
"milestone",
|
|
68
|
-
"phase",
|
|
69
|
-
"pipeline",
|
|
70
|
-
"project",
|
|
71
|
-
"status",
|
|
72
|
-
];
|
|
73
|
-
const STATUS_INTEGRATION_MODES = new Set(["direct", "pull-request"]);
|
|
74
|
-
const STATUS_INTEGRATION_SOURCES = new Set(["default", "milestone"]);
|
|
75
|
-
const STATUS_TRANSITIONS = new Map([
|
|
76
|
-
["inspect", new Set(["inspect", "define"])],
|
|
77
|
-
["define", new Set(["define", "research", "plan"])],
|
|
78
|
-
["research", new Set(["research", "decide"])],
|
|
79
|
-
["decide", new Set(["decide", "roadmap", "plan"])],
|
|
80
|
-
["roadmap", new Set(["roadmap", "define"])],
|
|
81
|
-
["plan", new Set(["plan", "build"])],
|
|
82
|
-
["build", new Set(["build"])],
|
|
83
|
-
["ship", new Set(["ship", "plan"])],
|
|
84
|
-
["shipped", new Set(["ship"])],
|
|
85
|
-
]);
|
|
86
|
-
const STATUS_SLUG = /^[a-z0-9][a-z0-9-]*$/;
|
|
87
|
-
const STATUS_BRANCH = /^gsd-path\/M(\d{3,})$/;
|
|
88
|
-
const STATUS_ARCHIVE = /^\.project\/archive\/(\d{3,})-([a-z0-9][a-z0-9-]*)\/?$/;
|
|
89
45
|
export const CLAUDE_MATCHER = ".*";
|
|
90
46
|
// The managed PreToolUse guard entry, as an object.
|
|
91
47
|
export function claudeGuardEntry(interpreter) {
|
|
@@ -236,21 +192,7 @@ export function resolveGitHooksPath(project) {
|
|
|
236
192
|
return path.resolve(project, hooksPath);
|
|
237
193
|
}
|
|
238
194
|
|
|
239
|
-
function gitHooksDirectory(project) {
|
|
240
|
-
return gitHooksLocation(project).directory;
|
|
241
|
-
}
|
|
242
195
|
|
|
243
|
-
function gitHooksLocation(project) {
|
|
244
|
-
const dotGit = path.join(project, ".git");
|
|
245
|
-
if (!lexists(dotGit)) return { directory: null, resolved: false };
|
|
246
|
-
const resolved = hooks.resolveGitHooksPath(project);
|
|
247
|
-
if (resolved !== null) return { directory: resolved, resolved: true };
|
|
248
|
-
// Fallback when git is not runnable: only a plain .git directory is safe.
|
|
249
|
-
return {
|
|
250
|
-
directory: isDirectory(dotGit) ? path.join(dotGit, "hooks") : null,
|
|
251
|
-
resolved: false,
|
|
252
|
-
};
|
|
253
|
-
}
|
|
254
196
|
|
|
255
197
|
function requiredPythonRuntime(command, selected = []) {
|
|
256
198
|
const targetSuffix = selected.length ? ` for selected hosts: ${selected.join(", ")}` : "";
|
|
@@ -263,18 +205,6 @@ function requiredPythonRuntime(command, selected = []) {
|
|
|
263
205
|
return interpreter;
|
|
264
206
|
}
|
|
265
207
|
|
|
266
|
-
function requiredHookRuntime(project, command, selected = []) {
|
|
267
|
-
const targetSuffix = selected.length ? ` for selected hosts: ${selected.join(", ")}` : "";
|
|
268
|
-
const interpreter = requiredPythonRuntime(command, selected);
|
|
269
|
-
const location = gitHooksLocation(project);
|
|
270
|
-
if (!location.resolved) {
|
|
271
|
-
throw new InstallerError(
|
|
272
|
-
`${command} requires an initialized Git repository with a resolvable hooks ` +
|
|
273
|
-
`directory${targetSuffix}`
|
|
274
|
-
);
|
|
275
|
-
}
|
|
276
|
-
return { interpreter, hooksDir: location.directory };
|
|
277
|
-
}
|
|
278
208
|
const HOST_NOTES = {
|
|
279
209
|
opencode:
|
|
280
210
|
"note: OpenCode stable discovers the skills but has no documented hard " +
|
|
@@ -475,6 +405,11 @@ function isOwnedRouterAlias(skillDir) {
|
|
|
475
405
|
}
|
|
476
406
|
}
|
|
477
407
|
|
|
408
|
+
function isManagedProjectRuntime(destination) {
|
|
409
|
+
if (!isFile(destination)) return false;
|
|
410
|
+
return fs.readFileSync(destination, "utf8").includes(PROJECT_RUNTIME_MARKER);
|
|
411
|
+
}
|
|
412
|
+
|
|
478
413
|
function isManagedInstallEntry(root, name) {
|
|
479
414
|
if (!isManagedName(name)) return false;
|
|
480
415
|
if (!Object.hasOwn(ROUTER_ALIASES, name.toLowerCase())) return true;
|
|
@@ -578,19 +513,6 @@ function samePath(left, right) {
|
|
|
578
513
|
return comparisonPath(left) === comparisonPath(right);
|
|
579
514
|
}
|
|
580
515
|
|
|
581
|
-
function validateProjectGitRoot(project) {
|
|
582
|
-
let probe = project;
|
|
583
|
-
while (!lexists(probe)) probe = path.dirname(probe);
|
|
584
|
-
const result = spawnSync("git", ["rev-parse", "--show-toplevel"], {
|
|
585
|
-
cwd: probe,
|
|
586
|
-
encoding: "utf8",
|
|
587
|
-
env: { ...process.env, GIT_OPTIONAL_LOCKS: "0" },
|
|
588
|
-
});
|
|
589
|
-
const topLevel = result.status === 0 ? result.stdout.trim() : "";
|
|
590
|
-
if (topLevel && !samePath(topLevel, project)) {
|
|
591
|
-
throw new InstallerError(`project path is not the Git worktree root: ${project}`);
|
|
592
|
-
}
|
|
593
|
-
}
|
|
594
516
|
|
|
595
517
|
function validateDirectoryDestination(candidate, label) {
|
|
596
518
|
if (lexists(candidate)) {
|
|
@@ -1051,10 +973,15 @@ function createInstallLock(lock) {
|
|
|
1051
973
|
}
|
|
1052
974
|
}
|
|
1053
975
|
|
|
976
|
+
export function installLockPath(root) {
|
|
977
|
+
const identity = createHash("sha256").update(resolveNonStrict(root)).digest("hex");
|
|
978
|
+
return path.join(os.homedir(), ".gsd-path", "install-locks", identity, INSTALL_LOCK_NAME);
|
|
979
|
+
}
|
|
980
|
+
|
|
1054
981
|
function acquireInstallLocks(roots) {
|
|
1055
982
|
const locks = [];
|
|
1056
983
|
for (const root of roots) {
|
|
1057
|
-
const candidate =
|
|
984
|
+
const candidate = installLockPath(root);
|
|
1058
985
|
if (!locks.some((lock) => samePath(lock, candidate))) locks.push(candidate);
|
|
1059
986
|
}
|
|
1060
987
|
locks.sort();
|
|
@@ -1178,822 +1105,12 @@ function rollbackTarget(transaction) {
|
|
|
1178
1105
|
// Each entry: [destination, sourceName, literalContent, executable].
|
|
1179
1106
|
// hooksDir is the pre-resolved git hooks directory (or null); resolving it
|
|
1180
1107
|
// once per run avoids repeated `git rev-parse` spawns.
|
|
1181
|
-
function projectDestinations(project, selected, hooksEnabled, interpreter, hooksDir) {
|
|
1182
|
-
const includeClaude = selected.includes("claude");
|
|
1183
|
-
const destinations = [
|
|
1184
|
-
[path.join(project, "AGENTS.md"), "AGENTS.md", null, false],
|
|
1185
|
-
[path.join(project, "WORKFLOW.md"), "WORKFLOW.md", null, false],
|
|
1186
|
-
[
|
|
1187
|
-
path.join(project, HOOKS_DIRECTORY, PROJECT_STATUS_LAUNCHER),
|
|
1188
|
-
path.join("scripts", PROJECT_STATUS_LAUNCHER),
|
|
1189
|
-
null,
|
|
1190
|
-
false,
|
|
1191
|
-
],
|
|
1192
|
-
];
|
|
1193
|
-
for (const name of PROJECT_RUNTIME_SCRIPTS) {
|
|
1194
|
-
destinations.push([
|
|
1195
|
-
path.join(project, HOOKS_DIRECTORY, "runtime", name),
|
|
1196
|
-
path.join("scripts", name),
|
|
1197
|
-
null,
|
|
1198
|
-
false,
|
|
1199
|
-
]);
|
|
1200
|
-
}
|
|
1201
|
-
if (includeClaude) {
|
|
1202
|
-
destinations.push([path.join(project, ".claude", "CLAUDE.md"), null, CLAUDE_BRIDGE, false]);
|
|
1203
|
-
}
|
|
1204
|
-
if (hooksEnabled) {
|
|
1205
|
-
for (const name of GUARD_SCRIPTS) {
|
|
1206
|
-
destinations.push([
|
|
1207
|
-
path.join(project, HOOKS_DIRECTORY, name),
|
|
1208
|
-
path.join("scripts", name),
|
|
1209
|
-
null,
|
|
1210
|
-
false,
|
|
1211
|
-
]);
|
|
1212
|
-
}
|
|
1213
|
-
if (includeClaude) {
|
|
1214
|
-
destinations.push([
|
|
1215
|
-
path.join(project, ".claude", "settings.json"),
|
|
1216
|
-
null,
|
|
1217
|
-
claudeHooksSettings(interpreter),
|
|
1218
|
-
false,
|
|
1219
|
-
]);
|
|
1220
|
-
}
|
|
1221
|
-
if (selected.includes("codex")) {
|
|
1222
|
-
destinations.push([
|
|
1223
|
-
path.join(project, ".codex", "hooks.json"),
|
|
1224
|
-
null,
|
|
1225
|
-
codexHooksSettings(interpreter),
|
|
1226
|
-
false,
|
|
1227
|
-
]);
|
|
1228
|
-
}
|
|
1229
|
-
if (selected.includes("cursor")) {
|
|
1230
|
-
destinations.push([
|
|
1231
|
-
path.join(project, ".cursor", "hooks.json"),
|
|
1232
|
-
null,
|
|
1233
|
-
cursorHooksSettings(interpreter),
|
|
1234
|
-
false,
|
|
1235
|
-
]);
|
|
1236
|
-
}
|
|
1237
|
-
if (hooksDir !== null) {
|
|
1238
|
-
for (const [hookName, generator] of GIT_HOOKS) {
|
|
1239
|
-
destinations.push([path.join(hooksDir, hookName), null, generator(interpreter), true]);
|
|
1240
|
-
}
|
|
1241
|
-
}
|
|
1242
|
-
}
|
|
1243
|
-
return destinations;
|
|
1244
|
-
}
|
|
1245
|
-
|
|
1246
|
-
function nativeSettingsMergers(project, selected, hooksEnabled) {
|
|
1247
|
-
const mergers = new Map();
|
|
1248
|
-
if (!hooksEnabled) return mergers;
|
|
1249
|
-
if (selected.includes("claude")) {
|
|
1250
|
-
mergers.set(path.join(project, ".claude", "settings.json"), mergedClaudeSettings);
|
|
1251
|
-
}
|
|
1252
|
-
if (selected.includes("codex")) {
|
|
1253
|
-
mergers.set(path.join(project, ".codex", "hooks.json"), mergedCodexSettings);
|
|
1254
|
-
}
|
|
1255
|
-
if (selected.includes("cursor")) {
|
|
1256
|
-
mergers.set(path.join(project, ".cursor", "hooks.json"), mergedCursorSettings);
|
|
1257
|
-
}
|
|
1258
|
-
return mergers;
|
|
1259
|
-
}
|
|
1260
|
-
|
|
1261
|
-
// --update replaces managed runtime, guard, and git hook files in place and
|
|
1262
|
-
// keeps everything else (AGENTS.md, WORKFLOW.md, CLAUDE.md). Returns the
|
|
1263
|
-
// managed-file check for a replaceable destination, or null for a kept one.
|
|
1264
|
-
function updateReplacement(project, destination, hooksDir) {
|
|
1265
|
-
const parent = path.join(project, HOOKS_DIRECTORY);
|
|
1266
|
-
const directory = path.dirname(destination);
|
|
1267
|
-
if (samePath(directory, path.join(parent, "runtime"))) return isManagedProjectRuntime;
|
|
1268
|
-
if (samePath(destination, path.join(parent, PROJECT_STATUS_LAUNCHER))) {
|
|
1269
|
-
return isManagedProjectStatusLauncher;
|
|
1270
|
-
}
|
|
1271
|
-
if (GUARD_SCRIPTS.some((name) => samePath(destination, path.join(parent, name)))) {
|
|
1272
|
-
return isManagedGuardScript;
|
|
1273
|
-
}
|
|
1274
|
-
if (hooksDir !== null && samePath(directory, hooksDir)) return isManagedGitHook;
|
|
1275
|
-
return null;
|
|
1276
|
-
}
|
|
1277
|
-
|
|
1278
|
-
function describeProjectPath(project, destination) {
|
|
1279
|
-
const relative = path.relative(project, destination);
|
|
1280
|
-
if (relative.startsWith("..") || path.isAbsolute(relative)) {
|
|
1281
|
-
return destination.split(path.sep).join("/");
|
|
1282
|
-
}
|
|
1283
|
-
return relative.split(path.sep).join("/");
|
|
1284
|
-
}
|
|
1285
|
-
|
|
1286
|
-
function projectFiles(project, selected, hooksEnabled, interpreter, hooksDir) {
|
|
1287
|
-
return projectDestinations(project, selected, hooksEnabled, interpreter, hooksDir)
|
|
1288
|
-
.map(([destination]) => describeProjectPath(project, destination))
|
|
1289
|
-
.join(", ");
|
|
1290
|
-
}
|
|
1291
|
-
|
|
1292
|
-
function projectResult(project, selected, hooksEnabled, interpreter, hooksDir, update, dryRun) {
|
|
1293
|
-
if (!update) {
|
|
1294
|
-
const files = projectFiles(project, selected, hooksEnabled, interpreter, hooksDir);
|
|
1295
|
-
return `project: ${dryRun ? "would copy" : "copied"} ${files} to ${project}`;
|
|
1296
|
-
}
|
|
1297
|
-
const mergers = nativeSettingsMergers(project, selected, hooksEnabled);
|
|
1298
|
-
const refreshed = [];
|
|
1299
|
-
const kept = [];
|
|
1300
|
-
for (const [destination] of projectDestinations(project, selected, hooksEnabled, interpreter, hooksDir)) {
|
|
1301
|
-
const replaceable =
|
|
1302
|
-
mergers.has(destination) || updateReplacement(project, destination, hooksDir) !== null;
|
|
1303
|
-
(lexists(destination) && !replaceable ? kept : refreshed).push(describeProjectPath(project, destination));
|
|
1304
|
-
}
|
|
1305
|
-
let line = `project: ${dryRun ? "would refresh" : "refreshed"} ${refreshed.join(", ")} in ${project}`;
|
|
1306
|
-
if (kept.length) line += `; kept ${kept.join(", ")}`;
|
|
1307
|
-
return line;
|
|
1308
|
-
}
|
|
1309
|
-
|
|
1310
|
-
function existingContractError(destination) {
|
|
1311
|
-
return new InstallerError(
|
|
1312
|
-
`project contract already exists: ${destination} — the installer never ` +
|
|
1313
|
-
"overwrites project files. Run --update --project PATH to refresh managed " +
|
|
1314
|
-
"runtime files; merge template changes manually (see UPDATE.md)."
|
|
1315
|
-
);
|
|
1316
|
-
}
|
|
1317
|
-
|
|
1318
|
-
function validateProject(sourceRoot, project, selected, hooksEnabled, reservedRoots, interpreter, hooksDir, update = false) {
|
|
1319
|
-
const includeClaude = selected.includes("claude");
|
|
1320
|
-
validateDirectoryDestination(project, "project path");
|
|
1321
|
-
validateProjectGitRoot(project);
|
|
1322
|
-
validateDirectoryDestination(
|
|
1323
|
-
path.join(project, HOOKS_DIRECTORY),
|
|
1324
|
-
"project runtime parent directory"
|
|
1325
|
-
);
|
|
1326
|
-
validateDirectoryDestination(
|
|
1327
|
-
path.join(project, HOOKS_DIRECTORY, "runtime"),
|
|
1328
|
-
"project runtime directory"
|
|
1329
|
-
);
|
|
1330
|
-
const projectDirectories = [];
|
|
1331
|
-
if (includeClaude) projectDirectories.push(["Claude", path.join(project, ".claude")]);
|
|
1332
|
-
if (hooksEnabled && selected.includes("codex")) {
|
|
1333
|
-
projectDirectories.push(["Codex", path.join(project, ".codex")]);
|
|
1334
|
-
}
|
|
1335
|
-
if (hooksEnabled && selected.includes("cursor")) {
|
|
1336
|
-
projectDirectories.push(["Cursor", path.join(project, ".cursor")]);
|
|
1337
|
-
}
|
|
1338
|
-
for (const [label, directory] of projectDirectories) {
|
|
1339
|
-
if (lexists(directory) && (isSymlink(directory) || !isDirectory(directory))) {
|
|
1340
|
-
throw new InstallerError(`unsafe ${label} project directory: ${directory}`);
|
|
1341
|
-
}
|
|
1342
|
-
}
|
|
1343
|
-
const sources = [
|
|
1344
|
-
"AGENTS.md",
|
|
1345
|
-
"WORKFLOW.md",
|
|
1346
|
-
path.join("scripts", PROJECT_STATUS_LAUNCHER),
|
|
1347
|
-
...PROJECT_RUNTIME_SCRIPTS.map((name) => path.join("scripts", name)),
|
|
1348
|
-
];
|
|
1349
|
-
if (hooksEnabled) {
|
|
1350
|
-
sources.push(...GUARD_SCRIPTS.map((name) => path.join("scripts", name)));
|
|
1351
|
-
}
|
|
1352
|
-
for (const sourceName of sources) {
|
|
1353
|
-
const source = path.join(sourceRoot, sourceName);
|
|
1354
|
-
if (isSymlink(source) || !isFile(source)) {
|
|
1355
|
-
throw new InstallerError(`missing project contract: ${source}`);
|
|
1356
|
-
}
|
|
1357
|
-
}
|
|
1358
|
-
const mergers = nativeSettingsMergers(project, selected, hooksEnabled);
|
|
1359
|
-
for (const [destination] of projectDestinations(project, selected, hooksEnabled, interpreter, hooksDir)) {
|
|
1360
|
-
if (lexists(destination)) {
|
|
1361
|
-
const merge = mergers.get(destination);
|
|
1362
|
-
const managed = update ? updateReplacement(project, destination, hooksDir) : null;
|
|
1363
|
-
if (isSymlink(destination) || (merge === undefined && !update)) {
|
|
1364
|
-
throw existingContractError(destination);
|
|
1365
|
-
}
|
|
1366
|
-
if (merge !== undefined) {
|
|
1367
|
-
merge(destination, interpreter);
|
|
1368
|
-
} else if (managed !== null && !managed(destination)) {
|
|
1369
|
-
throw new InstallerError(`not a managed GSD Path project file: ${destination}`);
|
|
1370
|
-
}
|
|
1371
|
-
}
|
|
1372
|
-
for (const [label, root] of reservedRoots) {
|
|
1373
|
-
if (pathsOverlap(destination, root)) {
|
|
1374
|
-
throw new InstallerError(`project contract overlaps ${label}: ${destination}, ${root}`);
|
|
1375
|
-
}
|
|
1376
|
-
}
|
|
1377
|
-
}
|
|
1378
|
-
}
|
|
1379
|
-
|
|
1380
|
-
function applyProject(sourceRoot, project, selected, hooksEnabled, transaction, interpreter, hooksDir, update = false) {
|
|
1381
|
-
createDirectory(project, transaction.createdDirectories);
|
|
1382
|
-
const mergers = nativeSettingsMergers(project, selected, hooksEnabled);
|
|
1383
|
-
for (const [destination, sourceName, literal, executable] of projectDestinations(
|
|
1384
|
-
project,
|
|
1385
|
-
selected,
|
|
1386
|
-
hooksEnabled,
|
|
1387
|
-
interpreter,
|
|
1388
|
-
hooksDir
|
|
1389
|
-
)) {
|
|
1390
|
-
createDirectory(path.dirname(destination), transaction.createdDirectories);
|
|
1391
|
-
const merge = mergers.get(destination);
|
|
1392
|
-
const content = sourceName
|
|
1393
|
-
? fs.readFileSync(path.join(sourceRoot, sourceName))
|
|
1394
|
-
: Buffer.from(literal, "utf8");
|
|
1395
|
-
if (lexists(destination) && !isSymlink(destination)) {
|
|
1396
|
-
const replaceable = update && updateReplacement(project, destination, hooksDir) !== null;
|
|
1397
|
-
if (merge !== undefined || replaceable) {
|
|
1398
|
-
const original = fs.readFileSync(destination);
|
|
1399
|
-
const mode = fs.statSync(destination).mode & 0o777;
|
|
1400
|
-
const replacement = merge !== undefined ? merge(destination, interpreter) : content;
|
|
1401
|
-
writeFileAtomic(destination, replacement, executable ? 0o755 : mode);
|
|
1402
|
-
transaction.replaced.push({ destination, original, mode });
|
|
1403
|
-
continue;
|
|
1404
|
-
}
|
|
1405
|
-
if (update) continue;
|
|
1406
|
-
}
|
|
1407
|
-
let fd = null;
|
|
1408
|
-
try {
|
|
1409
|
-
fd = fs.openSync(destination, "wx");
|
|
1410
|
-
fs.writeSync(fd, content);
|
|
1411
|
-
if (executable) fs.fchmodSync(fd, 0o755);
|
|
1412
|
-
fs.closeSync(fd);
|
|
1413
|
-
fd = null;
|
|
1414
|
-
transaction.copied.push(destination);
|
|
1415
|
-
} catch (error) {
|
|
1416
|
-
if (fd !== null) fs.closeSync(fd);
|
|
1417
|
-
if (error && error.code === "EEXIST") {
|
|
1418
|
-
throw existingContractError(destination);
|
|
1419
|
-
}
|
|
1420
|
-
removePath(destination);
|
|
1421
|
-
throw error;
|
|
1422
|
-
}
|
|
1423
|
-
}
|
|
1424
|
-
}
|
|
1425
|
-
|
|
1426
|
-
function rollbackProject(transaction) {
|
|
1427
|
-
for (const { destination, original, mode } of [...transaction.replaced].reverse()) {
|
|
1428
|
-
writeFileAtomic(destination, original, mode);
|
|
1429
|
-
}
|
|
1430
|
-
for (const destination of [...transaction.copied].reverse()) {
|
|
1431
|
-
removePath(destination);
|
|
1432
|
-
}
|
|
1433
|
-
removeEmptyDirectories(transaction.createdDirectories);
|
|
1434
|
-
}
|
|
1435
|
-
|
|
1436
|
-
function managedFileContains(destination, marker, label) {
|
|
1437
|
-
if (!isFile(destination)) return false;
|
|
1438
|
-
try {
|
|
1439
|
-
return fs.readFileSync(destination, "utf8").includes(marker);
|
|
1440
|
-
} catch {
|
|
1441
|
-
throw new InstallerError(`cannot read ${label}: ${destination}`);
|
|
1442
|
-
}
|
|
1443
|
-
}
|
|
1444
|
-
|
|
1445
|
-
function isManagedGuardScript(destination) {
|
|
1446
|
-
return managedFileContains(destination, GUARD_MARKER, "guard script");
|
|
1447
|
-
}
|
|
1448
|
-
|
|
1449
|
-
function isManagedProjectRuntime(destination) {
|
|
1450
|
-
return managedFileContains(destination, PROJECT_RUNTIME_MARKER, "project runtime");
|
|
1451
|
-
}
|
|
1452
|
-
|
|
1453
|
-
function isManagedProjectStatusLauncher(destination) {
|
|
1454
|
-
return managedFileContains(
|
|
1455
|
-
destination,
|
|
1456
|
-
PROJECT_STATUS_MARKER,
|
|
1457
|
-
"project status launcher"
|
|
1458
|
-
);
|
|
1459
|
-
}
|
|
1460
|
-
|
|
1461
|
-
function isManagedGitHook(destination) {
|
|
1462
|
-
if (!isFile(destination)) return false;
|
|
1463
|
-
return isManagedGitHookContent(fs.readFileSync(destination, "utf8"));
|
|
1464
|
-
}
|
|
1465
|
-
|
|
1466
|
-
function isManagedGitHookContent(text) {
|
|
1467
|
-
return text.includes(GUARD_MARKER) && text.includes("git_guard.py");
|
|
1468
|
-
}
|
|
1469
|
-
|
|
1470
|
-
function isManagedHookSettings(destination) {
|
|
1471
|
-
if (!isFile(destination)) return false;
|
|
1472
|
-
try {
|
|
1473
|
-
return hasManagedHookSettings(parsedManagedSettings(destination));
|
|
1474
|
-
} catch {
|
|
1475
|
-
return false;
|
|
1476
|
-
}
|
|
1477
|
-
}
|
|
1478
|
-
|
|
1479
|
-
function hasManagedGuardWiring(project) {
|
|
1480
|
-
const settings = [
|
|
1481
|
-
path.join(project, ".claude", "settings.json"),
|
|
1482
|
-
path.join(project, ".codex", "hooks.json"),
|
|
1483
|
-
path.join(project, ".cursor", "hooks.json"),
|
|
1484
|
-
];
|
|
1485
|
-
if (settings.some(isManagedHookSettings)) return true;
|
|
1486
|
-
const hooksDir = gitHooksDirectory(project);
|
|
1487
|
-
return (
|
|
1488
|
-
hooksDir !== null &&
|
|
1489
|
-
GIT_HOOK_NAMES.some((name) => isManagedGitHook(path.join(hooksDir, name)))
|
|
1490
|
-
);
|
|
1491
|
-
}
|
|
1492
|
-
|
|
1493
|
-
function atomicTemporary(destination) {
|
|
1494
|
-
const directory = fs.mkdtempSync(
|
|
1495
|
-
path.join(path.dirname(destination), `.${path.basename(destination)}.gsd-path-tmp-`)
|
|
1496
|
-
);
|
|
1497
|
-
return { directory, temporary: path.join(directory, "value") };
|
|
1498
|
-
}
|
|
1499
|
-
|
|
1500
|
-
function writeFileAtomic(destination, content, mode) {
|
|
1501
|
-
const { directory, temporary } = atomicTemporary(destination);
|
|
1502
|
-
try {
|
|
1503
|
-
fs.writeFileSync(temporary, content, { flag: "wx" });
|
|
1504
|
-
if (mode !== undefined) {
|
|
1505
|
-
fs.chmodSync(temporary, mode);
|
|
1506
|
-
} else if (isFile(destination) && !isSymlink(destination)) {
|
|
1507
|
-
fs.chmodSync(temporary, fs.statSync(destination).mode & 0o777);
|
|
1508
|
-
}
|
|
1509
|
-
fs.renameSync(temporary, destination);
|
|
1510
|
-
} finally {
|
|
1511
|
-
removePath(directory);
|
|
1512
|
-
}
|
|
1513
|
-
}
|
|
1514
|
-
|
|
1515
|
-
function copyFileAtomic(source, destination) {
|
|
1516
|
-
const { directory, temporary } = atomicTemporary(destination);
|
|
1517
|
-
try {
|
|
1518
|
-
fs.copyFileSync(source, temporary, fs.constants.COPYFILE_EXCL);
|
|
1519
|
-
fs.renameSync(temporary, destination);
|
|
1520
|
-
} finally {
|
|
1521
|
-
removePath(directory);
|
|
1522
|
-
}
|
|
1523
|
-
}
|
|
1524
|
-
|
|
1525
|
-
// A PreToolUse entry is ours when one of its commands runs the guard hook.
|
|
1526
|
-
function isGuardCommand(command) {
|
|
1527
|
-
const normalized = command.replaceAll("\\", "/");
|
|
1528
|
-
const match = /^(?:python3|python)\s+(?:"([^"\r\n]+)"|'([^'\r\n]+)'|(\S+))$/.exec(
|
|
1529
|
-
normalized
|
|
1530
|
-
);
|
|
1531
|
-
if (match === null) return false;
|
|
1532
|
-
const script = match[1] || match[2] || match[3];
|
|
1533
|
-
const managedScript = `${HOOKS_DIRECTORY}/guard_hook.py`;
|
|
1534
|
-
return script === managedScript || script.endsWith(`/${managedScript}`);
|
|
1535
|
-
}
|
|
1536
|
-
|
|
1537
|
-
function isManagedCommandHook(hook) {
|
|
1538
|
-
return Boolean(
|
|
1539
|
-
hook &&
|
|
1540
|
-
typeof hook === "object" &&
|
|
1541
|
-
!Array.isArray(hook) &&
|
|
1542
|
-
typeof hook.command === "string" &&
|
|
1543
|
-
isGuardCommand(hook.command)
|
|
1544
|
-
);
|
|
1545
|
-
}
|
|
1546
|
-
|
|
1547
|
-
function isManagedHookEntry(entry) {
|
|
1548
|
-
return Boolean(
|
|
1549
|
-
entry &&
|
|
1550
|
-
typeof entry === "object" &&
|
|
1551
|
-
!Array.isArray(entry) &&
|
|
1552
|
-
Array.isArray(entry.hooks) &&
|
|
1553
|
-
entry.hooks.some(isManagedCommandHook)
|
|
1554
|
-
);
|
|
1555
|
-
}
|
|
1556
|
-
|
|
1557
|
-
function isManagedDirectHookEntry(entry) {
|
|
1558
|
-
return isManagedCommandHook(entry);
|
|
1559
|
-
}
|
|
1560
|
-
|
|
1561
|
-
function hasManagedHookSettings(parsed) {
|
|
1562
|
-
const hooks = parsed.hooks;
|
|
1563
|
-
if (hooks === null || typeof hooks !== "object" || Array.isArray(hooks)) return false;
|
|
1564
|
-
const nested = hooks.PreToolUse;
|
|
1565
|
-
const direct = hooks.preToolUse;
|
|
1566
|
-
return (
|
|
1567
|
-
(Array.isArray(nested) && nested.some(isManagedHookEntry)) ||
|
|
1568
|
-
(Array.isArray(direct) && direct.some(isManagedDirectHookEntry))
|
|
1569
|
-
);
|
|
1570
|
-
}
|
|
1571
|
-
|
|
1572
|
-
function parsedManagedSettings(settings) {
|
|
1573
|
-
let parsed;
|
|
1574
|
-
try {
|
|
1575
|
-
parsed = JSON.parse(fs.readFileSync(settings, "utf8"));
|
|
1576
|
-
} catch {
|
|
1577
|
-
throw new InstallerError(`managed hook settings file is not valid JSON: ${settings}`);
|
|
1578
|
-
}
|
|
1579
|
-
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
1580
|
-
throw new InstallerError(`managed hook settings file is not a JSON object: ${settings}`);
|
|
1581
|
-
}
|
|
1582
|
-
return parsed;
|
|
1583
|
-
}
|
|
1584
|
-
|
|
1585
|
-
function mergedHookSettings(settings, eventName, managedEntry, isManagedEntry) {
|
|
1586
|
-
const parsed = parsedManagedSettings(settings);
|
|
1587
|
-
const hooksObject =
|
|
1588
|
-
parsed.hooks && typeof parsed.hooks === "object" && !Array.isArray(parsed.hooks)
|
|
1589
|
-
? parsed.hooks
|
|
1590
|
-
: {};
|
|
1591
|
-
const existing = Array.isArray(hooksObject[eventName]) ? hooksObject[eventName] : [];
|
|
1592
|
-
const merged = [];
|
|
1593
|
-
let replaced = false;
|
|
1594
|
-
for (const entry of existing) {
|
|
1595
|
-
if (isManagedEntry(entry)) {
|
|
1596
|
-
if (!replaced) {
|
|
1597
|
-
merged.push(managedEntry);
|
|
1598
|
-
replaced = true;
|
|
1599
|
-
}
|
|
1600
|
-
} else {
|
|
1601
|
-
merged.push(entry);
|
|
1602
|
-
}
|
|
1603
|
-
}
|
|
1604
|
-
if (!replaced) merged.push(managedEntry);
|
|
1605
|
-
hooksObject[eventName] = merged;
|
|
1606
|
-
parsed.hooks = hooksObject;
|
|
1607
|
-
return JSON.stringify(parsed, null, 2) + "\n";
|
|
1608
|
-
}
|
|
1609
|
-
|
|
1610
|
-
function mergedNestedHookSettings(settings, managedEntry) {
|
|
1611
|
-
const parsed = parsedManagedSettings(settings);
|
|
1612
|
-
const hooksObject =
|
|
1613
|
-
parsed.hooks && typeof parsed.hooks === "object" && !Array.isArray(parsed.hooks)
|
|
1614
|
-
? parsed.hooks
|
|
1615
|
-
: {};
|
|
1616
|
-
const existing = Array.isArray(hooksObject.PreToolUse) ? hooksObject.PreToolUse : [];
|
|
1617
|
-
const merged = [];
|
|
1618
|
-
let replaced = false;
|
|
1619
|
-
for (const entry of existing) {
|
|
1620
|
-
if (!isManagedHookEntry(entry)) {
|
|
1621
|
-
merged.push(entry);
|
|
1622
|
-
continue;
|
|
1623
|
-
}
|
|
1624
|
-
const unrelatedHooks = entry.hooks.filter((hook) => !isManagedCommandHook(hook));
|
|
1625
|
-
if (!replaced) {
|
|
1626
|
-
merged.push(managedEntry);
|
|
1627
|
-
replaced = true;
|
|
1628
|
-
}
|
|
1629
|
-
if (unrelatedHooks.length > 0) {
|
|
1630
|
-
merged.push({ ...entry, hooks: unrelatedHooks });
|
|
1631
|
-
}
|
|
1632
|
-
}
|
|
1633
|
-
if (!replaced) merged.push(managedEntry);
|
|
1634
|
-
hooksObject.PreToolUse = merged;
|
|
1635
|
-
parsed.hooks = hooksObject;
|
|
1636
|
-
return JSON.stringify(parsed, null, 2) + "\n";
|
|
1637
|
-
}
|
|
1638
|
-
|
|
1639
|
-
function mergedClaudeSettings(settings, interpreter) {
|
|
1640
|
-
return mergedNestedHookSettings(settings, claudeGuardEntry(interpreter));
|
|
1641
|
-
}
|
|
1642
|
-
|
|
1643
|
-
function mergedCodexSettings(settings, interpreter) {
|
|
1644
|
-
const managedEntry = JSON.parse(codexHooksSettings(interpreter)).hooks.PreToolUse[0];
|
|
1645
|
-
return mergedNestedHookSettings(settings, managedEntry);
|
|
1646
|
-
}
|
|
1647
|
-
|
|
1648
|
-
function mergedCursorSettings(settings, interpreter) {
|
|
1649
|
-
const managedEntry = JSON.parse(cursorHooksSettings(interpreter)).hooks.preToolUse[0];
|
|
1650
|
-
return mergedHookSettings(settings, "preToolUse", managedEntry, isManagedDirectHookEntry);
|
|
1651
|
-
}
|
|
1652
|
-
|
|
1653
|
-
function refreshesGuards(project, full, initialize) {
|
|
1654
|
-
return (
|
|
1655
|
-
full ||
|
|
1656
|
-
initialize ||
|
|
1657
|
-
hasManagedGuardWiring(project) ||
|
|
1658
|
-
GUARD_SCRIPTS.some((name) => lexists(path.join(project, HOOKS_DIRECTORY, name)))
|
|
1659
|
-
);
|
|
1660
|
-
}
|
|
1661
|
-
|
|
1662
|
-
function contractSection(content, heading) {
|
|
1663
|
-
const normalized = content.replaceAll("\r\n", "\n");
|
|
1664
|
-
const startPattern = new RegExp(`^${heading.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}$`, "m");
|
|
1665
|
-
const startMatch = startPattern.exec(normalized);
|
|
1666
|
-
if (startMatch === null) return null;
|
|
1667
|
-
const depth = heading.indexOf(" ");
|
|
1668
|
-
const afterHeading = normalized.indexOf("\n", startMatch.index);
|
|
1669
|
-
const bodyStart = afterHeading === -1 ? normalized.length : afterHeading + 1;
|
|
1670
|
-
const nextHeading = new RegExp(`^#{1,${depth}}\\s`, "m").exec(normalized.slice(bodyStart));
|
|
1671
|
-
const end = nextHeading === null ? normalized.length : bodyStart + nextHeading.index;
|
|
1672
|
-
return normalized.slice(startMatch.index, end).trimEnd();
|
|
1673
|
-
}
|
|
1674
|
-
|
|
1675
|
-
function hasLegacyProjectContracts(sourceRoot, project) {
|
|
1676
|
-
return PROJECT_CONTRACTS.every(([name, heading]) => {
|
|
1677
|
-
const candidate = path.join(project, name);
|
|
1678
|
-
const source = path.join(sourceRoot, name);
|
|
1679
|
-
if (
|
|
1680
|
-
isSymlink(candidate) ||
|
|
1681
|
-
!isFile(candidate) ||
|
|
1682
|
-
isSymlink(source) ||
|
|
1683
|
-
!isFile(source)
|
|
1684
|
-
) return false;
|
|
1685
|
-
const installed = contractSection(fs.readFileSync(candidate, "utf8"), heading);
|
|
1686
|
-
const canonical = contractSection(fs.readFileSync(source, "utf8"), heading);
|
|
1687
|
-
return installed !== null && installed === canonical;
|
|
1688
|
-
});
|
|
1689
|
-
}
|
|
1690
|
-
|
|
1691
|
-
function validateHooksRefresh(sourceRoot, project, full, hooksDir, selected, initialize = false) {
|
|
1692
|
-
validateDirectoryDestination(project, "project path");
|
|
1693
|
-
validateProjectGitRoot(project);
|
|
1694
|
-
validateDirectoryDestination(
|
|
1695
|
-
path.join(project, HOOKS_DIRECTORY),
|
|
1696
|
-
"guard hooks directory"
|
|
1697
|
-
);
|
|
1698
|
-
validateDirectoryDestination(
|
|
1699
|
-
path.join(project, HOOKS_DIRECTORY, "runtime"),
|
|
1700
|
-
"project runtime directory"
|
|
1701
|
-
);
|
|
1702
|
-
const refreshGuards = refreshesGuards(project, full, initialize);
|
|
1703
|
-
const runtimeExists = PROJECT_RUNTIME_SCRIPTS.some((name) =>
|
|
1704
|
-
lexists(path.join(project, HOOKS_DIRECTORY, "runtime", name))
|
|
1705
|
-
);
|
|
1706
|
-
if (
|
|
1707
|
-
!initialize &&
|
|
1708
|
-
!refreshGuards &&
|
|
1709
|
-
!runtimeExists &&
|
|
1710
|
-
!hasLegacyProjectContracts(sourceRoot, project)
|
|
1711
|
-
) {
|
|
1712
|
-
throw new InstallerError(
|
|
1713
|
-
`no managed GSD Path hooks or runtime found in project: ${project}`
|
|
1714
|
-
);
|
|
1715
|
-
}
|
|
1716
|
-
const runtime = path.join(project, HOOKS_DIRECTORY, "runtime");
|
|
1717
|
-
const launcher = path.join(project, HOOKS_DIRECTORY, PROJECT_STATUS_LAUNCHER);
|
|
1718
|
-
const launcherSource = path.join(sourceRoot, "scripts", PROJECT_STATUS_LAUNCHER);
|
|
1719
|
-
if (isSymlink(launcher) || (lexists(launcher) && !isManagedProjectStatusLauncher(launcher))) {
|
|
1720
|
-
throw new InstallerError(`not a managed GSD Path status launcher: ${launcher}`);
|
|
1721
|
-
}
|
|
1722
|
-
if (isSymlink(launcherSource) || !isFile(launcherSource)) {
|
|
1723
|
-
throw new InstallerError(`missing project status launcher source: ${launcherSource}`);
|
|
1724
|
-
}
|
|
1725
|
-
if (isDirectory(runtime)) {
|
|
1726
|
-
const unexpected = fs
|
|
1727
|
-
.readdirSync(runtime)
|
|
1728
|
-
.filter((name) => !PROJECT_RUNTIME_SCRIPTS.includes(name));
|
|
1729
|
-
if (unexpected.length) {
|
|
1730
|
-
throw new InstallerError(`unexpected project runtime entries: ${unexpected.join(", ")}`);
|
|
1731
|
-
}
|
|
1732
|
-
}
|
|
1733
|
-
if (refreshGuards) {
|
|
1734
|
-
for (const name of GUARD_SCRIPTS) {
|
|
1735
|
-
const destination = path.join(project, HOOKS_DIRECTORY, name);
|
|
1736
|
-
const exists = lexists(destination);
|
|
1737
|
-
if (isSymlink(destination)) {
|
|
1738
|
-
throw new InstallerError(`refusing to refresh a symlink: ${destination}`);
|
|
1739
|
-
}
|
|
1740
|
-
if (exists && !isManagedGuardScript(destination)) {
|
|
1741
|
-
throw new InstallerError(`not a managed GSD Path guard script: ${destination}`);
|
|
1742
|
-
}
|
|
1743
|
-
const source = path.join(sourceRoot, "scripts", name);
|
|
1744
|
-
if (isSymlink(source) || !isFile(source)) {
|
|
1745
|
-
throw new InstallerError(`missing guard script source: ${source}`);
|
|
1746
|
-
}
|
|
1747
|
-
}
|
|
1748
|
-
}
|
|
1749
|
-
for (const name of PROJECT_RUNTIME_SCRIPTS) {
|
|
1750
|
-
const destination = path.join(project, HOOKS_DIRECTORY, "runtime", name);
|
|
1751
|
-
if (isSymlink(destination)) {
|
|
1752
|
-
throw new InstallerError(`refusing to refresh a symlink: ${destination}`);
|
|
1753
|
-
}
|
|
1754
|
-
if (lexists(destination) && !isManagedProjectRuntime(destination)) {
|
|
1755
|
-
throw new InstallerError(`not a managed GSD Path project runtime: ${destination}`);
|
|
1756
|
-
}
|
|
1757
|
-
const source = path.join(sourceRoot, "scripts", name);
|
|
1758
|
-
if (isSymlink(source) || !isFile(source)) {
|
|
1759
|
-
throw new InstallerError(`missing project runtime source: ${source}`);
|
|
1760
|
-
}
|
|
1761
|
-
}
|
|
1762
|
-
if (full) {
|
|
1763
|
-
for (const [target, label, settings] of [
|
|
1764
|
-
["claude", "Claude", path.join(project, ".claude", "settings.json")],
|
|
1765
|
-
["codex", "Codex", path.join(project, ".codex", "hooks.json")],
|
|
1766
|
-
["cursor", "Cursor", path.join(project, ".cursor", "hooks.json")],
|
|
1767
|
-
]) {
|
|
1768
|
-
const exists = lexists(settings);
|
|
1769
|
-
if (initialize && !selected.includes(target)) continue;
|
|
1770
|
-
if (!exists && !selected.includes(target)) continue;
|
|
1771
|
-
validateDirectoryDestination(
|
|
1772
|
-
path.dirname(settings),
|
|
1773
|
-
`unsafe ${label} project directory`
|
|
1774
|
-
);
|
|
1775
|
-
if (isSymlink(settings)) {
|
|
1776
|
-
throw new InstallerError(`refusing to refresh a symlink: ${settings}`);
|
|
1777
|
-
}
|
|
1778
|
-
if (exists) {
|
|
1779
|
-
const parsed = parsedManagedSettings(settings);
|
|
1780
|
-
if (!selected.includes(target) && !hasManagedHookSettings(parsed)) {
|
|
1781
|
-
throw new InstallerError(`not a managed GSD Path hook settings file: ${settings}`);
|
|
1782
|
-
}
|
|
1783
|
-
}
|
|
1784
|
-
}
|
|
1785
|
-
if (hooksDir !== null) {
|
|
1786
|
-
for (const hookName of GIT_HOOK_NAMES) {
|
|
1787
|
-
const hookPath = path.join(hooksDir, hookName);
|
|
1788
|
-
if (isSymlink(hookPath)) {
|
|
1789
|
-
throw new InstallerError(`refusing to refresh a symlink: ${hookPath}`);
|
|
1790
|
-
}
|
|
1791
|
-
if (lexists(hookPath) && !isManagedGitHook(hookPath)) {
|
|
1792
|
-
throw new InstallerError(`not a managed GSD Path git hook: ${hookPath}`);
|
|
1793
|
-
}
|
|
1794
|
-
}
|
|
1795
|
-
}
|
|
1796
|
-
}
|
|
1797
|
-
}
|
|
1798
|
-
|
|
1799
|
-
function publishProjectRuntime(sourceRoot, project, refreshGuards) {
|
|
1800
|
-
const parent = path.join(project, HOOKS_DIRECTORY);
|
|
1801
|
-
const runtime = path.join(parent, "runtime");
|
|
1802
|
-
fs.mkdirSync(parent, { recursive: true });
|
|
1803
|
-
const staging = fs.mkdtempSync(path.join(parent, ".runtime-stage-"));
|
|
1804
|
-
const previous = `${staging}-previous`;
|
|
1805
|
-
let movedPrevious = false;
|
|
1806
|
-
let publishedRuntime = false;
|
|
1807
|
-
const launcher = path.join(parent, PROJECT_STATUS_LAUNCHER);
|
|
1808
|
-
const launcherOriginal = lexists(launcher) ? fs.readFileSync(launcher) : null;
|
|
1809
|
-
const launcherMode = launcherOriginal === null ? undefined : fs.statSync(launcher).mode & 0o777;
|
|
1810
|
-
const guardOriginals = [];
|
|
1811
|
-
try {
|
|
1812
|
-
for (const name of PROJECT_RUNTIME_SCRIPTS) {
|
|
1813
|
-
hooks.copyRuntimeFile(
|
|
1814
|
-
path.join(sourceRoot, "scripts", name),
|
|
1815
|
-
path.join(staging, name)
|
|
1816
|
-
);
|
|
1817
|
-
}
|
|
1818
|
-
copyFileAtomic(path.join(sourceRoot, "scripts", PROJECT_STATUS_LAUNCHER), launcher);
|
|
1819
|
-
if (lexists(runtime)) {
|
|
1820
|
-
fs.renameSync(runtime, previous);
|
|
1821
|
-
movedPrevious = true;
|
|
1822
|
-
}
|
|
1823
|
-
fs.renameSync(staging, runtime);
|
|
1824
|
-
publishedRuntime = true;
|
|
1825
|
-
if (refreshGuards) {
|
|
1826
|
-
for (const name of GUARD_SCRIPTS) {
|
|
1827
|
-
const destination = path.join(parent, name);
|
|
1828
|
-
const original = lexists(destination) ? fs.readFileSync(destination) : null;
|
|
1829
|
-
const mode =
|
|
1830
|
-
original === null ? undefined : fs.statSync(destination).mode & 0o777;
|
|
1831
|
-
guardOriginals.push([destination, original, mode]);
|
|
1832
|
-
hooks.copyGuardFile(path.join(sourceRoot, "scripts", name), destination);
|
|
1833
|
-
}
|
|
1834
|
-
}
|
|
1835
|
-
if (movedPrevious) removePath(previous);
|
|
1836
|
-
} catch (error) {
|
|
1837
|
-
for (const [destination, original, mode] of guardOriginals.reverse()) {
|
|
1838
|
-
if (original === null) removePath(destination);
|
|
1839
|
-
else writeFileAtomic(destination, original, mode);
|
|
1840
|
-
}
|
|
1841
|
-
if (launcherOriginal === null) removePath(launcher);
|
|
1842
|
-
else writeFileAtomic(launcher, launcherOriginal, launcherMode);
|
|
1843
|
-
if (publishedRuntime && lexists(runtime)) removePath(runtime);
|
|
1844
|
-
if (!lexists(runtime) && movedPrevious && lexists(previous)) {
|
|
1845
|
-
fs.renameSync(previous, runtime);
|
|
1846
|
-
}
|
|
1847
|
-
throw new InstallerError(`project runtime refresh failed: ${error.message}`);
|
|
1848
|
-
} finally {
|
|
1849
|
-
removePath(staging);
|
|
1850
|
-
}
|
|
1851
|
-
}
|
|
1852
|
-
|
|
1853
|
-
// Returns refreshed project-relative paths; entries prefixed "note:" are
|
|
1854
|
-
// user-facing notes rather than refreshed files.
|
|
1855
|
-
function refreshHooksUnlocked(sourceRoot, project, full, dryRun, selected, initialize) {
|
|
1856
|
-
let hooksDir = full ? gitHooksDirectory(project) : null;
|
|
1857
|
-
let interpreter = null;
|
|
1858
|
-
if (!dryRun) {
|
|
1859
|
-
if (full) {
|
|
1860
|
-
({ interpreter, hooksDir } = requiredHookRuntime(
|
|
1861
|
-
project,
|
|
1862
|
-
initialize ? "--hooks-init" : "--hooks-refresh-full",
|
|
1863
|
-
selected
|
|
1864
|
-
));
|
|
1865
|
-
} else {
|
|
1866
|
-
interpreter = requiredPythonRuntime("--hooks-refresh", selected);
|
|
1867
|
-
}
|
|
1868
|
-
}
|
|
1869
|
-
validateHooksRefresh(sourceRoot, project, full, hooksDir, selected, initialize);
|
|
1870
|
-
const refreshed = [];
|
|
1871
|
-
const refreshGuards = refreshesGuards(project, full, initialize);
|
|
1872
|
-
if (!dryRun) publishProjectRuntime(sourceRoot, project, refreshGuards);
|
|
1873
|
-
for (const name of PROJECT_RUNTIME_SCRIPTS) {
|
|
1874
|
-
refreshed.push(
|
|
1875
|
-
describeProjectPath(project, path.join(project, HOOKS_DIRECTORY, "runtime", name))
|
|
1876
|
-
);
|
|
1877
|
-
}
|
|
1878
|
-
refreshed.push(
|
|
1879
|
-
describeProjectPath(
|
|
1880
|
-
project,
|
|
1881
|
-
path.join(project, HOOKS_DIRECTORY, PROJECT_STATUS_LAUNCHER)
|
|
1882
|
-
)
|
|
1883
|
-
);
|
|
1884
|
-
if (refreshGuards) {
|
|
1885
|
-
for (const name of GUARD_SCRIPTS) {
|
|
1886
|
-
const destination = path.join(project, HOOKS_DIRECTORY, name);
|
|
1887
|
-
refreshed.push(describeProjectPath(project, destination));
|
|
1888
|
-
}
|
|
1889
|
-
}
|
|
1890
|
-
if (full) {
|
|
1891
|
-
for (const [target, settings, merge, generated] of [
|
|
1892
|
-
[
|
|
1893
|
-
"claude",
|
|
1894
|
-
path.join(project, ".claude", "settings.json"),
|
|
1895
|
-
mergedClaudeSettings,
|
|
1896
|
-
claudeHooksSettings,
|
|
1897
|
-
],
|
|
1898
|
-
[
|
|
1899
|
-
"codex",
|
|
1900
|
-
path.join(project, ".codex", "hooks.json"),
|
|
1901
|
-
mergedCodexSettings,
|
|
1902
|
-
codexHooksSettings,
|
|
1903
|
-
],
|
|
1904
|
-
[
|
|
1905
|
-
"cursor",
|
|
1906
|
-
path.join(project, ".cursor", "hooks.json"),
|
|
1907
|
-
mergedCursorSettings,
|
|
1908
|
-
cursorHooksSettings,
|
|
1909
|
-
],
|
|
1910
|
-
]) {
|
|
1911
|
-
if (initialize && !selected.includes(target)) continue;
|
|
1912
|
-
const exists = lexists(settings);
|
|
1913
|
-
if (exists || selected.includes(target)) {
|
|
1914
|
-
if (!dryRun) {
|
|
1915
|
-
fs.mkdirSync(path.dirname(settings), { recursive: true });
|
|
1916
|
-
const content = exists ? merge(settings, interpreter) : generated(interpreter);
|
|
1917
|
-
writeFileAtomic(settings, content);
|
|
1918
|
-
}
|
|
1919
|
-
refreshed.push(describeProjectPath(project, settings));
|
|
1920
|
-
}
|
|
1921
|
-
}
|
|
1922
|
-
if (hooksDir !== null) {
|
|
1923
|
-
for (const [hookName, generator] of GIT_HOOKS) {
|
|
1924
|
-
const hookPath = path.join(hooksDir, hookName);
|
|
1925
|
-
if (!dryRun) {
|
|
1926
|
-
const content = generator(interpreter);
|
|
1927
|
-
fs.mkdirSync(path.dirname(hookPath), { recursive: true });
|
|
1928
|
-
writeFileAtomic(hookPath, content, 0o755);
|
|
1929
|
-
}
|
|
1930
|
-
refreshed.push(describeProjectPath(project, hookPath));
|
|
1931
|
-
}
|
|
1932
|
-
}
|
|
1933
|
-
}
|
|
1934
|
-
return refreshed;
|
|
1935
|
-
}
|
|
1936
|
-
|
|
1937
1108
|
function refreshHooks(sourceRoot, project, full, dryRun, selected = [], initialize = false) {
|
|
1938
|
-
|
|
1939
|
-
return refreshHooksUnlocked(sourceRoot, project, full, true, selected, initialize);
|
|
1940
|
-
}
|
|
1941
|
-
validateDirectoryDestination(project, "project path");
|
|
1942
|
-
const ownership = acquireInstallLocks([path.join(project, HOOKS_DIRECTORY)]);
|
|
1943
|
-
try {
|
|
1944
|
-
return refreshHooksUnlocked(sourceRoot, project, full, false, selected, initialize);
|
|
1945
|
-
} finally {
|
|
1946
|
-
releaseInstallLocks(ownership.locks, ownership.createdDirectories);
|
|
1947
|
-
}
|
|
1109
|
+
return hooks.projectAdapter(sourceRoot, project, "refresh", { full, dryRun, selected, initialize });
|
|
1948
1110
|
}
|
|
1949
1111
|
|
|
1950
1112
|
const STATE_SCHEMA = "gsd-path/state/v1";
|
|
1951
1113
|
|
|
1952
|
-
function validatedProjectState(sourceRoot, project) {
|
|
1953
|
-
const validator = path.join(sourceRoot, "scripts", "pipeline_state.py");
|
|
1954
|
-
if (isSymlink(validator) || !isFile(validator)) {
|
|
1955
|
-
throw new InstallerError(`canonical state validator is unavailable: ${validator}`);
|
|
1956
|
-
}
|
|
1957
|
-
const interpreter = effectiveInterpreter();
|
|
1958
|
-
if (interpreter === null) {
|
|
1959
|
-
throw new InstallerError("canonical state validation requires a working Python interpreter");
|
|
1960
|
-
}
|
|
1961
|
-
const result = spawnSync(
|
|
1962
|
-
interpreter,
|
|
1963
|
-
["-B", validator, "validate", "--repo", project],
|
|
1964
|
-
{ cwd: project, encoding: "utf8" }
|
|
1965
|
-
);
|
|
1966
|
-
if (result.error || result.status !== 0) {
|
|
1967
|
-
const detail =
|
|
1968
|
-
result.error?.message ||
|
|
1969
|
-
(result.stderr || "").trim() ||
|
|
1970
|
-
(result.stdout || "").trim() ||
|
|
1971
|
-
"unknown failure";
|
|
1972
|
-
throw new InstallerError(`canonical state validation failed: ${detail}`);
|
|
1973
|
-
}
|
|
1974
|
-
let payload;
|
|
1975
|
-
try {
|
|
1976
|
-
payload = JSON.parse(result.stdout);
|
|
1977
|
-
} catch {
|
|
1978
|
-
throw new InstallerError("canonical state validator returned invalid JSON");
|
|
1979
|
-
}
|
|
1980
|
-
if (!payload || typeof payload !== "object") {
|
|
1981
|
-
throw new InstallerError("canonical state validator returned an invalid payload");
|
|
1982
|
-
}
|
|
1983
|
-
const state = payload.state;
|
|
1984
|
-
if (
|
|
1985
|
-
payload.schema !== STATE_SCHEMA ||
|
|
1986
|
-
payload.status !== "valid" ||
|
|
1987
|
-
!state ||
|
|
1988
|
-
typeof state !== "object" ||
|
|
1989
|
-
typeof state.phase !== "string" ||
|
|
1990
|
-
typeof state.status !== "string"
|
|
1991
|
-
) {
|
|
1992
|
-
throw new InstallerError("canonical state validator returned an invalid payload");
|
|
1993
|
-
}
|
|
1994
|
-
return state;
|
|
1995
|
-
}
|
|
1996
|
-
|
|
1997
1114
|
function hasManagedInstall(root) {
|
|
1998
1115
|
return (
|
|
1999
1116
|
isDirectory(root) &&
|
|
@@ -2001,190 +1118,16 @@ function hasManagedInstall(root) {
|
|
|
2001
1118
|
);
|
|
2002
1119
|
}
|
|
2003
1120
|
|
|
2004
|
-
function validateProjectRuntimeStatus(sourceRoot, project) {
|
|
2005
|
-
const interpreter = requiredPythonRuntime("--doctor");
|
|
2006
|
-
const runtime = path.join(sourceRoot, "scripts", "pipeline_state.py");
|
|
2007
|
-
const result = spawnSync(
|
|
2008
|
-
interpreter,
|
|
2009
|
-
["-B", runtime, "status", "--repo", project],
|
|
2010
|
-
{
|
|
2011
|
-
cwd: project,
|
|
2012
|
-
encoding: "utf8",
|
|
2013
|
-
env: { ...process.env, GIT_OPTIONAL_LOCKS: "0" },
|
|
2014
|
-
}
|
|
2015
|
-
);
|
|
2016
|
-
if (result.error || result.status !== 0) {
|
|
2017
|
-
const detail = result.error?.message || result.stderr?.trim() || result.stdout?.trim() || "unknown failure";
|
|
2018
|
-
throw new InstallerError(`project runtime status failed: ${detail}`);
|
|
2019
|
-
}
|
|
2020
|
-
let payload;
|
|
2021
|
-
try {
|
|
2022
|
-
payload = JSON.parse(result.stdout);
|
|
2023
|
-
} catch {
|
|
2024
|
-
throw new InstallerError("project runtime status returned invalid JSON");
|
|
2025
|
-
}
|
|
2026
|
-
if (!validStatusPayload(payload, project)) {
|
|
2027
|
-
throw new InstallerError("project runtime status returned an invalid payload");
|
|
2028
|
-
}
|
|
2029
|
-
}
|
|
2030
|
-
|
|
2031
|
-
function projectRuntimeMatches(sourceRoot, project) {
|
|
2032
|
-
const pairs = [
|
|
2033
|
-
[
|
|
2034
|
-
path.join(project, HOOKS_DIRECTORY, PROJECT_STATUS_LAUNCHER),
|
|
2035
|
-
path.join(sourceRoot, "scripts", PROJECT_STATUS_LAUNCHER),
|
|
2036
|
-
],
|
|
2037
|
-
...PROJECT_RUNTIME_SCRIPTS.map((name) => [
|
|
2038
|
-
path.join(project, HOOKS_DIRECTORY, "runtime", name),
|
|
2039
|
-
path.join(sourceRoot, "scripts", name),
|
|
2040
|
-
]),
|
|
2041
|
-
];
|
|
2042
|
-
try {
|
|
2043
|
-
return pairs.every(
|
|
2044
|
-
([destination, source]) =>
|
|
2045
|
-
!isSymlink(destination) &&
|
|
2046
|
-
isFile(destination) &&
|
|
2047
|
-
fs.readFileSync(destination).equals(fs.readFileSync(source))
|
|
2048
|
-
);
|
|
2049
|
-
} catch {
|
|
2050
|
-
return false;
|
|
2051
|
-
}
|
|
2052
|
-
}
|
|
2053
|
-
|
|
2054
|
-
function validStatusPayload(payload, project) {
|
|
2055
|
-
if (!payload || typeof payload !== "object" || Array.isArray(payload)) return false;
|
|
2056
|
-
const state = payload.state;
|
|
2057
|
-
const route = payload.route;
|
|
2058
|
-
if (
|
|
2059
|
-
payload.schema !== "gsd-path/status/v1" ||
|
|
2060
|
-
payload.advance !== false ||
|
|
2061
|
-
!state ||
|
|
2062
|
-
typeof state !== "object" ||
|
|
2063
|
-
!validStatusState(state) ||
|
|
2064
|
-
!route ||
|
|
2065
|
-
typeof route !== "object" ||
|
|
2066
|
-
!STATUS_ACTIONS.has(route.action) ||
|
|
2067
|
-
typeof route.reason !== "string" ||
|
|
2068
|
-
!route.reason ||
|
|
2069
|
-
typeof payload.path !== "string" ||
|
|
2070
|
-
!path.isAbsolute(payload.path) ||
|
|
2071
|
-
!samePath(payload.path, path.join(project, ".project", "STATE.md"))
|
|
2072
|
-
) {
|
|
2073
|
-
return false;
|
|
2074
|
-
}
|
|
2075
|
-
let expectedNextSkill = null;
|
|
2076
|
-
if (route.action === "run-phase") {
|
|
2077
|
-
if (
|
|
2078
|
-
typeof route.phase !== "string" ||
|
|
2079
|
-
!STATUS_TRANSITIONS.get(state.phase).has(route.phase) ||
|
|
2080
|
-
state.branch === null
|
|
2081
|
-
) return false;
|
|
2082
|
-
expectedNextSkill = `gsd-path-${route.phase}`;
|
|
2083
|
-
} else if (Object.hasOwn(route, "phase")) {
|
|
2084
|
-
return false;
|
|
2085
|
-
} else if (route.action === "bind-initial") {
|
|
2086
|
-
return (
|
|
2087
|
-
state.branch === null &&
|
|
2088
|
-
typeof route.branch === "string" &&
|
|
2089
|
-
validStatusBranch(route.branch) !== null &&
|
|
2090
|
-
payload.next_skill === "gsd-path"
|
|
2091
|
-
);
|
|
2092
|
-
} else if (route.action === "resume-undo") {
|
|
2093
|
-
expectedNextSkill = "gsd-path-undo";
|
|
2094
|
-
} else if (route.action !== "wait") {
|
|
2095
|
-
expectedNextSkill = "gsd-path";
|
|
2096
|
-
}
|
|
2097
|
-
if (state.branch === null) return false;
|
|
2098
|
-
if (route.action === "validate-integrated" && !(state.phase === "shipped" && state.status === "done")) return false;
|
|
2099
|
-
if (route.action === "wait" && !(state.phase === "plan" && state.status === "done")) return false;
|
|
2100
|
-
return payload.next_skill === expectedNextSkill;
|
|
2101
|
-
}
|
|
2102
|
-
|
|
2103
|
-
function validStatusState(state) {
|
|
2104
|
-
if (Object.keys(state).sort().join("\0") !== STATUS_STATE_FIELDS.join("\0")) return false;
|
|
2105
|
-
const archiveMatch = typeof state.archive === "string" ? STATUS_ARCHIVE.exec(state.archive) : null;
|
|
2106
|
-
if (
|
|
2107
|
-
state.pipeline !== "gsd-path/v2" ||
|
|
2108
|
-
typeof state.project !== "string" ||
|
|
2109
|
-
!STATUS_SLUG.test(state.project) ||
|
|
2110
|
-
!(state.milestone === null || typeof state.milestone === "string" && STATUS_SLUG.test(state.milestone)) ||
|
|
2111
|
-
!STATUS_PHASES.has(state.phase) ||
|
|
2112
|
-
!STATUS_VALUES.has(state.status) ||
|
|
2113
|
-
!STATUS_INTEGRATION_MODES.has(state.integration_default) ||
|
|
2114
|
-
!STATUS_INTEGRATION_MODES.has(state.integration) ||
|
|
2115
|
-
!STATUS_INTEGRATION_SOURCES.has(state.integration_source) ||
|
|
2116
|
-
(state.integration_source === "default" &&
|
|
2117
|
-
state.integration !== state.integration_default) ||
|
|
2118
|
-
!(state.branch === null || validStatusBranch(state.branch) !== null) ||
|
|
2119
|
-
!(state.archive === null || archiveMatch)
|
|
2120
|
-
) return false;
|
|
2121
|
-
if (state.phase === "shipped" && (state.status !== "done" || state.archive === null)) return false;
|
|
2122
|
-
if (
|
|
2123
|
-
state.archive !== null &&
|
|
2124
|
-
state.phase !== "build" &&
|
|
2125
|
-
state.phase !== "ship" &&
|
|
2126
|
-
state.phase !== "shipped"
|
|
2127
|
-
) return false;
|
|
2128
|
-
if ((state.phase === "ship" || state.phase === "shipped") && state.branch === null) return false;
|
|
2129
|
-
if (archiveMatch) {
|
|
2130
|
-
const branchMatch = validStatusBranch(state.branch);
|
|
2131
|
-
return state.milestone === archiveMatch[2] && branchMatch !== null && Number(branchMatch[1]) === Number(archiveMatch[1]);
|
|
2132
|
-
}
|
|
2133
|
-
return true;
|
|
2134
|
-
}
|
|
2135
|
-
|
|
2136
|
-
function validStatusBranch(value) {
|
|
2137
|
-
if (typeof value !== "string") return null;
|
|
2138
|
-
const match = STATUS_BRANCH.exec(value);
|
|
2139
|
-
return match !== null && Number(match[1]) >= 1 ? match : null;
|
|
2140
|
-
}
|
|
2141
|
-
|
|
2142
|
-
function isExecutable(candidate) {
|
|
2143
|
-
try {
|
|
2144
|
-
return (fs.statSync(candidate).mode & 0o111) !== 0;
|
|
2145
|
-
} catch {
|
|
2146
|
-
return false;
|
|
2147
|
-
}
|
|
2148
|
-
}
|
|
2149
|
-
|
|
2150
|
-
function nativeGuardContract(target, project) {
|
|
2151
|
-
if (target === "claude") {
|
|
2152
|
-
return {
|
|
2153
|
-
settings: path.join(project, ".claude", "settings.json"),
|
|
2154
|
-
event: "PreToolUse",
|
|
2155
|
-
entry: claudeGuardEntry,
|
|
2156
|
-
};
|
|
2157
|
-
}
|
|
2158
|
-
if (target === "codex") {
|
|
2159
|
-
return {
|
|
2160
|
-
settings: path.join(project, ".codex", "hooks.json"),
|
|
2161
|
-
event: "PreToolUse",
|
|
2162
|
-
entry: codexGuardEntry,
|
|
2163
|
-
};
|
|
2164
|
-
}
|
|
2165
|
-
if (target === "cursor") {
|
|
2166
|
-
return {
|
|
2167
|
-
settings: path.join(project, ".cursor", "hooks.json"),
|
|
2168
|
-
event: "preToolUse",
|
|
2169
|
-
entry: cursorGuardEntry,
|
|
2170
|
-
};
|
|
2171
|
-
}
|
|
2172
|
-
return null;
|
|
2173
|
-
}
|
|
2174
|
-
|
|
2175
1121
|
// Read-only health check: host installs, project contracts, guard hooks,
|
|
2176
1122
|
// and pipeline state. Never writes.
|
|
2177
1123
|
export function doctor(sourceRoot, { targets, rootFor, project = null }) {
|
|
1124
|
+
if (project !== null) {
|
|
1125
|
+
return hooks.projectAdapter(sourceRoot, project, "doctor", {
|
|
1126
|
+
targets, roots: Object.fromEntries(targets.map(target => [target, rootFor(target)])),
|
|
1127
|
+
});
|
|
1128
|
+
}
|
|
2178
1129
|
const findings = [];
|
|
2179
1130
|
const push = (level, text) => findings.push({ level, text });
|
|
2180
|
-
function readProjectFile(candidate, label) {
|
|
2181
|
-
try {
|
|
2182
|
-
return fs.readFileSync(candidate);
|
|
2183
|
-
} catch (error) {
|
|
2184
|
-
push("fail", `${label} cannot be read: ${error.message}`);
|
|
2185
|
-
return null;
|
|
2186
|
-
}
|
|
2187
|
-
}
|
|
2188
1131
|
const version = readPackageVersion(path.join(sourceRoot, "package.json"));
|
|
2189
1132
|
if (version === null) push("fail", "package: version cannot be read");
|
|
2190
1133
|
|
|
@@ -2230,314 +1173,6 @@ export function doctor(sourceRoot, { targets, rootFor, project = null }) {
|
|
|
2230
1173
|
}
|
|
2231
1174
|
}
|
|
2232
1175
|
|
|
2233
|
-
if (project === null) return findings;
|
|
2234
|
-
|
|
2235
|
-
for (const [name, heading] of PROJECT_CONTRACTS) {
|
|
2236
|
-
const contract = path.join(project, name);
|
|
2237
|
-
if (isSymlink(contract)) {
|
|
2238
|
-
push("fail", `project: contract ${name} is a symlink`);
|
|
2239
|
-
continue;
|
|
2240
|
-
}
|
|
2241
|
-
if (!isFile(contract)) {
|
|
2242
|
-
push("fail", `project: missing contract ${name} — run --project "${project}"`);
|
|
2243
|
-
continue;
|
|
2244
|
-
}
|
|
2245
|
-
const content = readProjectFile(contract, `project: ${name}`);
|
|
2246
|
-
if (content === null) continue;
|
|
2247
|
-
const canonical = readProjectFile(
|
|
2248
|
-
path.join(sourceRoot, name),
|
|
2249
|
-
`package: ${name}`
|
|
2250
|
-
);
|
|
2251
|
-
if (canonical === null) continue;
|
|
2252
|
-
const installedSection = contractSection(content.toString("utf8"), heading);
|
|
2253
|
-
const canonicalSection = contractSection(canonical.toString("utf8"), heading);
|
|
2254
|
-
if (installedSection !== null && installedSection === canonicalSection) {
|
|
2255
|
-
push("ok", `project: ${name} present`);
|
|
2256
|
-
} else {
|
|
2257
|
-
push("fail", `project: ${name} lacks plain-prompt re-entry — merge the current contract`);
|
|
2258
|
-
}
|
|
2259
|
-
}
|
|
2260
|
-
const bridge = path.join(project, ".claude", "CLAUDE.md");
|
|
2261
|
-
if (!isFile(bridge)) {
|
|
2262
|
-
push("note", "project: no .claude/CLAUDE.md bridge (only written for --claude installs)");
|
|
2263
|
-
} else {
|
|
2264
|
-
const bridgeContent = readProjectFile(bridge, "project: .claude/CLAUDE.md");
|
|
2265
|
-
if (bridgeContent !== null) {
|
|
2266
|
-
if (bridgeContent.toString("utf8") === CLAUDE_BRIDGE) {
|
|
2267
|
-
push("ok", "project: .claude/CLAUDE.md bridge present");
|
|
2268
|
-
} else {
|
|
2269
|
-
push("note", "project: .claude/CLAUDE.md exists but is not the managed bridge");
|
|
2270
|
-
}
|
|
2271
|
-
}
|
|
2272
|
-
}
|
|
2273
|
-
|
|
2274
|
-
const runtime = path.join(project, HOOKS_DIRECTORY, "runtime");
|
|
2275
|
-
const launcher = path.join(project, HOOKS_DIRECTORY, PROJECT_STATUS_LAUNCHER);
|
|
2276
|
-
let runtimeCurrent = true;
|
|
2277
|
-
if (isSymlink(launcher)) {
|
|
2278
|
-
push("fail", "project: status launcher is a symlink");
|
|
2279
|
-
runtimeCurrent = false;
|
|
2280
|
-
} else if (!isFile(launcher)) {
|
|
2281
|
-
push("fail", "project: missing status launcher");
|
|
2282
|
-
runtimeCurrent = false;
|
|
2283
|
-
} else {
|
|
2284
|
-
const launcherContent = readProjectFile(launcher, "project: status launcher");
|
|
2285
|
-
const launcherSource = readProjectFile(
|
|
2286
|
-
path.join(sourceRoot, "scripts", PROJECT_STATUS_LAUNCHER),
|
|
2287
|
-
"package: status launcher"
|
|
2288
|
-
);
|
|
2289
|
-
if (launcherContent === null || launcherSource === null) {
|
|
2290
|
-
runtimeCurrent = false;
|
|
2291
|
-
} else {
|
|
2292
|
-
if (!launcherContent.toString("utf8").includes(PROJECT_STATUS_MARKER)) {
|
|
2293
|
-
push("warn", "project: status launcher is not managed");
|
|
2294
|
-
runtimeCurrent = false;
|
|
2295
|
-
} else if (!launcherContent.equals(launcherSource)) {
|
|
2296
|
-
push(
|
|
2297
|
-
"warn",
|
|
2298
|
-
"project: status launcher is stale — refresh it with the project contracts"
|
|
2299
|
-
);
|
|
2300
|
-
runtimeCurrent = false;
|
|
2301
|
-
} else {
|
|
2302
|
-
push("ok", "project: status launcher current");
|
|
2303
|
-
}
|
|
2304
|
-
}
|
|
2305
|
-
}
|
|
2306
|
-
let runtimeSafe = true;
|
|
2307
|
-
try {
|
|
2308
|
-
validateDirectoryDestination(
|
|
2309
|
-
path.join(project, HOOKS_DIRECTORY),
|
|
2310
|
-
"project runtime parent directory"
|
|
2311
|
-
);
|
|
2312
|
-
validateDirectoryDestination(runtime, "project runtime directory");
|
|
2313
|
-
} catch (error) {
|
|
2314
|
-
if (!(error instanceof InstallerError)) throw error;
|
|
2315
|
-
push("fail", `project: unsafe runtime — ${error.message}`);
|
|
2316
|
-
runtimeSafe = false;
|
|
2317
|
-
runtimeCurrent = false;
|
|
2318
|
-
}
|
|
2319
|
-
if (runtimeSafe) {
|
|
2320
|
-
for (const name of PROJECT_RUNTIME_SCRIPTS) {
|
|
2321
|
-
const destination = path.join(runtime, name);
|
|
2322
|
-
if (isSymlink(destination)) {
|
|
2323
|
-
push("fail", `project: runtime ${name} is a symlink`);
|
|
2324
|
-
runtimeCurrent = false;
|
|
2325
|
-
continue;
|
|
2326
|
-
}
|
|
2327
|
-
if (!isFile(destination)) {
|
|
2328
|
-
push("fail", `project: missing runtime ${name}`);
|
|
2329
|
-
runtimeCurrent = false;
|
|
2330
|
-
continue;
|
|
2331
|
-
}
|
|
2332
|
-
const content = readProjectFile(destination, `project: runtime ${name}`);
|
|
2333
|
-
if (content === null) {
|
|
2334
|
-
runtimeCurrent = false;
|
|
2335
|
-
continue;
|
|
2336
|
-
}
|
|
2337
|
-
const source = readProjectFile(
|
|
2338
|
-
path.join(sourceRoot, "scripts", name),
|
|
2339
|
-
`package: runtime ${name}`
|
|
2340
|
-
);
|
|
2341
|
-
if (source === null) {
|
|
2342
|
-
runtimeCurrent = false;
|
|
2343
|
-
continue;
|
|
2344
|
-
}
|
|
2345
|
-
if (!content.toString("utf8").includes(PROJECT_RUNTIME_MARKER)) {
|
|
2346
|
-
push("warn", `project: runtime ${name} is not managed`);
|
|
2347
|
-
runtimeCurrent = false;
|
|
2348
|
-
} else if (!content.equals(source)) {
|
|
2349
|
-
push("warn", `project: runtime ${name} is stale — refresh it with the project contracts`);
|
|
2350
|
-
runtimeCurrent = false;
|
|
2351
|
-
} else {
|
|
2352
|
-
push("ok", `project: runtime ${name} current`);
|
|
2353
|
-
}
|
|
2354
|
-
}
|
|
2355
|
-
}
|
|
2356
|
-
|
|
2357
|
-
let guardInstalled = GUARD_SCRIPTS.some((name) =>
|
|
2358
|
-
lexists(path.join(project, HOOKS_DIRECTORY, name))
|
|
2359
|
-
);
|
|
2360
|
-
const nativeWiredTargets = new Set();
|
|
2361
|
-
for (const target of targets) {
|
|
2362
|
-
const contract = nativeGuardContract(target, project);
|
|
2363
|
-
if (
|
|
2364
|
-
contract !== null &&
|
|
2365
|
-
(isSymlink(contract.settings) || isManagedHookSettings(contract.settings))
|
|
2366
|
-
) {
|
|
2367
|
-
nativeWiredTargets.add(target);
|
|
2368
|
-
}
|
|
2369
|
-
}
|
|
2370
|
-
let guardWired = nativeWiredTargets.size > 0;
|
|
2371
|
-
const unreadableGitHooks = new Map();
|
|
2372
|
-
if (lexists(path.join(project, ".git"))) {
|
|
2373
|
-
const hooksDir = gitHooksDirectory(project);
|
|
2374
|
-
if (hooksDir !== null) {
|
|
2375
|
-
for (const name of GIT_HOOK_NAMES) {
|
|
2376
|
-
const hookPath = path.join(hooksDir, name);
|
|
2377
|
-
if (!lexists(hookPath)) continue;
|
|
2378
|
-
if (isSymlink(hookPath)) {
|
|
2379
|
-
guardWired = true;
|
|
2380
|
-
continue;
|
|
2381
|
-
}
|
|
2382
|
-
try {
|
|
2383
|
-
const managed = isManagedGitHookContent(fs.readFileSync(hookPath, "utf8"));
|
|
2384
|
-
guardWired = managed || guardWired;
|
|
2385
|
-
} catch (error) {
|
|
2386
|
-
unreadableGitHooks.set(hookPath, error.message);
|
|
2387
|
-
}
|
|
2388
|
-
}
|
|
2389
|
-
}
|
|
2390
|
-
}
|
|
2391
|
-
for (const [hookPath, error] of unreadableGitHooks) {
|
|
2392
|
-
const label = describeProjectPath(project, hookPath);
|
|
2393
|
-
push("fail", `hooks: ${label} cannot be read: ${error}`);
|
|
2394
|
-
}
|
|
2395
|
-
guardInstalled = guardInstalled || guardWired;
|
|
2396
|
-
if (!guardInstalled) {
|
|
2397
|
-
push("note", "hooks: guard hooks not installed (opt in with --hooks; see HOOKS.md)");
|
|
2398
|
-
} else {
|
|
2399
|
-
for (const name of GUARD_SCRIPTS) {
|
|
2400
|
-
const destination = path.join(project, HOOKS_DIRECTORY, name);
|
|
2401
|
-
if (isSymlink(destination)) {
|
|
2402
|
-
push("fail", `hooks: ${HOOKS_DIRECTORY}/${name} is a symlink`);
|
|
2403
|
-
continue;
|
|
2404
|
-
}
|
|
2405
|
-
if (!isFile(destination)) {
|
|
2406
|
-
push("fail", `hooks: missing ${HOOKS_DIRECTORY}/${name} — run --hooks-refresh`);
|
|
2407
|
-
continue;
|
|
2408
|
-
}
|
|
2409
|
-
const content = readProjectFile(destination, `hooks: ${HOOKS_DIRECTORY}/${name}`);
|
|
2410
|
-
if (content === null) continue;
|
|
2411
|
-
const source = readProjectFile(
|
|
2412
|
-
path.join(sourceRoot, "scripts", name),
|
|
2413
|
-
`package: guard ${name}`
|
|
2414
|
-
);
|
|
2415
|
-
if (source === null) continue;
|
|
2416
|
-
if (!content.toString("utf8").includes(GUARD_MARKER)) {
|
|
2417
|
-
push("warn", `hooks: ${HOOKS_DIRECTORY}/${name} is not a managed guard script`);
|
|
2418
|
-
} else if (!content.equals(source)) {
|
|
2419
|
-
push("warn", `hooks: ${HOOKS_DIRECTORY}/${name} is stale — run --hooks-refresh`);
|
|
2420
|
-
} else {
|
|
2421
|
-
push("ok", `hooks: ${HOOKS_DIRECTORY}/${name} current`);
|
|
2422
|
-
}
|
|
2423
|
-
}
|
|
2424
|
-
for (const target of targets) {
|
|
2425
|
-
if (!installedTargets.has(target) && !nativeWiredTargets.has(target)) continue;
|
|
2426
|
-
const contract = nativeGuardContract(target, project);
|
|
2427
|
-
if (contract === null) {
|
|
2428
|
-
if (MANIFEST.hosts[target]?.guard_tier !== "git-only") {
|
|
2429
|
-
push("fail", `hooks: ${target} declares a native guard without a health contract`);
|
|
2430
|
-
}
|
|
2431
|
-
continue;
|
|
2432
|
-
}
|
|
2433
|
-
if (isSymlink(contract.settings)) {
|
|
2434
|
-
push("fail", `hooks: ${target} native guard wiring is a symlink`);
|
|
2435
|
-
continue;
|
|
2436
|
-
}
|
|
2437
|
-
if (!isFile(contract.settings)) {
|
|
2438
|
-
push("fail", `hooks: ${target} native guard wiring is missing — run --hooks-refresh-full`);
|
|
2439
|
-
continue;
|
|
2440
|
-
}
|
|
2441
|
-
let current = false;
|
|
2442
|
-
try {
|
|
2443
|
-
const parsed = JSON.parse(fs.readFileSync(contract.settings, "utf8"));
|
|
2444
|
-
const entries =
|
|
2445
|
-
parsed && typeof parsed === "object" && parsed.hooks
|
|
2446
|
-
? parsed.hooks[contract.event]
|
|
2447
|
-
: null;
|
|
2448
|
-
const variants = INTERPRETER_CANDIDATES.map((candidate) =>
|
|
2449
|
-
JSON.stringify(contract.entry(candidate))
|
|
2450
|
-
);
|
|
2451
|
-
current =
|
|
2452
|
-
Array.isArray(entries) &&
|
|
2453
|
-
entries.some((entry) => variants.includes(JSON.stringify(entry)));
|
|
2454
|
-
} catch {
|
|
2455
|
-
current = false;
|
|
2456
|
-
}
|
|
2457
|
-
if (current) {
|
|
2458
|
-
push("ok", `hooks: ${target} native guard wiring present`);
|
|
2459
|
-
} else {
|
|
2460
|
-
push("fail", `hooks: ${target} native guard wiring is stale — run --hooks-refresh-full`);
|
|
2461
|
-
}
|
|
2462
|
-
}
|
|
2463
|
-
const dotGit = path.join(project, ".git");
|
|
2464
|
-
if (lexists(dotGit)) {
|
|
2465
|
-
const hooksDir = gitHooksDirectory(project);
|
|
2466
|
-
if (hooksDir === null) {
|
|
2467
|
-
push(
|
|
2468
|
-
"fail",
|
|
2469
|
-
"hooks: cannot resolve the git hooks directory (is git runnable?); git hooks unverified"
|
|
2470
|
-
);
|
|
2471
|
-
} else {
|
|
2472
|
-
const custom = !samePath(hooksDir, path.join(dotGit, "hooks"));
|
|
2473
|
-
let missingFromCustom = false;
|
|
2474
|
-
for (const [hookName, generator] of GIT_HOOKS) {
|
|
2475
|
-
const hookPath = path.join(hooksDir, hookName);
|
|
2476
|
-
const label = describeProjectPath(project, hookPath);
|
|
2477
|
-
if (unreadableGitHooks.has(hookPath)) {
|
|
2478
|
-
continue;
|
|
2479
|
-
}
|
|
2480
|
-
if (!lexists(hookPath)) {
|
|
2481
|
-
push(
|
|
2482
|
-
"fail",
|
|
2483
|
-
`hooks: ${hookName} is missing from the effective git hooks directory ` +
|
|
2484
|
-
`${hooksDir} — run --hooks-refresh-full`
|
|
2485
|
-
);
|
|
2486
|
-
if (custom) missingFromCustom = true;
|
|
2487
|
-
} else if (isSymlink(hookPath)) {
|
|
2488
|
-
push("fail", `hooks: ${label} is a symlink`);
|
|
2489
|
-
} else {
|
|
2490
|
-
const hookContent = readProjectFile(hookPath, `hooks: ${label}`);
|
|
2491
|
-
if (hookContent === null) continue;
|
|
2492
|
-
const hookText = hookContent.toString("utf8");
|
|
2493
|
-
if (!isManagedGitHookContent(hookText)) {
|
|
2494
|
-
push("warn", `hooks: ${label} is not a managed GSD Path git hook`);
|
|
2495
|
-
} else if (
|
|
2496
|
-
!INTERPRETER_CANDIDATES.some(
|
|
2497
|
-
(candidate) => hookText === generator(candidate)
|
|
2498
|
-
)
|
|
2499
|
-
) {
|
|
2500
|
-
push("warn", `hooks: ${label} is stale — run --hooks-refresh-full`);
|
|
2501
|
-
} else if (!isExecutable(hookPath)) {
|
|
2502
|
-
push("warn", `hooks: ${label} is not executable — run --hooks-refresh-full`);
|
|
2503
|
-
} else {
|
|
2504
|
-
push("ok", `hooks: ${label} wired`);
|
|
2505
|
-
}
|
|
2506
|
-
}
|
|
2507
|
-
}
|
|
2508
|
-
if (missingFromCustom) {
|
|
2509
|
-
push(
|
|
2510
|
-
"warn",
|
|
2511
|
-
`hooks: core.hooksPath points this repository at ${hooksDir}, ` +
|
|
2512
|
-
"but the guard hooks are not wired there"
|
|
2513
|
-
);
|
|
2514
|
-
}
|
|
2515
|
-
}
|
|
2516
|
-
}
|
|
2517
|
-
}
|
|
2518
|
-
|
|
2519
|
-
const stateFile = path.join(project, ".project", "STATE.md");
|
|
2520
|
-
if (!isDirectory(path.join(project, ".project"))) {
|
|
2521
|
-
push("note", "state: no .project/ pipeline state (nothing started yet)");
|
|
2522
|
-
} else if (!isFile(stateFile)) {
|
|
2523
|
-
push("warn", "state: .project/ exists but STATE.md is missing");
|
|
2524
|
-
} else {
|
|
2525
|
-
try {
|
|
2526
|
-
const state = hooks.validatedProjectState(sourceRoot, project);
|
|
2527
|
-
if (!runtimeCurrent) {
|
|
2528
|
-
throw new InstallerError(
|
|
2529
|
-
"project runtime status was not executed because the installed runtime is not current"
|
|
2530
|
-
);
|
|
2531
|
-
}
|
|
2532
|
-
validateProjectRuntimeStatus(sourceRoot, project);
|
|
2533
|
-
if (!projectRuntimeMatches(sourceRoot, project)) {
|
|
2534
|
-
throw new InstallerError("project runtime changed during doctor validation");
|
|
2535
|
-
}
|
|
2536
|
-
push("ok", `state: ${state.phase}/${state.status}`);
|
|
2537
|
-
} catch (error) {
|
|
2538
|
-
push("fail", `state: ${messageOf(error)}`);
|
|
2539
|
-
}
|
|
2540
|
-
}
|
|
2541
1176
|
return findings;
|
|
2542
1177
|
}
|
|
2543
1178
|
|
|
@@ -2686,16 +1321,26 @@ export const hooks = {
|
|
|
2686
1321
|
rename: fs.renameSync.bind(fs),
|
|
2687
1322
|
renameInstallLock: fs.renameSync.bind(fs),
|
|
2688
1323
|
renameInstallStage: fs.renameSync.bind(fs),
|
|
2689
|
-
validatedProjectState,
|
|
2690
1324
|
processIdentity,
|
|
2691
1325
|
reserveDirectory,
|
|
2692
1326
|
reserveFile,
|
|
2693
1327
|
detectPythonInterpreter,
|
|
2694
1328
|
resolveGitHooksPath,
|
|
2695
|
-
|
|
2696
|
-
copyGuardFile: copyFileAtomic,
|
|
1329
|
+
projectAdapter,
|
|
2697
1330
|
};
|
|
2698
1331
|
|
|
1332
|
+
// Project installation already requires Python; keep runtime lifecycle ownership there.
|
|
1333
|
+
function projectAdapter(source, project, action, payload = {}, env = process.env) {
|
|
1334
|
+
const interpreter = requiredPythonRuntime("project runtime");
|
|
1335
|
+
const result = spawnSync(interpreter, ["-B", path.join(SCRIPT_DIRECTORY, "install.py"), "--adapter-request"], {
|
|
1336
|
+
input: JSON.stringify({ source, project, action, ...payload }), encoding: "utf8", env: { ...process.env, ...env },
|
|
1337
|
+
});
|
|
1338
|
+
if (result.error || result.status !== 0) {
|
|
1339
|
+
throw new InstallerError(result.stderr?.trim() || result.error?.message || "project runtime operation failed");
|
|
1340
|
+
}
|
|
1341
|
+
return JSON.parse(result.stdout);
|
|
1342
|
+
}
|
|
1343
|
+
|
|
2699
1344
|
export async function install(sourceRoot, plans, options = {}) {
|
|
2700
1345
|
const {
|
|
2701
1346
|
project = null,
|
|
@@ -2709,14 +1354,13 @@ export async function install(sourceRoot, plans, options = {}) {
|
|
|
2709
1354
|
if (hooksEnabled && project === null) {
|
|
2710
1355
|
throw new InstallerError("--hooks requires --project");
|
|
2711
1356
|
}
|
|
2712
|
-
|
|
2713
|
-
|
|
2714
|
-
|
|
2715
|
-
|
|
2716
|
-
|
|
2717
|
-
} else if (hooksEnabled) {
|
|
2718
|
-
({ interpreter, hooksDir } = requiredHookRuntime(project, "--hooks", selected));
|
|
1357
|
+
if (project !== null) {
|
|
1358
|
+
return hooks.projectAdapter(sourceRoot, project, "install", {
|
|
1359
|
+
plans: plans.map(({ name, root }) => ({ name, root })), dryRun,
|
|
1360
|
+
hooks: hooksEnabled, migrateLegacy, update,
|
|
1361
|
+
}, env);
|
|
2719
1362
|
}
|
|
1363
|
+
const selected = plans.map((plan) => plan.name);
|
|
2720
1364
|
const progress = async (text) => {
|
|
2721
1365
|
if (onProgress) onProgress(text);
|
|
2722
1366
|
await tick();
|
|
@@ -2791,21 +1435,6 @@ export async function install(sourceRoot, plans, options = {}) {
|
|
|
2791
1435
|
}
|
|
2792
1436
|
}
|
|
2793
1437
|
|
|
2794
|
-
if (project !== null) {
|
|
2795
|
-
validateDirectoryDestination(project, "project path");
|
|
2796
|
-
if (dryRun) {
|
|
2797
|
-
validateProject(
|
|
2798
|
-
sourceRoot,
|
|
2799
|
-
project,
|
|
2800
|
-
selected,
|
|
2801
|
-
hooksEnabled,
|
|
2802
|
-
[...mutationRoots, ...plannedBackups],
|
|
2803
|
-
interpreter,
|
|
2804
|
-
hooksDir,
|
|
2805
|
-
update
|
|
2806
|
-
);
|
|
2807
|
-
}
|
|
2808
|
-
}
|
|
2809
1438
|
|
|
2810
1439
|
const results = [];
|
|
2811
1440
|
const staging = fs.mkdtempSync(path.join(os.tmpdir(), "gsd-path-install-"));
|
|
@@ -2832,32 +1461,15 @@ export async function install(sourceRoot, plans, options = {}) {
|
|
|
2832
1461
|
const suffix = count ? `; would back up ${count} entries` : "";
|
|
2833
1462
|
results.push(installResult(plan, true, update) + suffix);
|
|
2834
1463
|
}
|
|
2835
|
-
if (project !== null) {
|
|
2836
|
-
results.push(projectResult(project, selected, hooksEnabled, interpreter, hooksDir, update, true));
|
|
2837
|
-
}
|
|
2838
1464
|
appendHostNotes(results, selected);
|
|
2839
1465
|
return results;
|
|
2840
1466
|
}
|
|
2841
1467
|
|
|
2842
1468
|
const lockRoots = deployments.map((plan) => plan.root);
|
|
2843
1469
|
if (legacyRoot !== null && isDirectory(legacyRoot)) lockRoots.push(legacyRoot);
|
|
2844
|
-
if (project !== null) lockRoots.push(path.join(project, HOOKS_DIRECTORY));
|
|
2845
1470
|
const ownership = acquireInstallLocks(lockRoots);
|
|
2846
1471
|
const targetTransactions = [];
|
|
2847
|
-
const projectTransaction = { createdDirectories: [], copied: [], replaced: [] };
|
|
2848
1472
|
try {
|
|
2849
|
-
if (project !== null) {
|
|
2850
|
-
validateProject(
|
|
2851
|
-
sourceRoot,
|
|
2852
|
-
project,
|
|
2853
|
-
selected,
|
|
2854
|
-
hooksEnabled,
|
|
2855
|
-
[...mutationRoots, ...plannedBackups],
|
|
2856
|
-
interpreter,
|
|
2857
|
-
hooksDir,
|
|
2858
|
-
update
|
|
2859
|
-
);
|
|
2860
|
-
}
|
|
2861
1473
|
if (legacyRoot !== null && isDirectory(legacyRoot)) {
|
|
2862
1474
|
await progress("Backing up legacy Codex skills");
|
|
2863
1475
|
const legacyTransaction = {
|
|
@@ -2895,28 +1507,8 @@ export async function install(sourceRoot, plans, options = {}) {
|
|
|
2895
1507
|
);
|
|
2896
1508
|
}
|
|
2897
1509
|
}
|
|
2898
|
-
if (project !== null) {
|
|
2899
|
-
await progress("Writing project contracts");
|
|
2900
|
-
const resultLine = projectResult(project, selected, hooksEnabled, interpreter, hooksDir, update, false);
|
|
2901
|
-
applyProject(
|
|
2902
|
-
sourceRoot,
|
|
2903
|
-
project,
|
|
2904
|
-
selected,
|
|
2905
|
-
hooksEnabled,
|
|
2906
|
-
projectTransaction,
|
|
2907
|
-
interpreter,
|
|
2908
|
-
hooksDir,
|
|
2909
|
-
update
|
|
2910
|
-
);
|
|
2911
|
-
results.push(resultLine);
|
|
2912
|
-
}
|
|
2913
1510
|
} catch (error) {
|
|
2914
1511
|
const rollbackErrors = [];
|
|
2915
|
-
try {
|
|
2916
|
-
rollbackProject(projectTransaction);
|
|
2917
|
-
} catch (rollbackError) {
|
|
2918
|
-
rollbackErrors.push(messageOf(rollbackError));
|
|
2919
|
-
}
|
|
2920
1512
|
for (const transaction of [...targetTransactions].reverse()) {
|
|
2921
1513
|
try {
|
|
2922
1514
|
rollbackTarget(transaction);
|
|
@@ -2970,6 +1562,9 @@ export function parseCli(argv) {
|
|
|
2970
1562
|
doctor: { type: "boolean", default: false },
|
|
2971
1563
|
hooks: { type: "boolean", default: false },
|
|
2972
1564
|
"hooks-init": { type: "boolean", default: false },
|
|
1565
|
+
"runtime-restore": { type: "boolean", default: false },
|
|
1566
|
+
"runtime-upgrade": { type: "boolean", default: false },
|
|
1567
|
+
"runtime-migrate": { type: "boolean", default: false },
|
|
2973
1568
|
"hooks-refresh": { type: "boolean", default: false },
|
|
2974
1569
|
"hooks-refresh-full": { type: "boolean", default: false },
|
|
2975
1570
|
"source-root": { type: "string" },
|
|
@@ -3052,13 +1647,16 @@ function usage() {
|
|
|
3052
1647
|
" Update skills: gsd-path --update (or npx @opengsd/gsd-path@latest --update)\n\n" +
|
|
3053
1648
|
`targets: ${flags}\n` +
|
|
3054
1649
|
" --update refresh existing installs in place; with --project also\n" +
|
|
3055
|
-
"
|
|
1650
|
+
" keeps its selected runtime and project contracts\n" +
|
|
3056
1651
|
" --local install into this project's per-host skill dirs\n" +
|
|
3057
1652
|
" --project PATH write project contracts and status runtime; requires Python 3.9+\n" +
|
|
3058
1653
|
" --doctor read-only health check of installs, hooks, and state\n" +
|
|
3059
1654
|
" --hooks with --project: install guard hooks (see HOOKS.md)\n" +
|
|
3060
1655
|
" --hooks-init add guards to an existing project without changing its contracts\n" +
|
|
3061
|
-
" --
|
|
1656
|
+
" --runtime-restore restore the exact declared runtime from --source-root\n" +
|
|
1657
|
+
" --runtime-upgrade explicitly select the supplied package runtime\n" +
|
|
1658
|
+
" --runtime-migrate migrate a tracked legacy runtime for review; add --update to continue upgrading\n" +
|
|
1659
|
+
" --hooks-refresh validate the selected runtime; keep its version\n" +
|
|
3062
1660
|
" --hooks-refresh-full refresh native settings/git hooks; target flags create missing configs\n" +
|
|
3063
1661
|
" --dry-run preview without writing\n" +
|
|
3064
1662
|
" each target also accepts --<target>-root PATH to override its skills root"
|
|
@@ -3086,6 +1684,8 @@ export async function main(argv, env = process.env) {
|
|
|
3086
1684
|
colored: !process.env.NO_COLOR,
|
|
3087
1685
|
version: packageVersion(),
|
|
3088
1686
|
targets: TARGETS,
|
|
1687
|
+
legacyRuntime: lexists(path.join(process.cwd(), ".gsd-path", "runtime")) &&
|
|
1688
|
+
!lexists(path.join(process.cwd(), ".gsd-path", "runtime.json")),
|
|
3089
1689
|
installed: (target, local) =>
|
|
3090
1690
|
hasManagedInstall(local ? localRoot(target, process.cwd()) : defaultRoot(target, env)),
|
|
3091
1691
|
});
|
|
@@ -3097,6 +1697,17 @@ export async function main(argv, env = process.env) {
|
|
|
3097
1697
|
: path.resolve(SCRIPT_DIRECTORY, "..");
|
|
3098
1698
|
const project =
|
|
3099
1699
|
values.project !== undefined ? absolutePath(values.project) : null;
|
|
1700
|
+
const migrateAndUpdate = values["runtime-migrate"] && values.update;
|
|
1701
|
+
if (migrateAndUpdate && (project === null || values["runtime-restore"] || values["runtime-upgrade"] || values.doctor || values["hooks-init"] || values["hooks-refresh"] || values["hooks-refresh-full"])) {
|
|
1702
|
+
ui.error("--runtime-migrate --update requires --project and cannot be combined with another runtime, doctor, or hook operation");
|
|
1703
|
+
return 2;
|
|
1704
|
+
}
|
|
1705
|
+
if (!migrateAndUpdate && ["runtime-restore", "runtime-upgrade", "runtime-migrate"].some(name => values[name])) {
|
|
1706
|
+
const interpreter = requiredPythonRuntime("project runtime");
|
|
1707
|
+
const result = spawnSync(interpreter, ["-B", path.join(SCRIPT_DIRECTORY, "install.py"),
|
|
1708
|
+
...argv.filter(arg => arg !== "--no-color")], { stdio: "inherit", env });
|
|
1709
|
+
return result.status ?? 1;
|
|
1710
|
+
}
|
|
3100
1711
|
const local = values.local;
|
|
3101
1712
|
const rootFor = (target) => {
|
|
3102
1713
|
const override = values[`${target}-root`];
|
|
@@ -3196,6 +1807,21 @@ export async function main(argv, env = process.env) {
|
|
|
3196
1807
|
} else {
|
|
3197
1808
|
plans = selected.map((target) => targetPlan(target, rootFor(target)));
|
|
3198
1809
|
}
|
|
1810
|
+
if (migrateAndUpdate) {
|
|
1811
|
+
const interpreter = requiredPythonRuntime("project runtime");
|
|
1812
|
+
const migration = spawnSync(interpreter, ["-B", path.join(SCRIPT_DIRECTORY, "install.py"),
|
|
1813
|
+
"--runtime-migrate", "--project", project, "--source-root", sourceRoot,
|
|
1814
|
+
...(values["dry-run"] ? ["--dry-run"] : [])], { stdio: "inherit", env });
|
|
1815
|
+
if (migration.error || migration.status !== 0) {
|
|
1816
|
+
if (migration.error) ui.error(migration.error.message);
|
|
1817
|
+
return migration.status ?? 1;
|
|
1818
|
+
}
|
|
1819
|
+
if (values["dry-run"]) {
|
|
1820
|
+
ui.result("Migration preview only; nothing was written. Run without --dry-run to migrate, then validate and apply the update.");
|
|
1821
|
+
return 0;
|
|
1822
|
+
}
|
|
1823
|
+
ui.result("Migration completed. Review the unstaged Git diff. Updating skills and wiring next; an update failure will retain the completed migration.");
|
|
1824
|
+
}
|
|
3199
1825
|
const spin = ui.spinner("Preparing");
|
|
3200
1826
|
try {
|
|
3201
1827
|
const results = await install(sourceRootForInstall, plans, {
|