@opengsd/gsd-path 1.3.1 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +2 -207
- package/CHANGELOG.md +331 -69
- package/DOCS.md +25 -9
- package/FULL.md +12 -5
- package/GUIDE.md +1 -1
- package/HOOKS.md +104 -14
- package/QUICK.md +5 -2
- package/README.md +131 -111
- package/RUNTIME.md +2 -2
- package/UPDATE.md +61 -11
- package/WORKFLOW.md +44 -7
- package/package.json +13 -9
- package/platforms/shared-agents/dispatch.md +13 -0
- package/scripts/_common.py +619 -18
- package/scripts/archive_milestone.py +160 -86
- package/scripts/bootstrap_repository.py +8 -5
- package/scripts/build_recovery.py +100 -32
- package/scripts/build_state.py +143 -24
- package/scripts/check_docs_audit.py +91 -7
- package/scripts/check_handoffs.py +662 -50
- package/scripts/check_task_briefs.py +338 -30
- package/scripts/check_update.py +2 -2
- package/scripts/core_hook_gate.py +10 -1
- package/scripts/core_hook_settings.py +11 -4
- package/scripts/detect_project.py +587 -32
- package/scripts/discussion_records.py +19 -13
- package/scripts/discussion_validate.py +152 -50
- package/scripts/dispatch_driver.py +501 -46
- package/scripts/git_guard.py +118 -31
- package/scripts/guard_hook.py +1567 -194
- package/scripts/install.mjs +77 -46
- package/scripts/install.py +611 -50
- package/scripts/integration.py +580 -25
- package/scripts/isolation.py +1438 -57
- package/scripts/lean_verification.py +99 -19
- package/scripts/loop_run.py +31 -29
- package/scripts/members.py +706 -0
- package/scripts/migrate_core.py +2 -2
- package/scripts/path_config.py +5 -5
- package/scripts/pipeline_diagnose.py +82 -31
- package/scripts/pipeline_git.py +124 -34
- package/scripts/pipeline_state.py +287 -87
- package/scripts/pipeline_undo.py +218 -30
- package/scripts/promote_lookahead.py +1 -1
- package/scripts/review_panel.py +1 -1
- package/scripts/runtime_store.py +55 -11
- package/scripts/skill-resources.json +37 -5
- package/scripts/state_checkpoint.py +135 -44
- package/scripts/state_promote.py +2 -0
- package/scripts/status_runtime.py +58 -10
- package/scripts/sync_skill_resources.py +8 -7
- package/scripts/task_context.py +12 -3
- package/scripts/token_budget.py +12 -8
- package/scripts/wizard.mjs +1 -0
- package/scripts/workflow_run.py +107 -30
- package/scripts/worktree_paths.py +72 -10
- package/skills/gsd-path/BUILD.md +7 -1
- package/skills/gsd-path/DECIDE.md +11 -4
- package/skills/gsd-path/DEFINE.md +36 -4
- package/skills/gsd-path/DOCS-AUDIT.md +6 -4
- package/skills/gsd-path/INSPECT.md +5 -4
- package/skills/gsd-path/PLAN.md +18 -4
- package/skills/gsd-path/RESEARCH.md +13 -1
- package/skills/gsd-path/ROADMAP.md +2 -1
- package/skills/gsd-path/SHIP.md +33 -11
- package/skills/gsd-path/SKILL.md +45 -1
- package/skills/gsd-path/references/build-native.md +63 -15
- package/skills/gsd-path/references/codebase-mapper.md +2 -1
- package/skills/gsd-path/references/dispatch.md +13 -0
- package/skills/gsd-path/references/docs-auditor.md +7 -1
- package/skills/gsd-path/references/operating-rules.md +214 -0
- package/skills/gsd-path/references/planner.md +22 -4
- package/skills/gsd-path/references/reviewer-final.md +20 -2
- package/skills/gsd-path/references/reviewer-wave.md +18 -5
- package/skills/gsd-path/references/reviewer.md +3 -1
- package/skills/gsd-path/references/spec-probes.md +150 -0
- package/skills/gsd-path/scripts/_common.py +619 -18
- package/skills/gsd-path/scripts/archive_milestone.py +160 -86
- package/skills/gsd-path/scripts/bootstrap_repository.py +8 -5
- package/skills/gsd-path/scripts/build_recovery.py +100 -32
- package/skills/gsd-path/scripts/build_state.py +143 -24
- package/skills/gsd-path/scripts/check_docs_audit.py +91 -7
- package/skills/gsd-path/scripts/check_handoffs.py +662 -50
- package/skills/gsd-path/scripts/check_task_briefs.py +338 -30
- package/skills/gsd-path/scripts/check_update.py +2 -2
- package/skills/gsd-path/scripts/detect_project.py +587 -32
- package/skills/gsd-path/scripts/discussion_records.py +19 -13
- package/skills/gsd-path/scripts/discussion_validate.py +152 -50
- package/skills/gsd-path/scripts/dispatch_driver.py +501 -46
- package/skills/gsd-path/scripts/git_guard.py +118 -31
- package/skills/gsd-path/scripts/integration.py +580 -25
- package/skills/gsd-path/scripts/isolation.py +1438 -57
- package/skills/gsd-path/scripts/lean_verification.py +99 -19
- package/skills/gsd-path/scripts/loop_run.py +31 -29
- package/skills/gsd-path/scripts/members.py +706 -0
- package/skills/gsd-path/scripts/path_config.py +5 -5
- package/skills/gsd-path/scripts/pipeline_git.py +124 -34
- package/skills/gsd-path/scripts/pipeline_state.py +287 -87
- package/skills/gsd-path/scripts/promote_lookahead.py +1 -1
- package/skills/gsd-path/scripts/review_panel.py +1 -1
- package/skills/gsd-path/scripts/state_checkpoint.py +135 -44
- package/skills/gsd-path/scripts/state_promote.py +2 -0
- package/skills/gsd-path/scripts/status_runtime.py +58 -10
- package/skills/gsd-path/scripts/task_context.py +12 -3
- package/skills/gsd-path/scripts/token_budget.py +12 -8
- package/skills/gsd-path/scripts/workflow_run.py +107 -30
- package/skills/gsd-path/scripts/worktree_paths.py +72 -10
- package/skills/gsd-path/templates/codebase.md +1 -1
- package/skills/gsd-path/templates/docs-audit.md +6 -2
- package/skills/gsd-path/templates/final-review.md +16 -4
- package/skills/gsd-path/templates/gap-review.md +2 -1
- package/skills/gsd-path/templates/intent.md +30 -0
- package/skills/gsd-path/templates/patch-findings.md +3 -3
- package/skills/gsd-path/templates/plan.md +17 -2
- package/skills/gsd-path/templates/research-handoff.md +14 -3
- package/skills/gsd-path/templates/state.md +1 -1
- package/skills/gsd-path/templates/task.md +1 -0
- package/skills/gsd-path/templates/wave-review.md +16 -4
- package/skills/gsd-path-build/SKILL.md +7 -1
- package/skills/gsd-path-build/references/build-native.md +63 -15
- package/skills/gsd-path-build/references/dispatch.md +13 -0
- package/skills/gsd-path-build/references/operating-rules.md +214 -0
- package/skills/gsd-path-build/references/reviewer-final.md +20 -2
- package/skills/gsd-path-build/references/reviewer-wave.md +18 -5
- package/skills/gsd-path-build/references/reviewer.md +3 -1
- package/skills/gsd-path-build/references/spec-probes.md +150 -0
- package/skills/gsd-path-build/scripts/_common.py +619 -18
- package/skills/gsd-path-build/scripts/archive_milestone.py +160 -86
- package/skills/gsd-path-build/scripts/build_recovery.py +100 -32
- package/skills/gsd-path-build/scripts/build_state.py +143 -24
- package/skills/gsd-path-build/scripts/check_handoffs.py +662 -50
- package/skills/gsd-path-build/scripts/check_task_briefs.py +338 -30
- package/skills/gsd-path-build/scripts/detect_project.py +587 -32
- package/skills/gsd-path-build/scripts/discussion_records.py +19 -13
- package/skills/gsd-path-build/scripts/discussion_validate.py +152 -50
- package/skills/gsd-path-build/scripts/dispatch_driver.py +501 -46
- package/skills/gsd-path-build/scripts/integration.py +580 -25
- package/skills/gsd-path-build/scripts/isolation.py +1438 -57
- package/skills/gsd-path-build/scripts/loop_run.py +31 -29
- package/skills/gsd-path-build/scripts/members.py +706 -0
- package/skills/gsd-path-build/scripts/path_config.py +5 -5
- package/skills/gsd-path-build/scripts/pipeline_git.py +124 -34
- package/skills/gsd-path-build/scripts/pipeline_state.py +287 -87
- package/skills/gsd-path-build/scripts/promote_lookahead.py +1 -1
- package/skills/gsd-path-build/scripts/review_panel.py +1 -1
- package/skills/gsd-path-build/scripts/state_checkpoint.py +135 -44
- package/skills/gsd-path-build/scripts/state_promote.py +2 -0
- package/skills/gsd-path-build/scripts/task_context.py +12 -3
- package/skills/gsd-path-build/scripts/token_budget.py +12 -8
- package/skills/gsd-path-build/scripts/workflow_run.py +107 -30
- package/skills/gsd-path-build/scripts/worktree_paths.py +72 -10
- package/skills/gsd-path-build/templates/task.md +1 -0
- package/skills/gsd-path-build/templates/wave-review.md +16 -4
- package/skills/gsd-path-decide/SKILL.md +11 -4
- package/skills/gsd-path-decide/references/dispatch.md +13 -0
- package/skills/gsd-path-decide/references/operating-rules.md +214 -0
- package/skills/gsd-path-decide/scripts/_common.py +619 -18
- package/skills/gsd-path-decide/scripts/archive_milestone.py +160 -86
- package/skills/gsd-path-decide/scripts/build_recovery.py +100 -32
- package/skills/gsd-path-decide/scripts/check_handoffs.py +662 -50
- package/skills/gsd-path-decide/scripts/check_task_briefs.py +338 -30
- package/skills/gsd-path-decide/scripts/detect_project.py +587 -32
- package/skills/gsd-path-decide/scripts/discussion_records.py +19 -13
- package/skills/gsd-path-decide/scripts/discussion_validate.py +152 -50
- package/skills/gsd-path-decide/scripts/integration.py +580 -25
- package/skills/gsd-path-decide/scripts/isolation.py +1438 -57
- package/skills/gsd-path-decide/scripts/loop_run.py +31 -29
- package/skills/gsd-path-decide/scripts/members.py +706 -0
- package/skills/gsd-path-decide/scripts/path_config.py +5 -5
- package/skills/gsd-path-decide/scripts/pipeline_git.py +124 -34
- package/skills/gsd-path-decide/scripts/pipeline_state.py +287 -87
- package/skills/gsd-path-decide/scripts/promote_lookahead.py +1 -1
- package/skills/gsd-path-decide/scripts/review_panel.py +1 -1
- package/skills/gsd-path-decide/scripts/state_checkpoint.py +135 -44
- package/skills/gsd-path-decide/scripts/state_promote.py +2 -0
- package/skills/gsd-path-decide/scripts/worktree_paths.py +72 -10
- package/skills/gsd-path-define/SKILL.md +36 -4
- package/skills/gsd-path-define/references/operating-rules.md +214 -0
- package/skills/gsd-path-define/references/spec-probes.md +150 -0
- package/skills/gsd-path-define/scripts/_common.py +619 -18
- package/skills/gsd-path-define/scripts/archive_milestone.py +160 -86
- package/skills/gsd-path-define/scripts/build_recovery.py +100 -32
- package/skills/gsd-path-define/scripts/check_handoffs.py +662 -50
- package/skills/gsd-path-define/scripts/check_task_briefs.py +338 -30
- package/skills/gsd-path-define/scripts/detect_project.py +587 -32
- package/skills/gsd-path-define/scripts/discussion_records.py +19 -13
- package/skills/gsd-path-define/scripts/discussion_validate.py +152 -50
- package/skills/gsd-path-define/scripts/integration.py +580 -25
- package/skills/gsd-path-define/scripts/isolation.py +1438 -57
- package/skills/gsd-path-define/scripts/loop_run.py +31 -29
- package/skills/gsd-path-define/scripts/members.py +706 -0
- package/skills/gsd-path-define/scripts/path_config.py +5 -5
- package/skills/gsd-path-define/scripts/pipeline_git.py +124 -34
- package/skills/gsd-path-define/scripts/pipeline_state.py +287 -87
- package/skills/gsd-path-define/scripts/promote_lookahead.py +1 -1
- package/skills/gsd-path-define/scripts/review_panel.py +1 -1
- package/skills/gsd-path-define/scripts/state_checkpoint.py +135 -44
- package/skills/gsd-path-define/scripts/state_promote.py +2 -0
- package/skills/gsd-path-define/scripts/worktree_paths.py +72 -10
- package/skills/gsd-path-define/templates/intent.md +30 -0
- package/skills/gsd-path-define/templates/state.md +1 -1
- package/skills/gsd-path-discuss/SKILL.md +2 -1
- package/skills/gsd-path-discuss/references/operating-rules.md +214 -0
- package/skills/gsd-path-discuss/scripts/_common.py +619 -18
- package/skills/gsd-path-discuss/scripts/discussion_records.py +19 -13
- package/skills/gsd-path-discuss/scripts/loop_run.py +31 -29
- package/skills/gsd-path-discuss/scripts/path_config.py +5 -5
- package/skills/gsd-path-discuss/scripts/review_panel.py +1 -1
- package/skills/gsd-path-discuss/scripts/worktree_paths.py +72 -10
- package/skills/gsd-path-docs-audit/SKILL.md +6 -4
- package/skills/gsd-path-docs-audit/references/dispatch.md +13 -0
- package/skills/gsd-path-docs-audit/references/docs-auditor.md +7 -1
- package/skills/gsd-path-docs-audit/references/operating-rules.md +214 -0
- package/skills/gsd-path-docs-audit/scripts/_common.py +619 -18
- package/skills/gsd-path-docs-audit/scripts/archive_milestone.py +160 -86
- package/skills/gsd-path-docs-audit/scripts/build_recovery.py +100 -32
- package/skills/gsd-path-docs-audit/scripts/check_docs_audit.py +91 -7
- package/skills/gsd-path-docs-audit/scripts/check_handoffs.py +662 -50
- package/skills/gsd-path-docs-audit/scripts/check_task_briefs.py +338 -30
- package/skills/gsd-path-docs-audit/scripts/detect_project.py +587 -32
- package/skills/gsd-path-docs-audit/scripts/discussion_records.py +19 -13
- package/skills/gsd-path-docs-audit/scripts/discussion_validate.py +152 -50
- package/skills/gsd-path-docs-audit/scripts/integration.py +580 -25
- package/skills/gsd-path-docs-audit/scripts/isolation.py +1438 -57
- package/skills/gsd-path-docs-audit/scripts/loop_run.py +31 -29
- package/skills/gsd-path-docs-audit/scripts/members.py +706 -0
- package/skills/gsd-path-docs-audit/scripts/path_config.py +5 -5
- package/skills/gsd-path-docs-audit/scripts/pipeline_git.py +124 -34
- package/skills/gsd-path-docs-audit/scripts/pipeline_state.py +287 -87
- package/skills/gsd-path-docs-audit/scripts/promote_lookahead.py +1 -1
- package/skills/gsd-path-docs-audit/scripts/review_panel.py +1 -1
- package/skills/gsd-path-docs-audit/scripts/state_checkpoint.py +135 -44
- package/skills/gsd-path-docs-audit/scripts/state_promote.py +2 -0
- package/skills/gsd-path-docs-audit/scripts/worktree_paths.py +72 -10
- package/skills/gsd-path-docs-audit/templates/docs-audit.md +6 -2
- package/skills/gsd-path-forensics/SKILL.md +2 -1
- package/skills/gsd-path-forensics/references/operating-rules.md +214 -0
- package/skills/gsd-path-forensics/scripts/_common.py +619 -18
- package/skills/gsd-path-forensics/scripts/discussion_records.py +19 -13
- package/skills/gsd-path-forensics/scripts/loop_run.py +31 -29
- package/skills/gsd-path-forensics/scripts/path_config.py +5 -5
- package/skills/gsd-path-forensics/scripts/pipeline_diagnose.py +82 -31
- package/skills/gsd-path-forensics/scripts/pipeline_undo.py +218 -30
- package/skills/gsd-path-forensics/scripts/review_panel.py +1 -1
- package/skills/gsd-path-forensics/scripts/worktree_paths.py +72 -10
- package/skills/gsd-path-inspect/SKILL.md +5 -4
- package/skills/gsd-path-inspect/references/codebase-mapper.md +2 -1
- package/skills/gsd-path-inspect/references/dispatch.md +13 -0
- package/skills/gsd-path-inspect/references/docs-auditor.md +7 -1
- package/skills/gsd-path-inspect/references/operating-rules.md +214 -0
- package/skills/gsd-path-inspect/scripts/_common.py +619 -18
- package/skills/gsd-path-inspect/scripts/archive_milestone.py +160 -86
- package/skills/gsd-path-inspect/scripts/build_recovery.py +100 -32
- package/skills/gsd-path-inspect/scripts/check_docs_audit.py +91 -7
- package/skills/gsd-path-inspect/scripts/check_handoffs.py +662 -50
- package/skills/gsd-path-inspect/scripts/check_task_briefs.py +338 -30
- package/skills/gsd-path-inspect/scripts/detect_project.py +587 -32
- package/skills/gsd-path-inspect/scripts/discussion_records.py +19 -13
- package/skills/gsd-path-inspect/scripts/discussion_validate.py +152 -50
- package/skills/gsd-path-inspect/scripts/integration.py +580 -25
- package/skills/gsd-path-inspect/scripts/isolation.py +1438 -57
- package/skills/gsd-path-inspect/scripts/loop_run.py +31 -29
- package/skills/gsd-path-inspect/scripts/members.py +706 -0
- package/skills/gsd-path-inspect/scripts/path_config.py +5 -5
- package/skills/gsd-path-inspect/scripts/pipeline_git.py +124 -34
- package/skills/gsd-path-inspect/scripts/pipeline_state.py +287 -87
- package/skills/gsd-path-inspect/scripts/promote_lookahead.py +1 -1
- package/skills/gsd-path-inspect/scripts/review_panel.py +1 -1
- package/skills/gsd-path-inspect/scripts/state_checkpoint.py +135 -44
- package/skills/gsd-path-inspect/scripts/state_promote.py +2 -0
- package/skills/gsd-path-inspect/scripts/workflow_run.py +107 -30
- package/skills/gsd-path-inspect/scripts/worktree_paths.py +72 -10
- package/skills/gsd-path-inspect/templates/codebase.md +1 -1
- package/skills/gsd-path-inspect/templates/docs-audit.md +6 -2
- package/skills/gsd-path-inspect/templates/state.md +1 -1
- package/skills/gsd-path-loop/SKILL.md +2 -1
- package/skills/gsd-path-loop/references/operating-rules.md +214 -0
- package/skills/gsd-path-loop/scripts/_common.py +619 -18
- package/skills/gsd-path-loop/scripts/discussion_records.py +19 -13
- package/skills/gsd-path-loop/scripts/loop_run.py +31 -29
- package/skills/gsd-path-loop/scripts/path_config.py +5 -5
- package/skills/gsd-path-loop/scripts/review_panel.py +1 -1
- package/skills/gsd-path-loop/scripts/worktree_paths.py +72 -10
- package/skills/gsd-path-migrate/SKILL.md +2 -1
- package/skills/gsd-path-migrate/references/operating-rules.md +214 -0
- package/skills/gsd-path-migrate/scripts/_common.py +619 -18
- package/skills/gsd-path-migrate/scripts/core_hook_gate.py +10 -1
- package/skills/gsd-path-migrate/scripts/core_hook_settings.py +11 -4
- package/skills/gsd-path-migrate/scripts/discussion_records.py +19 -13
- package/skills/gsd-path-migrate/scripts/loop_run.py +31 -29
- package/skills/gsd-path-migrate/scripts/migrate_core.py +2 -2
- package/skills/gsd-path-migrate/scripts/path_config.py +5 -5
- package/skills/gsd-path-migrate/scripts/review_panel.py +1 -1
- package/skills/gsd-path-plan/SKILL.md +18 -4
- package/skills/gsd-path-plan/references/dispatch.md +13 -0
- package/skills/gsd-path-plan/references/operating-rules.md +214 -0
- package/skills/gsd-path-plan/references/planner.md +22 -4
- package/skills/gsd-path-plan/references/spec-probes.md +150 -0
- package/skills/gsd-path-plan/scripts/_common.py +619 -18
- package/skills/gsd-path-plan/scripts/archive_milestone.py +160 -86
- package/skills/gsd-path-plan/scripts/build_recovery.py +100 -32
- package/skills/gsd-path-plan/scripts/check_handoffs.py +662 -50
- package/skills/gsd-path-plan/scripts/check_task_briefs.py +338 -30
- package/skills/gsd-path-plan/scripts/detect_project.py +587 -32
- package/skills/gsd-path-plan/scripts/discussion_records.py +19 -13
- package/skills/gsd-path-plan/scripts/discussion_validate.py +152 -50
- package/skills/gsd-path-plan/scripts/integration.py +580 -25
- package/skills/gsd-path-plan/scripts/isolation.py +1438 -57
- package/skills/gsd-path-plan/scripts/loop_run.py +31 -29
- package/skills/gsd-path-plan/scripts/members.py +706 -0
- package/skills/gsd-path-plan/scripts/path_config.py +5 -5
- package/skills/gsd-path-plan/scripts/pipeline_git.py +124 -34
- package/skills/gsd-path-plan/scripts/pipeline_state.py +287 -87
- package/skills/gsd-path-plan/scripts/promote_lookahead.py +1 -1
- package/skills/gsd-path-plan/scripts/review_panel.py +1 -1
- package/skills/gsd-path-plan/scripts/state_checkpoint.py +135 -44
- package/skills/gsd-path-plan/scripts/state_promote.py +2 -0
- package/skills/gsd-path-plan/scripts/workflow_run.py +107 -30
- package/skills/gsd-path-plan/scripts/worktree_paths.py +72 -10
- package/skills/gsd-path-plan/templates/patch-findings.md +3 -3
- package/skills/gsd-path-plan/templates/plan.md +17 -2
- package/skills/gsd-path-plan/templates/task.md +1 -0
- package/skills/gsd-path-research/SKILL.md +13 -1
- package/skills/gsd-path-research/references/dispatch.md +13 -0
- package/skills/gsd-path-research/references/operating-rules.md +214 -0
- package/skills/gsd-path-research/scripts/_common.py +619 -18
- package/skills/gsd-path-research/scripts/archive_milestone.py +160 -86
- package/skills/gsd-path-research/scripts/build_recovery.py +100 -32
- package/skills/gsd-path-research/scripts/check_handoffs.py +662 -50
- package/skills/gsd-path-research/scripts/check_task_briefs.py +338 -30
- package/skills/gsd-path-research/scripts/detect_project.py +587 -32
- package/skills/gsd-path-research/scripts/discussion_records.py +19 -13
- package/skills/gsd-path-research/scripts/discussion_validate.py +152 -50
- package/skills/gsd-path-research/scripts/integration.py +580 -25
- package/skills/gsd-path-research/scripts/isolation.py +1438 -57
- package/skills/gsd-path-research/scripts/loop_run.py +31 -29
- package/skills/gsd-path-research/scripts/members.py +706 -0
- package/skills/gsd-path-research/scripts/path_config.py +5 -5
- package/skills/gsd-path-research/scripts/pipeline_git.py +124 -34
- package/skills/gsd-path-research/scripts/pipeline_state.py +287 -87
- package/skills/gsd-path-research/scripts/promote_lookahead.py +1 -1
- package/skills/gsd-path-research/scripts/review_panel.py +1 -1
- package/skills/gsd-path-research/scripts/state_checkpoint.py +135 -44
- package/skills/gsd-path-research/scripts/state_promote.py +2 -0
- package/skills/gsd-path-research/scripts/worktree_paths.py +72 -10
- package/skills/gsd-path-research/templates/research-handoff.md +14 -3
- package/skills/gsd-path-roadmap/SKILL.md +2 -1
- package/skills/gsd-path-roadmap/references/dispatch.md +13 -0
- package/skills/gsd-path-roadmap/references/operating-rules.md +214 -0
- package/skills/gsd-path-roadmap/scripts/_common.py +619 -18
- package/skills/gsd-path-roadmap/scripts/archive_milestone.py +160 -86
- package/skills/gsd-path-roadmap/scripts/build_recovery.py +100 -32
- package/skills/gsd-path-roadmap/scripts/check_handoffs.py +662 -50
- package/skills/gsd-path-roadmap/scripts/check_task_briefs.py +338 -30
- package/skills/gsd-path-roadmap/scripts/detect_project.py +587 -32
- package/skills/gsd-path-roadmap/scripts/discussion_records.py +19 -13
- package/skills/gsd-path-roadmap/scripts/discussion_validate.py +152 -50
- package/skills/gsd-path-roadmap/scripts/integration.py +580 -25
- package/skills/gsd-path-roadmap/scripts/isolation.py +1438 -57
- package/skills/gsd-path-roadmap/scripts/loop_run.py +31 -29
- package/skills/gsd-path-roadmap/scripts/members.py +706 -0
- package/skills/gsd-path-roadmap/scripts/path_config.py +5 -5
- package/skills/gsd-path-roadmap/scripts/pipeline_git.py +124 -34
- package/skills/gsd-path-roadmap/scripts/pipeline_state.py +287 -87
- package/skills/gsd-path-roadmap/scripts/promote_lookahead.py +1 -1
- package/skills/gsd-path-roadmap/scripts/review_panel.py +1 -1
- package/skills/gsd-path-roadmap/scripts/state_checkpoint.py +135 -44
- package/skills/gsd-path-roadmap/scripts/state_promote.py +2 -0
- package/skills/gsd-path-roadmap/scripts/worktree_paths.py +72 -10
- package/skills/gsd-path-ship/SKILL.md +33 -11
- package/skills/gsd-path-ship/references/dispatch.md +13 -0
- package/skills/gsd-path-ship/references/operating-rules.md +214 -0
- package/skills/gsd-path-ship/references/reviewer-final.md +20 -2
- package/skills/gsd-path-ship/references/reviewer-wave.md +18 -5
- package/skills/gsd-path-ship/references/reviewer.md +3 -1
- package/skills/gsd-path-ship/references/spec-probes.md +150 -0
- package/skills/gsd-path-ship/scripts/_common.py +619 -18
- package/skills/gsd-path-ship/scripts/archive_milestone.py +160 -86
- package/skills/gsd-path-ship/scripts/build_recovery.py +100 -32
- package/skills/gsd-path-ship/scripts/build_state.py +143 -24
- package/skills/gsd-path-ship/scripts/check_handoffs.py +662 -50
- package/skills/gsd-path-ship/scripts/check_task_briefs.py +338 -30
- package/skills/gsd-path-ship/scripts/detect_project.py +587 -32
- package/skills/gsd-path-ship/scripts/discussion_records.py +19 -13
- package/skills/gsd-path-ship/scripts/discussion_validate.py +152 -50
- package/skills/gsd-path-ship/scripts/integration.py +580 -25
- package/skills/gsd-path-ship/scripts/isolation.py +1438 -57
- package/skills/gsd-path-ship/scripts/lean_verification.py +99 -19
- package/skills/gsd-path-ship/scripts/loop_run.py +31 -29
- package/skills/gsd-path-ship/scripts/members.py +706 -0
- package/skills/gsd-path-ship/scripts/path_config.py +5 -5
- package/skills/gsd-path-ship/scripts/pipeline_git.py +124 -34
- package/skills/gsd-path-ship/scripts/pipeline_state.py +287 -87
- package/skills/gsd-path-ship/scripts/promote_lookahead.py +1 -1
- package/skills/gsd-path-ship/scripts/review_panel.py +1 -1
- package/skills/gsd-path-ship/scripts/state_checkpoint.py +135 -44
- package/skills/gsd-path-ship/scripts/state_promote.py +2 -0
- package/skills/gsd-path-ship/scripts/workflow_run.py +107 -30
- package/skills/gsd-path-ship/scripts/worktree_paths.py +72 -10
- package/skills/gsd-path-ship/templates/final-review.md +16 -4
- package/skills/gsd-path-ship/templates/gap-review.md +2 -1
- package/skills/gsd-path-ship/templates/patch-findings.md +3 -3
- package/skills/gsd-path-ship/templates/wave-review.md +16 -4
- package/skills/gsd-path-undo/SKILL.md +2 -1
- package/skills/gsd-path-undo/references/operating-rules.md +214 -0
- package/skills/gsd-path-undo/scripts/_common.py +619 -18
- package/skills/gsd-path-undo/scripts/discussion_records.py +19 -13
- package/skills/gsd-path-undo/scripts/loop_run.py +31 -29
- package/skills/gsd-path-undo/scripts/path_config.py +5 -5
- package/skills/gsd-path-undo/scripts/pipeline_undo.py +218 -30
- package/skills/gsd-path-undo/scripts/review_panel.py +1 -1
- package/skills/gsd-path-undo/scripts/worktree_paths.py +72 -10
- package/skills/path/BUILD.md +7 -1
- package/skills/path/DECIDE.md +11 -4
- package/skills/path/DEFINE.md +36 -4
- package/skills/path/DOCS-AUDIT.md +6 -4
- package/skills/path/INSPECT.md +5 -4
- package/skills/path/PLAN.md +18 -4
- package/skills/path/RESEARCH.md +13 -1
- package/skills/path/ROADMAP.md +2 -1
- package/skills/path/SHIP.md +33 -11
- package/skills/path/SKILL.md +45 -1
- package/skills/path/references/build-native.md +63 -15
- package/skills/path/references/codebase-mapper.md +2 -1
- package/skills/path/references/dispatch.md +13 -0
- package/skills/path/references/docs-auditor.md +7 -1
- package/skills/path/references/operating-rules.md +214 -0
- package/skills/path/references/planner.md +22 -4
- package/skills/path/references/reviewer-final.md +20 -2
- package/skills/path/references/reviewer-wave.md +18 -5
- package/skills/path/references/reviewer.md +3 -1
- package/skills/path/references/spec-probes.md +150 -0
- package/skills/path/scripts/_common.py +619 -18
- package/skills/path/scripts/archive_milestone.py +160 -86
- package/skills/path/scripts/bootstrap_repository.py +8 -5
- package/skills/path/scripts/build_recovery.py +100 -32
- package/skills/path/scripts/build_state.py +143 -24
- package/skills/path/scripts/check_docs_audit.py +91 -7
- package/skills/path/scripts/check_handoffs.py +662 -50
- package/skills/path/scripts/check_task_briefs.py +338 -30
- package/skills/path/scripts/check_update.py +2 -2
- package/skills/path/scripts/detect_project.py +587 -32
- package/skills/path/scripts/discussion_records.py +19 -13
- package/skills/path/scripts/discussion_validate.py +152 -50
- package/skills/path/scripts/dispatch_driver.py +501 -46
- package/skills/path/scripts/git_guard.py +118 -31
- package/skills/path/scripts/integration.py +580 -25
- package/skills/path/scripts/isolation.py +1438 -57
- package/skills/path/scripts/lean_verification.py +99 -19
- package/skills/path/scripts/loop_run.py +31 -29
- package/skills/path/scripts/members.py +706 -0
- package/skills/path/scripts/path_config.py +5 -5
- package/skills/path/scripts/pipeline_git.py +124 -34
- package/skills/path/scripts/pipeline_state.py +287 -87
- package/skills/path/scripts/promote_lookahead.py +1 -1
- package/skills/path/scripts/review_panel.py +1 -1
- package/skills/path/scripts/state_checkpoint.py +135 -44
- package/skills/path/scripts/state_promote.py +2 -0
- package/skills/path/scripts/status_runtime.py +58 -10
- package/skills/path/scripts/task_context.py +12 -3
- package/skills/path/scripts/token_budget.py +12 -8
- package/skills/path/scripts/workflow_run.py +107 -30
- package/skills/path/scripts/worktree_paths.py +72 -10
- package/skills/path/templates/codebase.md +1 -1
- package/skills/path/templates/docs-audit.md +6 -2
- package/skills/path/templates/final-review.md +16 -4
- package/skills/path/templates/gap-review.md +2 -1
- package/skills/path/templates/intent.md +30 -0
- package/skills/path/templates/patch-findings.md +3 -3
- package/skills/path/templates/plan.md +17 -2
- package/skills/path/templates/research-handoff.md +14 -3
- package/skills/path/templates/state.md +1 -1
- package/skills/path/templates/task.md +1 -0
- package/skills/path/templates/wave-review.md +16 -4
- package/skills/gsd-path/scripts/__pycache__/_common.cpython-312.pyc +0 -0
- package/skills/gsd-path/scripts/__pycache__/check_handoffs.cpython-312.pyc +0 -0
- package/skills/gsd-path-build/scripts/__pycache__/_common.cpython-312.pyc +0 -0
- package/skills/gsd-path-build/scripts/__pycache__/check_handoffs.cpython-312.pyc +0 -0
- package/skills/gsd-path-inspect/scripts/__pycache__/_common.cpython-312.pyc +0 -0
- package/skills/gsd-path-plan/scripts/__pycache__/_common.cpython-312.pyc +0 -0
- package/skills/gsd-path-ship/scripts/__pycache__/_common.cpython-312.pyc +0 -0
package/scripts/install.py
CHANGED
|
@@ -2,15 +2,18 @@
|
|
|
2
2
|
"""Install GSD Path skills for supported coding agents."""
|
|
3
3
|
|
|
4
4
|
import argparse
|
|
5
|
+
import difflib
|
|
5
6
|
import errno
|
|
6
7
|
import hashlib
|
|
7
8
|
import json
|
|
8
9
|
import os
|
|
9
10
|
import re
|
|
11
|
+
import shlex
|
|
10
12
|
import shutil
|
|
11
13
|
import subprocess
|
|
12
14
|
import sys
|
|
13
15
|
import tempfile
|
|
16
|
+
import time
|
|
14
17
|
from dataclasses import dataclass, field
|
|
15
18
|
from pathlib import Path
|
|
16
19
|
from typing import Callable, Iterable, List, Mapping, Optional, Sequence, Tuple, Union
|
|
@@ -45,6 +48,10 @@ LOCAL_ROOTS = local_roots_for_manifest(sync_skill_resources.RESOURCE_MANIFEST)
|
|
|
45
48
|
SKILL_NAMES = skill_names_for_manifest(sync_skill_resources.RESOURCE_MANIFEST)
|
|
46
49
|
SKILL_ALIASES = dict(sync_skill_resources.RESOURCE_MANIFEST["skill_aliases"])
|
|
47
50
|
ROUTER_ALIASES = dict(sync_skill_resources.RESOURCE_MANIFEST["router_aliases"])
|
|
51
|
+
LOCAL_EDITS_NOTE = (
|
|
52
|
+
"; local edits inside these skills are not carried forward "
|
|
53
|
+
"(see UPDATE.md#local-additions)"
|
|
54
|
+
)
|
|
48
55
|
CLAUDE_BRIDGE = "@../AGENTS.md\n@../WORKFLOW.md\n"
|
|
49
56
|
HOOKS_DIRECTORY = ".gsd-path"
|
|
50
57
|
GUARD_SCRIPTS = ("guard_hook.py", "git_guard.py")
|
|
@@ -56,6 +63,21 @@ PROJECT_STATUS_MARKER = "gsd-path project status launcher"
|
|
|
56
63
|
INSTALL_LOCK_NAME = ".gsd-path-install-lock"
|
|
57
64
|
INSTALL_LOCK_OWNER = "owner.json"
|
|
58
65
|
INSTALL_LOCK_SCHEMA = "gsd-path/install-lock/v2"
|
|
66
|
+
AGENTS_BEGIN = "<!-- gsd-path:begin -->"
|
|
67
|
+
AGENTS_END = "<!-- gsd-path:end -->"
|
|
68
|
+
# Codex default project_doc_max_bytes; it silently cuts the rest of AGENTS.md.
|
|
69
|
+
CODEX_DOC_LIMIT = 32768
|
|
70
|
+
# sha256 of each released whole-file AGENTS.md (v1.0.0, v1.1.0, v1.2.0-v1.3.1).
|
|
71
|
+
RELEASED_AGENTS = frozenset(
|
|
72
|
+
{
|
|
73
|
+
"dfc969299e10c4430742c66c273436136a813d75e332cecf9403f94c904b330b",
|
|
74
|
+
"3a5c4ed62f397725acce962a7fdfafd5a3296f0a784acafc55454b5304a4eb7c",
|
|
75
|
+
"831de61d4b4b624319930162d45ac88c49cc13ca22894f37733ba68e6503f972",
|
|
76
|
+
}
|
|
77
|
+
)
|
|
78
|
+
LEGACY_AGENTS_TITLE = "# AGENTS.md — Operating Rules for the GSD Path Pipeline"
|
|
79
|
+
# Every released whole-file AGENTS.md carries this marker.
|
|
80
|
+
LEGACY_AGENTS_MARKER = "<!-- gsd-path/plain-prompt-reentry/v1 -->"
|
|
59
81
|
PROJECT_CONTRACTS = (
|
|
60
82
|
("AGENTS.md", "## Plain-prompt re-entry"),
|
|
61
83
|
("WORKFLOW.md", "### Plain-prompt re-entry"),
|
|
@@ -102,7 +124,7 @@ STATUS_STATE_FIELDS = frozenset(
|
|
|
102
124
|
"integration_source",
|
|
103
125
|
}
|
|
104
126
|
)
|
|
105
|
-
STATUS_INTEGRATION_MODES = frozenset({"direct", "pull-request"})
|
|
127
|
+
STATUS_INTEGRATION_MODES = frozenset({"direct", "pull-request", "external-landing"})
|
|
106
128
|
STATUS_INTEGRATION_SOURCES = frozenset({"default", "milestone"})
|
|
107
129
|
STATUS_SLUG = re.compile(r"^[a-z0-9][a-z0-9-]*$")
|
|
108
130
|
STATUS_BRANCH = re.compile(r"^gsd-path/M(\d{3,})$")
|
|
@@ -231,19 +253,149 @@ GIT_HOOKS = (
|
|
|
231
253
|
("pre-push", pre_push_hook),
|
|
232
254
|
)
|
|
233
255
|
GIT_HOOK_NAMES = tuple(name for name, _ in GIT_HOOKS)
|
|
256
|
+
MEMBER_HOOK_MARKER = "gsd-path member guard"
|
|
257
|
+
CHAINED_HOOK_SUFFIX = ".gsd-path-chained"
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def member_hook(interpreter: str, name: str) -> str:
|
|
261
|
+
"""A member hook finds its coordinator through the member marker, so a
|
|
262
|
+
moved coordinator needs only `members.py repair`, then runs any chained hook."""
|
|
263
|
+
guard = f'{interpreter} "$coordinator/{HOOKS_DIRECTORY}/git_guard.py" {name} "$@"'
|
|
264
|
+
lines = [
|
|
265
|
+
"#!/bin/sh",
|
|
266
|
+
f"# {MEMBER_HOOK_MARKER}: runs the coordinator's git_guard.py named by this",
|
|
267
|
+
"# repository's member marker, then any chained hook.",
|
|
268
|
+
"common=$(git rev-parse --path-format=absolute --git-common-dir) || exit 1",
|
|
269
|
+
f"coordinator=$({interpreter} -c 'import json, sys; print(json.load(open(sys.argv[1]))[\"coordinator\"])' "
|
|
270
|
+
'"$common/gsd-path/member.json") || {',
|
|
271
|
+
' echo "gsd-path guard: member marker is unreadable; run members.py repair --repo <coordinator>" >&2',
|
|
272
|
+
" exit 1",
|
|
273
|
+
"}",
|
|
274
|
+
f'chained="$(dirname "$0")/{name}{CHAINED_HOOK_SUFFIX}"',
|
|
275
|
+
]
|
|
276
|
+
if name == "pre-push":
|
|
277
|
+
# Git gives pre-push its ref updates on stdin; both hooks need them.
|
|
278
|
+
lines += [
|
|
279
|
+
"input=$(cat)",
|
|
280
|
+
'[ -n "$input" ] && input="$input\n"',
|
|
281
|
+
f'printf %s "$input" | {guard} || exit 1',
|
|
282
|
+
'if [ -x "$chained" ]; then printf %s "$input" | exec "$chained" "$@"; fi',
|
|
283
|
+
]
|
|
284
|
+
else:
|
|
285
|
+
lines += [f"{guard} || exit 1", 'if [ -x "$chained" ]; then exec "$chained" "$@"; fi']
|
|
286
|
+
return "\n".join(lines) + "\n"
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
def install_member_hooks(coordinator: Path, member: Path, dry_run: bool = False) -> List[str]:
|
|
290
|
+
"""Install git hooks that hold a member repo to its coordinator's guard.
|
|
291
|
+
|
|
292
|
+
Unmanaged hooks are kept as `<hook>.gsd-path-chained` and run after the
|
|
293
|
+
guard passes; the member's own single-repo Path hooks are replaced.
|
|
294
|
+
"""
|
|
295
|
+
_validate_project_git_root(member)
|
|
296
|
+
# Imported here so other installer commands keep their small import closure.
|
|
297
|
+
try:
|
|
298
|
+
from . import members
|
|
299
|
+
except ImportError:
|
|
300
|
+
import members
|
|
301
|
+
try:
|
|
302
|
+
role = members.member_role(member)
|
|
303
|
+
except members.MembersError as error:
|
|
304
|
+
raise InstallerError(str(error)) from error
|
|
305
|
+
if role is None or not _same_path(Path(role["coordinator"]), coordinator):
|
|
306
|
+
raise InstallerError(f"{member} is not a member of {coordinator}; run members.py add first")
|
|
307
|
+
guard = coordinator / HOOKS_DIRECTORY / "git_guard.py"
|
|
308
|
+
if guard.is_symlink() or not _is_managed_guard_script(guard):
|
|
309
|
+
raise InstallerError(f"coordinator guard is not installed: {guard}")
|
|
310
|
+
hooks_dir = _resolve_git_hooks_path(member)
|
|
311
|
+
if hooks_dir is None:
|
|
312
|
+
raise InstallerError(f"cannot resolve the git hooks directory of {member}")
|
|
313
|
+
common = Path(subprocess.run(
|
|
314
|
+
["git", "rev-parse", "--path-format=absolute", "--git-common-dir"],
|
|
315
|
+
cwd=os.fspath(member), capture_output=True, encoding="utf-8", errors="replace", check=True,
|
|
316
|
+
).stdout.strip()).resolve()
|
|
317
|
+
resolved = hooks_dir.resolve()
|
|
318
|
+
worktree_output = subprocess.run(
|
|
319
|
+
["git", "worktree", "list", "--porcelain", "-z"],
|
|
320
|
+
cwd=os.fspath(member), capture_output=True, check=True,
|
|
321
|
+
).stdout
|
|
322
|
+
worktrees = [
|
|
323
|
+
Path(os.fsdecode(field[len(b"worktree "):])).resolve()
|
|
324
|
+
for field in worktree_output.split(b"\0") if field.startswith(b"worktree ")
|
|
325
|
+
]
|
|
326
|
+
if not worktrees or (
|
|
327
|
+
any(resolved.is_relative_to(root) for root in worktrees)
|
|
328
|
+
and not resolved.is_relative_to(common)
|
|
329
|
+
):
|
|
330
|
+
raise InstallerError(
|
|
331
|
+
f"git hooks path is inside the member worktree: {hooks_dir}; "
|
|
332
|
+
"point core.hooksPath outside the worktree or unset it"
|
|
333
|
+
)
|
|
334
|
+
interpreter = _required_python_runtime("--member-of")
|
|
335
|
+
plan = []
|
|
336
|
+
for name in GIT_HOOK_NAMES:
|
|
337
|
+
hook, chained = hooks_dir / name, hooks_dir / f"{name}{CHAINED_HOOK_SUFFIX}"
|
|
338
|
+
if hook.is_symlink() or chained.is_symlink():
|
|
339
|
+
raise InstallerError(f"refusing a symlinked git hook: {hook}")
|
|
340
|
+
if _lexists(hook) and not hook.is_file():
|
|
341
|
+
raise InstallerError(f"git hook is not a regular file: {hook}")
|
|
342
|
+
text = hook.read_text(encoding="utf-8", errors="replace") if hook.is_file() else None
|
|
343
|
+
chain = (
|
|
344
|
+
text is not None
|
|
345
|
+
and MEMBER_HOOK_MARKER not in text
|
|
346
|
+
and not _is_managed_git_hook_content(text)
|
|
347
|
+
)
|
|
348
|
+
if chain and _lexists(chained):
|
|
349
|
+
raise InstallerError(f"cannot chain {hook}: {chained} already exists")
|
|
350
|
+
plan.append((name, hook, chained, chain))
|
|
351
|
+
lines = []
|
|
352
|
+
for name, hook, chained, chain in plan:
|
|
353
|
+
note = " (keeps the existing hook as a chained hook)" if chain else ""
|
|
354
|
+
lines.append(f"member hook: {hook}{note}" + (" (dry run)" if dry_run else ""))
|
|
355
|
+
if dry_run:
|
|
356
|
+
return lines
|
|
357
|
+
hooks_dir.mkdir(parents=True, exist_ok=True)
|
|
358
|
+
staged = []
|
|
359
|
+
try:
|
|
360
|
+
for name, _, _, _ in plan:
|
|
361
|
+
temporary = hooks_dir / f".{name}.gsd-path-tmp"
|
|
362
|
+
# O_EXCL already refuses any existing name; O_NOFOLLOW (absent on
|
|
363
|
+
# Windows) adds defense in depth where it exists.
|
|
364
|
+
flags = (os.O_WRONLY | os.O_CREAT | os.O_EXCL
|
|
365
|
+
| getattr(os, "O_NOFOLLOW", 0) | getattr(os, "O_BINARY", 0))
|
|
366
|
+
fd = os.open(temporary, flags, 0o755)
|
|
367
|
+
staged.append(temporary)
|
|
368
|
+
with os.fdopen(fd, "wb") as output:
|
|
369
|
+
if hasattr(os, "fchmod"): # Windows before 3.13; Git for Windows needs no exec bit.
|
|
370
|
+
os.fchmod(output.fileno(), 0o755)
|
|
371
|
+
output.write(member_hook(interpreter, name).encode("utf-8"))
|
|
372
|
+
for name, hook, chained, chain in plan:
|
|
373
|
+
if chain:
|
|
374
|
+
os.replace(hook, chained)
|
|
375
|
+
os.replace(hooks_dir / f".{name}.gsd-path-tmp", hook)
|
|
376
|
+
finally:
|
|
377
|
+
for temporary in staged:
|
|
378
|
+
temporary.unlink(missing_ok=True)
|
|
379
|
+
return lines
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
# Every interpreter a managed hook may be pinned to; each is a command prefix.
|
|
383
|
+
INTERPRETER_CANDIDATES = ("python3", "python", "py -3")
|
|
234
384
|
|
|
235
385
|
|
|
236
386
|
def _detect_python_interpreter() -> Optional[str]:
|
|
237
|
-
"""Probe for a runnable interpreter (python3, then python).
|
|
387
|
+
"""Probe for a runnable interpreter (python3, then python, then py -3).
|
|
238
388
|
|
|
239
389
|
Emitted hooks must never hard-code an interpreter that does not exist on
|
|
240
|
-
this machine (python3 is typically absent on Windows
|
|
390
|
+
this machine (python3 is typically absent on Windows, and python.org
|
|
391
|
+
installs may put only the py launcher on PATH). The result is a command
|
|
392
|
+
prefix; split it before spawning.
|
|
241
393
|
"""
|
|
242
|
-
for candidate in
|
|
394
|
+
for candidate in INTERPRETER_CANDIDATES:
|
|
243
395
|
try:
|
|
244
396
|
result = subprocess.run(
|
|
245
397
|
[
|
|
246
|
-
candidate,
|
|
398
|
+
*candidate.split(),
|
|
247
399
|
"-B",
|
|
248
400
|
"-c",
|
|
249
401
|
"import sys; raise SystemExit(sys.version_info < (3, 9))",
|
|
@@ -277,7 +429,7 @@ def _resolve_git_hooks_path(project: "Path") -> Optional["Path"]:
|
|
|
277
429
|
["git", "rev-parse", "--show-toplevel", "--git-path", "hooks"],
|
|
278
430
|
cwd=os.fspath(project),
|
|
279
431
|
capture_output=True,
|
|
280
|
-
|
|
432
|
+
encoding="utf-8", errors="replace",
|
|
281
433
|
check=False,
|
|
282
434
|
)
|
|
283
435
|
except OSError:
|
|
@@ -353,9 +505,9 @@ HOST_NOTES = {
|
|
|
353
505
|
"kiro": KIRO_NOTE,
|
|
354
506
|
}
|
|
355
507
|
EXPLICIT_ONLY_TARGETS = frozenset(
|
|
356
|
-
{"claude", "grok", "copilot", "qwen", "cursor", "zed", "kimi", "shared-agents"}
|
|
508
|
+
{"claude", "grok", "copilot", "qwen", "cursor", "zed", "kimi", "muse", "shared-agents"}
|
|
357
509
|
)
|
|
358
|
-
SHARED_AGENT_TARGETS = frozenset({"codex", "antigravity", "zed"})
|
|
510
|
+
SHARED_AGENT_TARGETS = frozenset({"codex", "antigravity", "zed", "muse"})
|
|
359
511
|
SHARED_AGENT_PROFILE = "shared-agents"
|
|
360
512
|
CURSOR_AGENT_FILENAME = "gsd-path.md"
|
|
361
513
|
CURSOR_AGENT_BACKUP_NAME = "cursor-agent-gsd-path.md"
|
|
@@ -381,7 +533,7 @@ def _shared_invocations(text: str) -> str:
|
|
|
381
533
|
others = f"{hosts['antigravity']['invocation_prefix']}{skill}"
|
|
382
534
|
return (
|
|
383
535
|
f"{quote}{codex}{arguments}{quote} (Codex) or "
|
|
384
|
-
f"{quote}{others}{arguments}{quote} (Antigravity/Zed)"
|
|
536
|
+
f"{quote}{others}{arguments}{quote} (Antigravity/Zed/Muse)"
|
|
385
537
|
)
|
|
386
538
|
|
|
387
539
|
return re.sub(
|
|
@@ -390,7 +542,7 @@ def _shared_invocations(text: str) -> str:
|
|
|
390
542
|
r"(?: \(Codex\) (?:and|or) (?P<paired_quote>`?)/"
|
|
391
543
|
r"(?P<paired_skill>gsd-path(?:-[a-z0-9]+)*|path)"
|
|
392
544
|
r"(?P<paired_arguments> status)?(?P=paired_quote)"
|
|
393
|
-
r" \((?:other hosts|Antigravity/Zed)\))?",
|
|
545
|
+
r" \((?:other hosts|Antigravity/Zed/Muse)\))?",
|
|
394
546
|
replace,
|
|
395
547
|
text,
|
|
396
548
|
)
|
|
@@ -468,7 +620,7 @@ def default_root(target: str, environ: Optional[Mapping[str, str]] = None) -> Pa
|
|
|
468
620
|
return absolute_path(Path("~/.gemini/antigravity-cli/skills"))
|
|
469
621
|
if target == "cursor":
|
|
470
622
|
return absolute_path(Path("~/.cursor/skills"))
|
|
471
|
-
if target
|
|
623
|
+
if target in ("zed", "muse"):
|
|
472
624
|
return absolute_path(Path("~/.agents/skills"))
|
|
473
625
|
if target == "kiro":
|
|
474
626
|
return absolute_path(Path(env.get("KIRO_HOME") or "~/.kiro") / "skills")
|
|
@@ -504,7 +656,15 @@ def _is_owned_router_alias(skill_dir: Path) -> bool:
|
|
|
504
656
|
version = version_file.read_text(encoding="utf-8").strip()
|
|
505
657
|
except (OSError, UnicodeError):
|
|
506
658
|
return False
|
|
507
|
-
return
|
|
659
|
+
return (
|
|
660
|
+
re.fullmatch(
|
|
661
|
+
r"[0-9]+(?:\.[0-9]+)+"
|
|
662
|
+
r"(?:-[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?"
|
|
663
|
+
r"(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?",
|
|
664
|
+
version,
|
|
665
|
+
)
|
|
666
|
+
is not None
|
|
667
|
+
)
|
|
508
668
|
|
|
509
669
|
|
|
510
670
|
def _is_managed_install_entry(root: Path, name: str) -> bool:
|
|
@@ -534,7 +694,7 @@ def _stamp_installed_versions(staged_root: Path, version: Optional[str]) -> None
|
|
|
534
694
|
if not version:
|
|
535
695
|
return
|
|
536
696
|
for name in {"gsd-path", *ROUTER_ALIASES}:
|
|
537
|
-
(staged_root / name / "VERSION").
|
|
697
|
+
(staged_root / name / "VERSION").write_bytes(f"{version}\n".encode("utf-8"))
|
|
538
698
|
|
|
539
699
|
|
|
540
700
|
def _lexists(path: Path) -> bool:
|
|
@@ -670,7 +830,7 @@ def stage_target(source_root: Path, target: str, staged_root: Path) -> None:
|
|
|
670
830
|
encoding="utf-8"
|
|
671
831
|
)
|
|
672
832
|
for dispatch in sorted(staged_root.glob("*/references/dispatch.md")):
|
|
673
|
-
dispatch.
|
|
833
|
+
dispatch.write_bytes(adapter.encode("utf-8"))
|
|
674
834
|
if target == "cursor":
|
|
675
835
|
shutil.copy2(
|
|
676
836
|
source_root / "platforms" / "cursor" / "agent.md",
|
|
@@ -687,9 +847,8 @@ def stage_target(source_root: Path, target: str, staged_root: Path) -> None:
|
|
|
687
847
|
path_invocation = "path" if target == "opencode" else "/path"
|
|
688
848
|
for name in SKILL_NAMES:
|
|
689
849
|
entrypoint = staged_root / name / "SKILL.md"
|
|
690
|
-
entrypoint.
|
|
691
|
-
_augment_frontmatter(entrypoint.read_text(encoding="utf-8"), target),
|
|
692
|
-
encoding="utf-8",
|
|
850
|
+
entrypoint.write_bytes(
|
|
851
|
+
_augment_frontmatter(entrypoint.read_text(encoding="utf-8"), target).encode("utf-8"),
|
|
693
852
|
)
|
|
694
853
|
for markdown in sorted(staged_root.rglob("*.md")):
|
|
695
854
|
content = markdown.read_text(encoding="utf-8")
|
|
@@ -698,7 +857,7 @@ def stage_target(source_root: Path, target: str, staged_root: Path) -> None:
|
|
|
698
857
|
if target == SHARED_AGENT_PROFILE
|
|
699
858
|
else content.replace("$gsd-path", invocation).replace("$path", path_invocation)
|
|
700
859
|
)
|
|
701
|
-
markdown.
|
|
860
|
+
markdown.write_bytes(transformed.encode("utf-8"))
|
|
702
861
|
|
|
703
862
|
|
|
704
863
|
def _missing_directories(path: Path) -> List[Path]:
|
|
@@ -735,7 +894,7 @@ def _process_identity(pid: int) -> Optional[str]:
|
|
|
735
894
|
else:
|
|
736
895
|
command = ["ps", "-o", "lstart=", "-p", str(pid)]
|
|
737
896
|
try:
|
|
738
|
-
result = subprocess.run(command, capture_output=True,
|
|
897
|
+
result = subprocess.run(command, capture_output=True, encoding="utf-8", errors="replace", check=False)
|
|
739
898
|
except OSError:
|
|
740
899
|
return None
|
|
741
900
|
value = result.stdout.strip()
|
|
@@ -743,11 +902,39 @@ def _process_identity(pid: int) -> Optional[str]:
|
|
|
743
902
|
|
|
744
903
|
|
|
745
904
|
def _process_alive(pid: int) -> bool:
|
|
905
|
+
"""Whether pid names a running process, without signalling it.
|
|
906
|
+
|
|
907
|
+
Windows os.kill(pid, 0) sends CTRL_C_EVENT, so Windows asks the kernel.
|
|
908
|
+
status_runtime and install carry verbatim copies (they cannot import this
|
|
909
|
+
module); tests/test_common_platform.py keeps them identical.
|
|
910
|
+
"""
|
|
911
|
+
if pid <= 0:
|
|
912
|
+
return False
|
|
913
|
+
if os.name != "nt":
|
|
914
|
+
try:
|
|
915
|
+
os.kill(pid, 0)
|
|
916
|
+
except OSError as error:
|
|
917
|
+
return error.errno == errno.EPERM
|
|
918
|
+
return True
|
|
919
|
+
import ctypes
|
|
920
|
+
from ctypes import wintypes
|
|
921
|
+
|
|
922
|
+
kernel32 = ctypes.WinDLL("kernel32", use_last_error=True)
|
|
923
|
+
kernel32.OpenProcess.restype = wintypes.HANDLE
|
|
924
|
+
kernel32.OpenProcess.argtypes = (wintypes.DWORD, wintypes.BOOL, wintypes.DWORD)
|
|
925
|
+
kernel32.GetExitCodeProcess.argtypes = (wintypes.HANDLE, ctypes.POINTER(wintypes.DWORD))
|
|
926
|
+
kernel32.CloseHandle.argtypes = (wintypes.HANDLE,)
|
|
927
|
+
handle = kernel32.OpenProcess(0x1000, False, pid) # PROCESS_QUERY_LIMITED_INFORMATION
|
|
928
|
+
if not handle:
|
|
929
|
+
# ERROR_ACCESS_DENIED: a protected process exists but refuses the query.
|
|
930
|
+
return ctypes.get_last_error() == 5
|
|
746
931
|
try:
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
932
|
+
code = wintypes.DWORD()
|
|
933
|
+
if not kernel32.GetExitCodeProcess(handle, ctypes.byref(code)):
|
|
934
|
+
return True
|
|
935
|
+
return code.value == 259 # STILL_ACTIVE
|
|
936
|
+
finally:
|
|
937
|
+
kernel32.CloseHandle(handle)
|
|
751
938
|
|
|
752
939
|
|
|
753
940
|
def _stale_install_lock_snapshot(lock: Path) -> Tuple[os.stat_result, bytes]:
|
|
@@ -784,6 +971,31 @@ def _stale_install_lock_snapshot(lock: Path) -> Tuple[os.stat_result, bytes]:
|
|
|
784
971
|
return observed, owner_bytes
|
|
785
972
|
|
|
786
973
|
|
|
974
|
+
# Access denied and sharing violation: on Windows a scanner or indexer that
|
|
975
|
+
# briefly opens a freshly written file blocks moving it or its directory.
|
|
976
|
+
_WINDOWS_BUSY_ERRORS = (5, 32)
|
|
977
|
+
_MOVE_ATTEMPTS = 20
|
|
978
|
+
|
|
979
|
+
|
|
980
|
+
def _move_retrying(move: Callable[[Path, Path], object], source: Path, destination: Path) -> None:
|
|
981
|
+
"""Run ``move``, retried on Windows while another process briefly holds ``source``."""
|
|
982
|
+
if os.name != "nt":
|
|
983
|
+
move(source, destination)
|
|
984
|
+
return
|
|
985
|
+
for attempt in range(_MOVE_ATTEMPTS):
|
|
986
|
+
try:
|
|
987
|
+
move(source, destination)
|
|
988
|
+
return
|
|
989
|
+
except PermissionError as error:
|
|
990
|
+
# A directory moved onto an existing directory also reports access
|
|
991
|
+
# denied; that is not transient, so never retry it.
|
|
992
|
+
if (getattr(error, "winerror", None) not in _WINDOWS_BUSY_ERRORS
|
|
993
|
+
or os.path.isdir(destination)
|
|
994
|
+
or attempt == _MOVE_ATTEMPTS - 1):
|
|
995
|
+
raise
|
|
996
|
+
time.sleep(min(0.01 * 2 ** attempt, 0.25))
|
|
997
|
+
|
|
998
|
+
|
|
787
999
|
def _recover_stale_install_lock(lock: Path, quarantine: Path) -> Optional[Path]:
|
|
788
1000
|
legacy_quarantine = lock.with_name(f"{lock.name}.stale")
|
|
789
1001
|
if not _lexists(lock):
|
|
@@ -839,19 +1051,18 @@ def _create_install_lock(lock: Path) -> None:
|
|
|
839
1051
|
quarantine = None
|
|
840
1052
|
published = False
|
|
841
1053
|
try:
|
|
842
|
-
(staging / INSTALL_LOCK_OWNER).
|
|
843
|
-
json.dumps(
|
|
1054
|
+
(staging / INSTALL_LOCK_OWNER).write_bytes(
|
|
1055
|
+
(json.dumps(
|
|
844
1056
|
{
|
|
845
1057
|
"schema": INSTALL_LOCK_SCHEMA,
|
|
846
1058
|
"pid": os.getpid(),
|
|
847
1059
|
"identity": identity,
|
|
848
1060
|
}
|
|
849
1061
|
)
|
|
850
|
-
+ "\n",
|
|
851
|
-
encoding="utf-8",
|
|
1062
|
+
+ "\n").encode("utf-8"),
|
|
852
1063
|
)
|
|
853
1064
|
quarantine = _recover_stale_install_lock(lock, recovery)
|
|
854
|
-
|
|
1065
|
+
_move_retrying(Path.rename, staging, lock)
|
|
855
1066
|
published = True
|
|
856
1067
|
if quarantine is not None:
|
|
857
1068
|
shutil.rmtree(quarantine)
|
|
@@ -926,7 +1137,7 @@ def _backup_existing(
|
|
|
926
1137
|
for entry, backup_name in existing:
|
|
927
1138
|
stored = transaction.backup / backup_name
|
|
928
1139
|
transaction.moved.append((entry, stored))
|
|
929
|
-
os.replace
|
|
1140
|
+
_move_retrying(os.replace, entry, stored)
|
|
930
1141
|
|
|
931
1142
|
|
|
932
1143
|
def _reserve_directory(path: Path) -> None:
|
|
@@ -987,7 +1198,7 @@ def _rollback_target(transaction: TargetTransaction) -> None:
|
|
|
987
1198
|
_remove_path(destination)
|
|
988
1199
|
for original, stored in reversed(transaction.moved):
|
|
989
1200
|
if _lexists(stored):
|
|
990
|
-
os.replace
|
|
1201
|
+
_move_retrying(os.replace, stored, original)
|
|
991
1202
|
if transaction.backup is not None:
|
|
992
1203
|
try:
|
|
993
1204
|
transaction.backup.rmdir()
|
|
@@ -1082,7 +1293,8 @@ def _update_replacement(
|
|
|
1082
1293
|
project: Path, destination: Path, hooks_dir: Optional[Path]
|
|
1083
1294
|
) -> Optional[Callable[[Path], bool]]:
|
|
1084
1295
|
"""--update replaces managed runtime, guard, and git hook files in place
|
|
1085
|
-
and keeps everything else (
|
|
1296
|
+
and keeps everything else (WORKFLOW.md, CLAUDE.md); AGENTS.md has its own
|
|
1297
|
+
block merge. Returns
|
|
1086
1298
|
the managed-file check for a replaceable destination, or None for a
|
|
1087
1299
|
kept one."""
|
|
1088
1300
|
parent = project / HOOKS_DIRECTORY
|
|
@@ -1143,6 +1355,7 @@ def _project_result(
|
|
|
1143
1355
|
):
|
|
1144
1356
|
replaceable = (
|
|
1145
1357
|
destination in mergers
|
|
1358
|
+
or destination == project / "AGENTS.md"
|
|
1146
1359
|
or _update_replacement(project, destination, hooks_dir) is not None
|
|
1147
1360
|
)
|
|
1148
1361
|
bucket = kept if _lexists(destination) and not replaceable else refreshed
|
|
@@ -1155,13 +1368,140 @@ def _project_result(
|
|
|
1155
1368
|
|
|
1156
1369
|
|
|
1157
1370
|
def _existing_contract_error(destination: Path) -> "InstallerError":
|
|
1158
|
-
|
|
1371
|
+
message = (
|
|
1159
1372
|
f"project contract already exists: {destination} — the installer never "
|
|
1160
|
-
"overwrites project files.
|
|
1373
|
+
"overwrites project files."
|
|
1374
|
+
)
|
|
1375
|
+
document = destination.name in ("AGENTS.md", "WORKFLOW.md") or (
|
|
1376
|
+
destination.name == "CLAUDE.md" and destination.parent.name == ".claude"
|
|
1377
|
+
)
|
|
1378
|
+
if document:
|
|
1379
|
+
aside = destination.with_name(f"{destination.stem}.pre-path{destination.suffix}")
|
|
1380
|
+
if _lexists(aside):
|
|
1381
|
+
message += (
|
|
1382
|
+
f" First install: {aside} already exists; move the contract to an "
|
|
1383
|
+
f"unused name, rerun, then merge its rules into the new {destination.name}."
|
|
1384
|
+
)
|
|
1385
|
+
else:
|
|
1386
|
+
message += (
|
|
1387
|
+
f" First install: move it aside with `mv {shlex.quote(str(destination))} "
|
|
1388
|
+
f"{shlex.quote(str(aside))}`, rerun, then merge its rules into the new "
|
|
1389
|
+
f"{destination.name}."
|
|
1390
|
+
)
|
|
1391
|
+
message += " Already a GSD Path project:"
|
|
1392
|
+
return InstallerError(
|
|
1393
|
+
f"{message} Run --update --project PATH to refresh managed "
|
|
1161
1394
|
"runtime files; merge template changes manually (see UPDATE.md)."
|
|
1162
1395
|
)
|
|
1163
1396
|
|
|
1164
1397
|
|
|
1398
|
+
def _agents_block(template: str) -> str:
|
|
1399
|
+
return f"{AGENTS_BEGIN}\n{template.rstrip(chr(10))}\n{AGENTS_END}\n"
|
|
1400
|
+
|
|
1401
|
+
|
|
1402
|
+
def _split_agents(text: str, destination: Path) -> Optional[Tuple[str, str]]:
|
|
1403
|
+
"""Owner text (before, after) around Path's block, or None without markers.
|
|
1404
|
+
Markers count only as whole lines; inline mentions are owner text."""
|
|
1405
|
+
begins = [m.start() for m in re.finditer(rf"(?m)^{re.escape(AGENTS_BEGIN)}\r?$", text)]
|
|
1406
|
+
ends = list(re.finditer(rf"(?m)^{re.escape(AGENTS_END)}\r?(?:\n|$)", text))
|
|
1407
|
+
if not begins and not ends:
|
|
1408
|
+
return None
|
|
1409
|
+
if len(begins) != 1 or len(ends) != 1 or ends[0].start() < begins[0]:
|
|
1410
|
+
begins, ends = len(begins), len(ends)
|
|
1411
|
+
raise InstallerError(
|
|
1412
|
+
f"{destination} needs exactly one {AGENTS_BEGIN} line followed by one "
|
|
1413
|
+
f"{AGENTS_END} line; found {begins} begin and {ends} end markers. "
|
|
1414
|
+
"Fix the markers by hand, then rerun."
|
|
1415
|
+
)
|
|
1416
|
+
return text[:begins[0]], text[ends[0].end():]
|
|
1417
|
+
|
|
1418
|
+
|
|
1419
|
+
def _read_agents(destination: Path) -> Optional[bytes]:
|
|
1420
|
+
if not _lexists(destination):
|
|
1421
|
+
return None
|
|
1422
|
+
try:
|
|
1423
|
+
return destination.read_bytes()
|
|
1424
|
+
except OSError as error:
|
|
1425
|
+
raise InstallerError(f"cannot read {destination}: {error}") from error
|
|
1426
|
+
|
|
1427
|
+
|
|
1428
|
+
def _merged_agents(
|
|
1429
|
+
source_root: Path, destination: Path, raw: Optional[bytes] = None
|
|
1430
|
+
) -> bytes:
|
|
1431
|
+
"""AGENTS.md with Path's block inserted or replaced; owner text kept as is.
|
|
1432
|
+
raw is the current file (None when absent); read from disk when omitted."""
|
|
1433
|
+
template = (source_root / "AGENTS.md").read_text(encoding="utf-8")
|
|
1434
|
+
block = _agents_block(template)
|
|
1435
|
+
if raw is None:
|
|
1436
|
+
raw = _read_agents(destination)
|
|
1437
|
+
if raw is not None:
|
|
1438
|
+
try:
|
|
1439
|
+
text = raw.decode("utf-8")
|
|
1440
|
+
except UnicodeDecodeError as error:
|
|
1441
|
+
raise InstallerError(f"cannot read {destination}: {error}") from error
|
|
1442
|
+
parts = _split_agents(text, destination)
|
|
1443
|
+
if parts is not None:
|
|
1444
|
+
merged = parts[0] + block + parts[1]
|
|
1445
|
+
elif hashlib.sha256(raw).hexdigest() in RELEASED_AGENTS:
|
|
1446
|
+
merged = block # One-time migration of an unedited whole-file install.
|
|
1447
|
+
elif LEGACY_AGENTS_TITLE in text or re.search(
|
|
1448
|
+
rf"(?m)^{re.escape(LEGACY_AGENTS_MARKER)}\r?$", text
|
|
1449
|
+
):
|
|
1450
|
+
diff = "".join(
|
|
1451
|
+
difflib.unified_diff(
|
|
1452
|
+
template.splitlines(keepends=True),
|
|
1453
|
+
text.splitlines(keepends=True),
|
|
1454
|
+
"AGENTS.md (current GSD Path contract)",
|
|
1455
|
+
str(destination),
|
|
1456
|
+
)
|
|
1457
|
+
)
|
|
1458
|
+
raise InstallerError(
|
|
1459
|
+
f"{destination} is an edited whole-file GSD Path contract; Path cannot "
|
|
1460
|
+
"tell its text from your edits. Migrate by hand: 1) keep your own "
|
|
1461
|
+
f"rules from the diff below; 2) move the file aside with `mv "
|
|
1462
|
+
f"{shlex.quote(str(destination))} "
|
|
1463
|
+
f"{shlex.quote(str(destination.with_name('AGENTS.pre-path.md')))}`; "
|
|
1464
|
+
"3) rerun; 4) add your rules after the "
|
|
1465
|
+
f"{AGENTS_END} line.\n{diff}"
|
|
1466
|
+
)
|
|
1467
|
+
else:
|
|
1468
|
+
merged = block + "\n" + text # Block first: Codex cuts the tail.
|
|
1469
|
+
else:
|
|
1470
|
+
merged = block
|
|
1471
|
+
data = merged.encode("utf-8")
|
|
1472
|
+
if len(data) > CODEX_DOC_LIMIT:
|
|
1473
|
+
aside = destination.with_name("AGENTS.pre-path.md")
|
|
1474
|
+
raise InstallerError(
|
|
1475
|
+
f"{destination} would be {len(data)} bytes (existing file {len(raw or b'')} "
|
|
1476
|
+
f"bytes, GSD Path block {len(block.encode('utf-8'))} bytes). Codex reads "
|
|
1477
|
+
f"only the first {CODEX_DOC_LIMIT} bytes and silently cuts the rest. "
|
|
1478
|
+
f"Move it aside with `mv {shlex.quote(str(destination))} "
|
|
1479
|
+
f"{shlex.quote(str(aside))}`, rerun, then add back only the rules that fit."
|
|
1480
|
+
)
|
|
1481
|
+
return data
|
|
1482
|
+
|
|
1483
|
+
|
|
1484
|
+
def _apply_agents(
|
|
1485
|
+
source_root: Path, destination: Path, transaction: "ProjectTransaction"
|
|
1486
|
+
) -> None:
|
|
1487
|
+
original = _read_agents(destination)
|
|
1488
|
+
merged = _merged_agents(source_root, destination, original)
|
|
1489
|
+
if original is None:
|
|
1490
|
+
try:
|
|
1491
|
+
with destination.open("xb") as output:
|
|
1492
|
+
transaction.copied.append(destination)
|
|
1493
|
+
output.write(merged)
|
|
1494
|
+
except FileExistsError as error:
|
|
1495
|
+
raise _existing_contract_error(destination) from error
|
|
1496
|
+
return
|
|
1497
|
+
if original != merged:
|
|
1498
|
+
if _read_agents(destination) != original:
|
|
1499
|
+
raise InstallerError(f"{destination} changed during install; rerun")
|
|
1500
|
+
mode = destination.stat().st_mode & 0o777
|
|
1501
|
+
_atomic_write(destination, merged, mode)
|
|
1502
|
+
transaction.replaced.append((destination, original, mode))
|
|
1503
|
+
|
|
1504
|
+
|
|
1165
1505
|
def _validate_project_git_root(project: Path) -> None:
|
|
1166
1506
|
probe = project
|
|
1167
1507
|
while not _lexists(probe):
|
|
@@ -1171,7 +1511,7 @@ def _validate_project_git_root(project: Path) -> None:
|
|
|
1171
1511
|
["git", "rev-parse", "--show-toplevel"],
|
|
1172
1512
|
cwd=probe,
|
|
1173
1513
|
capture_output=True,
|
|
1174
|
-
|
|
1514
|
+
encoding="utf-8", errors="replace",
|
|
1175
1515
|
check=False,
|
|
1176
1516
|
env={**os.environ, "GIT_OPTIONAL_LOCKS": "0"},
|
|
1177
1517
|
)
|
|
@@ -1186,6 +1526,90 @@ def _validate_project_git_root(project: Path) -> None:
|
|
|
1186
1526
|
raise InstallerError(f"project path is not the Git worktree root: {project}")
|
|
1187
1527
|
|
|
1188
1528
|
|
|
1529
|
+
GIT_ATTRIBUTES_BEGIN = "# gsd-path:begin"
|
|
1530
|
+
GIT_ATTRIBUTES_END = "# gsd-path:end"
|
|
1531
|
+
# Pipeline state is compared and hashed byte for byte. Git for Windows installs
|
|
1532
|
+
# with core.autocrlf=true, which would check .project files out as CRLF in
|
|
1533
|
+
# every worktree; this local rule keeps them LF without touching product files.
|
|
1534
|
+
GIT_ATTRIBUTES_BLOCK = (
|
|
1535
|
+
f"{GIT_ATTRIBUTES_BEGIN}\n/.project/** text eol=lf\n{GIT_ATTRIBUTES_END}\n"
|
|
1536
|
+
)
|
|
1537
|
+
|
|
1538
|
+
|
|
1539
|
+
def _git_attributes_path(project: Path) -> Optional[Path]:
|
|
1540
|
+
"""The repository's local attributes file, shared by every worktree.
|
|
1541
|
+
|
|
1542
|
+
`--git-path info/attributes` resolves to the common git directory, so
|
|
1543
|
+
managed worktrees see the same rule. None when git cannot resolve it.
|
|
1544
|
+
"""
|
|
1545
|
+
try:
|
|
1546
|
+
result = subprocess.run(
|
|
1547
|
+
["git", "rev-parse", "--show-toplevel", "--git-path", "info/attributes"],
|
|
1548
|
+
cwd=os.fspath(project), capture_output=True,
|
|
1549
|
+
encoding="utf-8", errors="replace", check=False,
|
|
1550
|
+
)
|
|
1551
|
+
except OSError:
|
|
1552
|
+
return None
|
|
1553
|
+
lines = [line for line in result.stdout.splitlines() if line]
|
|
1554
|
+
if result.returncode != 0 or len(lines) != 2 or not _same_path(Path(lines[0]), project):
|
|
1555
|
+
return None
|
|
1556
|
+
return Path(os.path.abspath(os.path.join(os.fspath(project), lines[1])))
|
|
1557
|
+
|
|
1558
|
+
|
|
1559
|
+
def _merged_git_attributes(raw: Optional[bytes], destination: Path) -> bytes:
|
|
1560
|
+
"""The attributes file with Path's block inserted or replaced; other lines kept."""
|
|
1561
|
+
if raw is None:
|
|
1562
|
+
return GIT_ATTRIBUTES_BLOCK.encode("utf-8")
|
|
1563
|
+
text = raw.decode("utf-8", "surrogateescape")
|
|
1564
|
+
lines = text.splitlines(keepends=True)
|
|
1565
|
+
begins = [i for i, line in enumerate(lines) if line.rstrip("\r\n") == GIT_ATTRIBUTES_BEGIN]
|
|
1566
|
+
ends = [i for i, line in enumerate(lines) if line.rstrip("\r\n") == GIT_ATTRIBUTES_END]
|
|
1567
|
+
if not begins and not ends:
|
|
1568
|
+
separator = "" if not text or text.endswith("\n") else "\n"
|
|
1569
|
+
merged = text + separator + GIT_ATTRIBUTES_BLOCK
|
|
1570
|
+
elif len(begins) != 1 or len(ends) != 1 or ends[0] < begins[0]:
|
|
1571
|
+
raise InstallerError(
|
|
1572
|
+
f"{destination} needs exactly one {GIT_ATTRIBUTES_BEGIN} line followed by "
|
|
1573
|
+
f"one {GIT_ATTRIBUTES_END} line; fix the markers by hand, then rerun"
|
|
1574
|
+
)
|
|
1575
|
+
else:
|
|
1576
|
+
merged = "".join(lines[:begins[0]]) + GIT_ATTRIBUTES_BLOCK + "".join(lines[ends[0] + 1:])
|
|
1577
|
+
return merged.encode("utf-8", "surrogateescape")
|
|
1578
|
+
|
|
1579
|
+
|
|
1580
|
+
def _read_git_attributes(destination: Path) -> Optional[bytes]:
|
|
1581
|
+
if not _lexists(destination):
|
|
1582
|
+
return None
|
|
1583
|
+
if destination.is_symlink() or not destination.is_file():
|
|
1584
|
+
raise InstallerError(f"unsafe git attributes file: {destination}")
|
|
1585
|
+
try:
|
|
1586
|
+
return destination.read_bytes()
|
|
1587
|
+
except OSError as error:
|
|
1588
|
+
raise InstallerError(f"cannot read {destination}: {error}") from error
|
|
1589
|
+
|
|
1590
|
+
|
|
1591
|
+
def _apply_git_attributes(project: Path, transaction: "ProjectTransaction") -> None:
|
|
1592
|
+
destination = _git_attributes_path(project)
|
|
1593
|
+
if destination is None:
|
|
1594
|
+
return
|
|
1595
|
+
original = _read_git_attributes(destination)
|
|
1596
|
+
merged = _merged_git_attributes(original, destination)
|
|
1597
|
+
if original == merged:
|
|
1598
|
+
return
|
|
1599
|
+
_create_directory(destination.parent, transaction.created_directories)
|
|
1600
|
+
if original is None:
|
|
1601
|
+
try:
|
|
1602
|
+
with destination.open("xb") as output:
|
|
1603
|
+
transaction.copied.append(destination)
|
|
1604
|
+
output.write(merged)
|
|
1605
|
+
except FileExistsError as error:
|
|
1606
|
+
raise InstallerError(f"{destination} changed during install; rerun") from error
|
|
1607
|
+
return
|
|
1608
|
+
mode = destination.stat().st_mode & 0o777
|
|
1609
|
+
_atomic_write(destination, merged, mode)
|
|
1610
|
+
transaction.replaced.append((destination, original, mode))
|
|
1611
|
+
|
|
1612
|
+
|
|
1189
1613
|
def _validate_project(
|
|
1190
1614
|
source_root: Path,
|
|
1191
1615
|
project: Path,
|
|
@@ -1232,11 +1656,18 @@ def _validate_project(
|
|
|
1232
1656
|
runtime_store.prepare(source_root, project, dry_run=True)
|
|
1233
1657
|
except (OSError, ValueError) as error:
|
|
1234
1658
|
raise InstallerError(str(error)) from error
|
|
1659
|
+
attributes = _git_attributes_path(project)
|
|
1660
|
+
if attributes is not None:
|
|
1661
|
+
_merged_git_attributes(_read_git_attributes(attributes), attributes)
|
|
1235
1662
|
mergers = _native_settings_mergers(project, selected, hooks)
|
|
1236
1663
|
for destination, _, _, _ in _project_destinations(
|
|
1237
1664
|
project, selected, hooks, interpreter, hooks_dir
|
|
1238
1665
|
):
|
|
1239
|
-
if
|
|
1666
|
+
if destination == project / "AGENTS.md":
|
|
1667
|
+
if destination.is_symlink():
|
|
1668
|
+
raise _existing_contract_error(destination)
|
|
1669
|
+
_merged_agents(source_root, destination)
|
|
1670
|
+
elif _lexists(destination):
|
|
1240
1671
|
merge = mergers.get(destination)
|
|
1241
1672
|
managed = (
|
|
1242
1673
|
_update_replacement(project, destination, hooks_dir) if update else None
|
|
@@ -1265,7 +1696,10 @@ def _apply_project(
|
|
|
1265
1696
|
interpreter: str,
|
|
1266
1697
|
hooks_dir: Optional[Path],
|
|
1267
1698
|
update: bool = False,
|
|
1699
|
+
contracts: bool = True,
|
|
1268
1700
|
) -> None:
|
|
1701
|
+
"""contracts=False (hook refresh) creates a missing AGENTS.md but never
|
|
1702
|
+
rewrites an existing one."""
|
|
1269
1703
|
try:
|
|
1270
1704
|
pin = runtime_store.prepare(source_root, project)
|
|
1271
1705
|
except (OSError, ValueError) as error:
|
|
@@ -1278,6 +1712,11 @@ def _apply_project(
|
|
|
1278
1712
|
if destination.name == "runtime.json" and destination.parent == project / HOOKS_DIRECTORY:
|
|
1279
1713
|
content = runtime_store.pin_text(pin)
|
|
1280
1714
|
_create_directory(destination.parent, transaction.created_directories)
|
|
1715
|
+
if destination == project / "AGENTS.md" and (
|
|
1716
|
+
contracts or not _lexists(destination)
|
|
1717
|
+
):
|
|
1718
|
+
_apply_agents(source_root, destination, transaction)
|
|
1719
|
+
continue
|
|
1281
1720
|
merge = mergers.get(destination)
|
|
1282
1721
|
if _lexists(destination) and not destination.is_symlink():
|
|
1283
1722
|
replaceable = (
|
|
@@ -1320,6 +1759,7 @@ def _apply_project(
|
|
|
1320
1759
|
if created:
|
|
1321
1760
|
_remove_path(destination)
|
|
1322
1761
|
raise
|
|
1762
|
+
_apply_git_attributes(project, transaction)
|
|
1323
1763
|
|
|
1324
1764
|
|
|
1325
1765
|
def _rollback_project(transaction: ProjectTransaction) -> None:
|
|
@@ -1411,7 +1851,7 @@ def _atomic_write(
|
|
|
1411
1851
|
with os.fdopen(descriptor, "wb") as output:
|
|
1412
1852
|
output.write(content)
|
|
1413
1853
|
else:
|
|
1414
|
-
with os.fdopen(descriptor, "w", encoding="utf-8") as output:
|
|
1854
|
+
with os.fdopen(descriptor, "w", encoding="utf-8", newline="\n") as output:
|
|
1415
1855
|
output.write(content)
|
|
1416
1856
|
if mode is not None:
|
|
1417
1857
|
temporary.chmod(mode)
|
|
@@ -1448,7 +1888,7 @@ def _is_managed_hook_entry(entry) -> bool:
|
|
|
1448
1888
|
def _is_guard_command(command: str) -> bool:
|
|
1449
1889
|
normalized = command.replace("\\", "/")
|
|
1450
1890
|
match = re.fullmatch(
|
|
1451
|
-
r"(?:python3|python)\s+(?:\"([^\"\r\n]+)\"|'([^'\r\n]+)'|(\S+))",
|
|
1891
|
+
r"(?:python3|python|py\s+-3)\s+(?:\"([^\"\r\n]+)\"|'([^'\r\n]+)'|(\S+))",
|
|
1452
1892
|
normalized,
|
|
1453
1893
|
)
|
|
1454
1894
|
if match is None:
|
|
@@ -1761,7 +2201,7 @@ def refresh_hooks(
|
|
|
1761
2201
|
_validate_hooks_refresh(source_root, project, full, hooks_dir, selected, True)
|
|
1762
2202
|
transaction = ProjectTransaction()
|
|
1763
2203
|
try:
|
|
1764
|
-
_apply_project(source_root, project, selected, full, transaction, interpreter, hooks_dir, update=True)
|
|
2204
|
+
_apply_project(source_root, project, selected, full, transaction, interpreter, hooks_dir, update=True, contracts=False)
|
|
1765
2205
|
except BaseException:
|
|
1766
2206
|
_rollback_project(transaction)
|
|
1767
2207
|
raise
|
|
@@ -1777,7 +2217,7 @@ def refresh_hooks(
|
|
|
1777
2217
|
refresh_targets.append(target)
|
|
1778
2218
|
transaction = ProjectTransaction()
|
|
1779
2219
|
try:
|
|
1780
|
-
_apply_project(source_root, project, refresh_targets, True, transaction, interpreter, hooks_dir, update=True)
|
|
2220
|
+
_apply_project(source_root, project, refresh_targets, True, transaction, interpreter, hooks_dir, update=True, contracts=False)
|
|
1781
2221
|
except BaseException:
|
|
1782
2222
|
_rollback_project(transaction)
|
|
1783
2223
|
raise
|
|
@@ -1827,7 +2267,7 @@ def _deployment_plans(plans: Sequence[TargetPlan]) -> List[DeploymentPlan]:
|
|
|
1827
2267
|
if not target_set <= SHARED_AGENT_TARGETS:
|
|
1828
2268
|
labels = ", ".join(targets)
|
|
1829
2269
|
raise InstallerError(
|
|
1830
|
-
"only Codex, Antigravity, and
|
|
2270
|
+
"only Codex, Antigravity, Zed, and Muse may share a skills root: "
|
|
1831
2271
|
f"{labels}"
|
|
1832
2272
|
)
|
|
1833
2273
|
profile = SHARED_AGENT_PROFILE
|
|
@@ -1940,10 +2380,10 @@ def _validated_project_state(source_root: Path, project: Path) -> dict:
|
|
|
1940
2380
|
interpreter = _required_python_runtime("--doctor")
|
|
1941
2381
|
try:
|
|
1942
2382
|
result = subprocess.run(
|
|
1943
|
-
[interpreter, "-B", str(validator), "validate", "--repo", str(project)],
|
|
2383
|
+
[*interpreter.split(), "-B", str(validator), "validate", "--repo", str(project)],
|
|
1944
2384
|
cwd=project,
|
|
1945
2385
|
capture_output=True,
|
|
1946
|
-
|
|
2386
|
+
encoding="utf-8", errors="replace",
|
|
1947
2387
|
check=False,
|
|
1948
2388
|
)
|
|
1949
2389
|
except OSError as error:
|
|
@@ -1981,10 +2421,10 @@ def _validate_project_runtime_status(source_root: Path, project: Path) -> None:
|
|
|
1981
2421
|
environment["GIT_OPTIONAL_LOCKS"] = "0"
|
|
1982
2422
|
try:
|
|
1983
2423
|
result = subprocess.run(
|
|
1984
|
-
[interpreter, "-B", str(runtime), "status", "--repo", str(project)],
|
|
2424
|
+
[*interpreter.split(), "-B", str(runtime), "status", "--repo", str(project)],
|
|
1985
2425
|
cwd=project,
|
|
1986
2426
|
capture_output=True,
|
|
1987
|
-
|
|
2427
|
+
encoding="utf-8", errors="replace",
|
|
1988
2428
|
check=False,
|
|
1989
2429
|
env=environment,
|
|
1990
2430
|
)
|
|
@@ -2148,9 +2588,13 @@ def _valid_status_branch(value: object) -> Optional[re.Match[str]]:
|
|
|
2148
2588
|
|
|
2149
2589
|
def _is_executable(path: Path) -> bool:
|
|
2150
2590
|
try:
|
|
2151
|
-
|
|
2591
|
+
status = path.stat()
|
|
2152
2592
|
except OSError:
|
|
2153
2593
|
return False
|
|
2594
|
+
if os.name == "nt":
|
|
2595
|
+
# Windows has no exec bit; Git for Windows runs any regular hook file.
|
|
2596
|
+
return path.is_file()
|
|
2597
|
+
return bool(status.st_mode & 0o111)
|
|
2154
2598
|
|
|
2155
2599
|
|
|
2156
2600
|
def _native_guard_contract(
|
|
@@ -2165,6 +2609,32 @@ def _native_guard_contract(
|
|
|
2165
2609
|
return None
|
|
2166
2610
|
|
|
2167
2611
|
|
|
2612
|
+
def _windows_path_warnings() -> List[str]:
|
|
2613
|
+
"""Settings that deep managed-worktree paths need past 260 characters."""
|
|
2614
|
+
import winreg
|
|
2615
|
+
|
|
2616
|
+
warnings = []
|
|
2617
|
+
try:
|
|
2618
|
+
with winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE,
|
|
2619
|
+
r"SYSTEM\CurrentControlSet\Control\FileSystem") as key:
|
|
2620
|
+
enabled = winreg.QueryValueEx(key, "LongPathsEnabled")[0] == 1
|
|
2621
|
+
except OSError:
|
|
2622
|
+
enabled = False
|
|
2623
|
+
if not enabled:
|
|
2624
|
+
warnings.append("windows: long paths are disabled (LongPathsEnabled=0); files deep in "
|
|
2625
|
+
"managed worktrees may exceed 260 characters")
|
|
2626
|
+
try:
|
|
2627
|
+
result = subprocess.run(["git", "config", "--get", "core.longpaths"], capture_output=True,
|
|
2628
|
+
encoding="utf-8", errors="replace", check=False)
|
|
2629
|
+
except OSError:
|
|
2630
|
+
# Without git on PATH there is no git setting to advise on.
|
|
2631
|
+
return warnings
|
|
2632
|
+
if result.stdout.strip().lower() != "true":
|
|
2633
|
+
warnings.append("windows: git core.longpaths is not true; run "
|
|
2634
|
+
"`git config --global core.longpaths true`")
|
|
2635
|
+
return warnings
|
|
2636
|
+
|
|
2637
|
+
|
|
2168
2638
|
def doctor(
|
|
2169
2639
|
source_root: Path,
|
|
2170
2640
|
targets: Sequence[str],
|
|
@@ -2186,6 +2656,9 @@ def doctor(
|
|
|
2186
2656
|
version = _read_package_version(source_root / "package.json")
|
|
2187
2657
|
if version is None:
|
|
2188
2658
|
push("fail", "package: version cannot be read")
|
|
2659
|
+
if os.name == "nt":
|
|
2660
|
+
for text in _windows_path_warnings():
|
|
2661
|
+
push("warn", text)
|
|
2189
2662
|
seen: List[Tuple[str, Path]] = []
|
|
2190
2663
|
installed_targets = set()
|
|
2191
2664
|
for target in targets:
|
|
@@ -2235,6 +2708,29 @@ def doctor(
|
|
|
2235
2708
|
if project is None:
|
|
2236
2709
|
return findings
|
|
2237
2710
|
|
|
2711
|
+
attributes = _git_attributes_path(project)
|
|
2712
|
+
if attributes is not None:
|
|
2713
|
+
try:
|
|
2714
|
+
current = _read_git_attributes(attributes)
|
|
2715
|
+
if current is None or _merged_git_attributes(current, attributes) != current:
|
|
2716
|
+
push("warn", f"git: {attributes} lacks the GSD Path LF rule for .project — "
|
|
2717
|
+
f'run --update --project "{project}"')
|
|
2718
|
+
else:
|
|
2719
|
+
push("ok", "git: .project is checked out LF in every worktree")
|
|
2720
|
+
except InstallerError as error:
|
|
2721
|
+
push("fail", f"git: {error}")
|
|
2722
|
+
crlf = sorted(
|
|
2723
|
+
path.relative_to(project).as_posix()
|
|
2724
|
+
for path in (project / ".project").rglob("*")
|
|
2725
|
+
if path.is_file() and not path.is_symlink() and b"\r\n" in path.read_bytes()
|
|
2726
|
+
) if (project / ".project").is_dir() else []
|
|
2727
|
+
if crlf:
|
|
2728
|
+
# git checkout skips files it considers clean, so delete them first.
|
|
2729
|
+
push("warn", f"git: {len(crlf)} .project file(s) were checked out as CRLF "
|
|
2730
|
+
f"(first: {crlf[0]}); with the LF rule in place and changes "
|
|
2731
|
+
"committed, run `git ls-files -z .project | xargs -0 rm -f && "
|
|
2732
|
+
"git checkout -- .project` to restore LF")
|
|
2733
|
+
|
|
2238
2734
|
for name, heading in PROJECT_CONTRACTS:
|
|
2239
2735
|
contract = project / name
|
|
2240
2736
|
if contract.is_symlink():
|
|
@@ -2249,6 +2745,26 @@ def doctor(
|
|
|
2249
2745
|
canonical = read_project_file(source_root / name, f"package: {name}")
|
|
2250
2746
|
if canonical is None:
|
|
2251
2747
|
continue
|
|
2748
|
+
if name == "AGENTS.md":
|
|
2749
|
+
text = content.decode("utf-8", errors="replace")
|
|
2750
|
+
try:
|
|
2751
|
+
parts = _split_agents(text, contract)
|
|
2752
|
+
except InstallerError as error:
|
|
2753
|
+
push("fail", f"project: {error}")
|
|
2754
|
+
continue
|
|
2755
|
+
if parts is None:
|
|
2756
|
+
push("fail", f'project: AGENTS.md has no GSD Path block — run --update --project "{project}"')
|
|
2757
|
+
elif _agents_block(canonical.decode("utf-8", errors="replace")) not in text:
|
|
2758
|
+
push("fail", f'project: AGENTS.md block is stale — run --update --project "{project}"')
|
|
2759
|
+
elif len(content) > CODEX_DOC_LIMIT:
|
|
2760
|
+
push(
|
|
2761
|
+
"fail",
|
|
2762
|
+
f"project: AGENTS.md is {len(content)} bytes; Codex reads only "
|
|
2763
|
+
f"the first {CODEX_DOC_LIMIT} — reduce the file to keep all rules visible",
|
|
2764
|
+
)
|
|
2765
|
+
else:
|
|
2766
|
+
push("ok", "project: AGENTS.md block present")
|
|
2767
|
+
continue
|
|
2252
2768
|
installed_section = _contract_section(
|
|
2253
2769
|
content.decode("utf-8", errors="replace"), heading
|
|
2254
2770
|
)
|
|
@@ -2442,7 +2958,7 @@ def doctor(
|
|
|
2442
2958
|
try:
|
|
2443
2959
|
parsed = json.loads(settings.read_text(encoding="utf-8"))
|
|
2444
2960
|
entries = parsed.get("hooks", {}).get(event)
|
|
2445
|
-
variants = [entry(candidate) for candidate in
|
|
2961
|
+
variants = [entry(candidate) for candidate in INTERPRETER_CANDIDATES]
|
|
2446
2962
|
current = isinstance(entries, list) and any(
|
|
2447
2963
|
candidate in variants for candidate in entries
|
|
2448
2964
|
)
|
|
@@ -2488,7 +3004,7 @@ def doctor(
|
|
|
2488
3004
|
push("warn", f"hooks: {label} is not a managed GSD Path git hook")
|
|
2489
3005
|
elif not any(
|
|
2490
3006
|
hook_text == generator(candidate)
|
|
2491
|
-
for candidate in
|
|
3007
|
+
for candidate in INTERPRETER_CANDIDATES
|
|
2492
3008
|
):
|
|
2493
3009
|
push("warn", f"hooks: {label} is stale — run --hooks-refresh-full")
|
|
2494
3010
|
elif not _is_executable(hook_path):
|
|
@@ -2715,7 +3231,7 @@ def install(
|
|
|
2715
3231
|
results.append(
|
|
2716
3232
|
"codex-legacy: backed up "
|
|
2717
3233
|
f"{len(legacy_transaction.moved)} entries to "
|
|
2718
|
-
f"{legacy_transaction.backup}"
|
|
3234
|
+
f"{legacy_transaction.backup}{LOCAL_EDITS_NOTE}"
|
|
2719
3235
|
)
|
|
2720
3236
|
for index, plan in enumerate(deployments):
|
|
2721
3237
|
transaction = TargetTransaction(plan.root)
|
|
@@ -2726,7 +3242,7 @@ def install(
|
|
|
2726
3242
|
results.append(
|
|
2727
3243
|
f"{'+'.join(plan.targets)}: backed up "
|
|
2728
3244
|
f"{len(transaction.moved)} entries "
|
|
2729
|
-
f"to {transaction.backup}"
|
|
3245
|
+
f"to {transaction.backup}{LOCAL_EDITS_NOTE}"
|
|
2730
3246
|
)
|
|
2731
3247
|
if project is not None:
|
|
2732
3248
|
result_line = _project_result(
|
|
@@ -2767,6 +3283,31 @@ def install(
|
|
|
2767
3283
|
return results
|
|
2768
3284
|
|
|
2769
3285
|
|
|
3286
|
+
def _runtime_provenance(arguments, source_root: Path):
|
|
3287
|
+
"""Owner-recorded provenance from --runtime-provenance-* upgrade flags.
|
|
3288
|
+
|
|
3289
|
+
When the supplied source root is a git checkout and no patch reference
|
|
3290
|
+
was given, its HEAD sha is recorded with one best-effort rev-parse.
|
|
3291
|
+
Provenance is display/audit metadata on the runtime declaration only; it
|
|
3292
|
+
never joins the pinned digest or runtime validation.
|
|
3293
|
+
"""
|
|
3294
|
+
supplied = (arguments.runtime_provenance_source, arguments.runtime_provenance_patch,
|
|
3295
|
+
arguments.runtime_provenance_note)
|
|
3296
|
+
recorded = {key: value for key, value in zip(runtime_store.PROVENANCE_KEYS, supplied) if value}
|
|
3297
|
+
if not recorded:
|
|
3298
|
+
return None
|
|
3299
|
+
if "patch_ref" not in recorded:
|
|
3300
|
+
try:
|
|
3301
|
+
head = subprocess.run(["git", "rev-parse", "--show-toplevel", "HEAD"], cwd=source_root,
|
|
3302
|
+
capture_output=True, encoding="utf-8", check=True)
|
|
3303
|
+
toplevel, _, sha = head.stdout.strip().partition("\n")
|
|
3304
|
+
if sha and Path(toplevel).resolve() == Path(source_root).resolve():
|
|
3305
|
+
recorded["patch_ref"] = sha
|
|
3306
|
+
except (OSError, subprocess.CalledProcessError):
|
|
3307
|
+
pass
|
|
3308
|
+
return recorded
|
|
3309
|
+
|
|
3310
|
+
|
|
2770
3311
|
def parser() -> argparse.ArgumentParser:
|
|
2771
3312
|
argument_parser = argparse.ArgumentParser(description=__doc__)
|
|
2772
3313
|
for target in TARGETS:
|
|
@@ -2778,10 +3319,14 @@ def parser() -> argparse.ArgumentParser:
|
|
|
2778
3319
|
argument_parser.add_argument("--local", action="store_true")
|
|
2779
3320
|
argument_parser.add_argument("--dry-run", action="store_true")
|
|
2780
3321
|
argument_parser.add_argument("--project", type=Path)
|
|
3322
|
+
argument_parser.add_argument("--member-of", type=Path)
|
|
2781
3323
|
argument_parser.add_argument("--doctor", action="store_true")
|
|
2782
3324
|
runtime_actions = argument_parser.add_mutually_exclusive_group()
|
|
2783
3325
|
for action in ("restore", "upgrade", "migrate"):
|
|
2784
3326
|
runtime_actions.add_argument(f"--runtime-{action}", action="store_true")
|
|
3327
|
+
argument_parser.add_argument("--runtime-provenance-source")
|
|
3328
|
+
argument_parser.add_argument("--runtime-provenance-patch")
|
|
3329
|
+
argument_parser.add_argument("--runtime-provenance-note")
|
|
2785
3330
|
argument_parser.add_argument("--hooks", action="store_true")
|
|
2786
3331
|
argument_parser.add_argument("--hooks-init", action="store_true")
|
|
2787
3332
|
argument_parser.add_argument("--hooks-refresh", action="store_true")
|
|
@@ -2827,6 +3372,9 @@ def main(argv: Optional[Sequence[str]] = None) -> int:
|
|
|
2827
3372
|
)
|
|
2828
3373
|
runtime_action = next((action for action in ("restore", "upgrade", "migrate")
|
|
2829
3374
|
if getattr(arguments, "runtime_" + action)), None)
|
|
3375
|
+
provenance = _runtime_provenance(arguments, source_root)
|
|
3376
|
+
if provenance is not None and runtime_action != "upgrade":
|
|
3377
|
+
argument_parser.error("--runtime-provenance-* requires --runtime-upgrade")
|
|
2830
3378
|
if runtime_action:
|
|
2831
3379
|
if project is None:
|
|
2832
3380
|
argument_parser.error(f"--runtime-{runtime_action} requires --project")
|
|
@@ -2834,7 +3382,8 @@ def main(argv: Optional[Sequence[str]] = None) -> int:
|
|
|
2834
3382
|
_validate_project_git_root(project)
|
|
2835
3383
|
locks, created = ([], []) if arguments.dry_run else _acquire_install_locks([project / HOOKS_DIRECTORY])
|
|
2836
3384
|
try:
|
|
2837
|
-
pin = runtime_store.operate(source_root, project, runtime_action,
|
|
3385
|
+
pin = runtime_store.operate(source_root, project, runtime_action,
|
|
3386
|
+
dry_run=arguments.dry_run, provenance=provenance)
|
|
2838
3387
|
finally:
|
|
2839
3388
|
_release_install_locks(locks, created)
|
|
2840
3389
|
print(f"runtime: {runtime_action} {pin['version']} ({pin['digest']})"
|
|
@@ -2845,6 +3394,18 @@ def main(argv: Optional[Sequence[str]] = None) -> int:
|
|
|
2845
3394
|
except (InstallerError, OSError, ValueError) as error:
|
|
2846
3395
|
print(f"error: {error}", file=sys.stderr)
|
|
2847
3396
|
return 1
|
|
3397
|
+
if arguments.member_of is not None:
|
|
3398
|
+
if project is None:
|
|
3399
|
+
argument_parser.error("--member-of requires --project")
|
|
3400
|
+
try:
|
|
3401
|
+
for line in install_member_hooks(
|
|
3402
|
+
absolute_path(arguments.member_of), project, arguments.dry_run
|
|
3403
|
+
):
|
|
3404
|
+
print(line)
|
|
3405
|
+
return 0
|
|
3406
|
+
except (InstallerError, OSError, subprocess.CalledProcessError) as error:
|
|
3407
|
+
print(f"error: {error}", file=sys.stderr)
|
|
3408
|
+
return 1
|
|
2848
3409
|
if arguments.doctor:
|
|
2849
3410
|
named = [target for target in TARGETS if getattr(arguments, target)]
|
|
2850
3411
|
selected = named if named and not arguments.all_targets else list(TARGETS)
|