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