@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
|
@@ -14,12 +14,45 @@ import sys
|
|
|
14
14
|
# Runtime helpers must not modify their immutable installation.
|
|
15
15
|
sys.dont_write_bytecode = True
|
|
16
16
|
|
|
17
|
+
import contextlib
|
|
18
|
+
import errno
|
|
17
19
|
import json
|
|
18
20
|
import os
|
|
19
21
|
import re
|
|
22
|
+
import shlex
|
|
23
|
+
import shutil
|
|
24
|
+
import signal
|
|
20
25
|
import subprocess
|
|
26
|
+
import time
|
|
21
27
|
from pathlib import Path
|
|
22
|
-
from typing import Optional
|
|
28
|
+
from typing import BinaryIO, Iterator, List, Optional, Sequence
|
|
29
|
+
|
|
30
|
+
# Chosen by what imports, not os.name, so tests can emulate Windows on POSIX.
|
|
31
|
+
try:
|
|
32
|
+
import fcntl
|
|
33
|
+
except ImportError:
|
|
34
|
+
fcntl = None # type: ignore[assignment]
|
|
35
|
+
import msvcrt
|
|
36
|
+
|
|
37
|
+
# Windows opens descriptors in text mode unless asked otherwise.
|
|
38
|
+
O_BINARY = getattr(os, "O_BINARY", 0)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def utf8_stdio() -> None:
|
|
42
|
+
"""Write stdout and stderr as UTF-8, as every caller of these CLIs reads them.
|
|
43
|
+
|
|
44
|
+
A Windows pipe defaults to the locale code page (cp1252), so an em dash
|
|
45
|
+
reaches the parent as byte 0x97. Streams a caller substituted are left alone.
|
|
46
|
+
"""
|
|
47
|
+
if os.name != "nt":
|
|
48
|
+
return
|
|
49
|
+
for stream in (sys.stdout, sys.stderr):
|
|
50
|
+
if hasattr(stream, "reconfigure") and (stream.encoding or "").lower() != "utf-8":
|
|
51
|
+
stream.reconfigure(encoding="utf-8", errors="replace")
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
# Every runtime script imports this module before producing output.
|
|
55
|
+
utf8_stdio()
|
|
23
56
|
|
|
24
57
|
PIPELINE_MARKER = "gsd-path/v2"
|
|
25
58
|
BOUND_BRANCH_RE = re.compile(r"^gsd-path/M(\d{3,})$")
|
|
@@ -32,6 +65,101 @@ VERIFY_RESULTS = ("pass", "fail")
|
|
|
32
65
|
VERIFY_LEDGER_SCHEMA = "gsd-path/verify-ledger/v2"
|
|
33
66
|
VERIFY_BLOCK_PATTERN = re.compile(r"```bash[ \t]*\n(?P<block>.*?)```", re.DOTALL)
|
|
34
67
|
|
|
68
|
+
REVIEW_BULLET_HEADING_PATTERN = re.compile(r"^#{2,3} ")
|
|
69
|
+
REVIEW_BULLET_FIELD_LABEL_PATTERN = re.compile(r"^- \*\*[^*]+\*\*:")
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def review_bullet_field_label_pattern(field: str) -> re.Pattern[str]:
|
|
73
|
+
return re.compile(rf"^- \*\*{re.escape(field)}\*\*:[ \t]*(.*)$")
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _review_bullet_field_value(lines: Sequence[str], index: int, inline: str) -> str:
|
|
77
|
+
if inline:
|
|
78
|
+
return inline
|
|
79
|
+
collected: list[str] = []
|
|
80
|
+
for line in lines[index + 1 :]:
|
|
81
|
+
if REVIEW_BULLET_FIELD_LABEL_PATTERN.match(line) or REVIEW_BULLET_HEADING_PATTERN.match(
|
|
82
|
+
line
|
|
83
|
+
):
|
|
84
|
+
break
|
|
85
|
+
if line and not line[0].isspace():
|
|
86
|
+
break
|
|
87
|
+
stripped = line.strip()
|
|
88
|
+
if stripped:
|
|
89
|
+
collected.append(stripped)
|
|
90
|
+
return "\n".join(collected)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def find_review_bullet_field_values(lines: Sequence[str], field: str) -> list[str]:
|
|
94
|
+
pattern = review_bullet_field_label_pattern(field)
|
|
95
|
+
values: list[str] = []
|
|
96
|
+
for index, line in enumerate(lines):
|
|
97
|
+
match = pattern.fullmatch(line)
|
|
98
|
+
if match is None:
|
|
99
|
+
continue
|
|
100
|
+
values.append(_review_bullet_field_value(lines, index, match.group(1)))
|
|
101
|
+
return values
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def parse_review_bullet_field(lines: Sequence[str], field: str) -> str:
|
|
105
|
+
"""Parse one `- **Field**:` review bullet, including indented continuations."""
|
|
106
|
+
values = find_review_bullet_field_values(lines, field)
|
|
107
|
+
if len(values) != 1:
|
|
108
|
+
raise ValueError(field)
|
|
109
|
+
return values[0]
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
# Sample paths the pipeline must commit. A product rule such as an unanchored
|
|
113
|
+
# `build/` would otherwise drop them silently from every commit.
|
|
114
|
+
PROJECT_PROBE_PATHS = (
|
|
115
|
+
".project/STATE.md",
|
|
116
|
+
".project/LESSONS.md",
|
|
117
|
+
".project/REPOSITORY.md",
|
|
118
|
+
".project/MEMBERS.md",
|
|
119
|
+
".project/CHARTER.md",
|
|
120
|
+
".project/ROADMAP.md",
|
|
121
|
+
".project/SYNTHESIS.md",
|
|
122
|
+
VERIFY_LEDGER_PATH,
|
|
123
|
+
".project/build/members.json",
|
|
124
|
+
".project/build/evidence.json",
|
|
125
|
+
".project/archive/001-probe/build/evidence.json",
|
|
126
|
+
".project/archive/001-probe/build/verify-ledger.jsonl",
|
|
127
|
+
".project/intent/probe.md",
|
|
128
|
+
".project/research/probe.md",
|
|
129
|
+
".project/plan/probe.md",
|
|
130
|
+
".project/plan/PLAN.md",
|
|
131
|
+
".project/tasks/probe.md",
|
|
132
|
+
".project/review/probe.md",
|
|
133
|
+
".project/discuss/probe.md",
|
|
134
|
+
".project/next/probe.md",
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def project_ignore_error(repo: Path) -> Optional[str]:
|
|
139
|
+
"""Why ignore rules would drop pipeline state from commits, or None."""
|
|
140
|
+
result = subprocess.run(
|
|
141
|
+
("git", "-C", str(repo), "check-ignore", "-v", "-n", "-z", "--no-index", "--stdin"),
|
|
142
|
+
input="\0".join(PROJECT_PROBE_PATHS) + "\0",
|
|
143
|
+
encoding="utf-8",
|
|
144
|
+
errors="surrogateescape",
|
|
145
|
+
capture_output=True,
|
|
146
|
+
check=False,
|
|
147
|
+
)
|
|
148
|
+
if result.returncode not in (0, 1):
|
|
149
|
+
return f"could not check ignore rules: {result.stderr.strip()}"
|
|
150
|
+
fields = result.stdout.split("\0")
|
|
151
|
+
hits = []
|
|
152
|
+
for index in range(0, len(fields) - 3, 4):
|
|
153
|
+
source, line, pattern, path = fields[index:index + 4]
|
|
154
|
+
# A `!` match re-includes the path; only a plain match excludes it.
|
|
155
|
+
if pattern and not pattern.startswith("!"):
|
|
156
|
+
hits.append(f"{source}:{line}:{pattern} excludes {path}")
|
|
157
|
+
if not hits:
|
|
158
|
+
return None
|
|
159
|
+
return ("ignore rules exclude GSD Path state that must be committed: "
|
|
160
|
+
+ "; ".join(hits)
|
|
161
|
+
+ ". Anchor the product rule (for example `/build/`) in a task that owns it.")
|
|
162
|
+
|
|
35
163
|
|
|
36
164
|
def section_body(text: str, heading: str) -> Optional[str]:
|
|
37
165
|
"""The text under `## <heading>` up to the next `## `, or None when absent."""
|
|
@@ -48,9 +176,11 @@ def task_verify_command(task_text: str) -> str:
|
|
|
48
176
|
return block.group("block").removesuffix("\n") if block else ""
|
|
49
177
|
|
|
50
178
|
|
|
51
|
-
def latest_verify_entry(entries: list, command: str, commit: str
|
|
179
|
+
def latest_verify_entry(entries: list, command: str, commit: str,
|
|
180
|
+
repo: Optional[str] = None) -> Optional[dict]:
|
|
181
|
+
"""Rows are keyed by (command, repo, commit); a row without `repo` is a coordinator row."""
|
|
52
182
|
for entry in reversed(entries):
|
|
53
|
-
if (entry.get("schema") == VERIFY_LEDGER_SCHEMA
|
|
183
|
+
if (entry.get("schema") == VERIFY_LEDGER_SCHEMA and entry.get("repo") == repo
|
|
54
184
|
and entry["command"] == command and entry["commit"] == commit):
|
|
55
185
|
return entry
|
|
56
186
|
return None
|
|
@@ -63,6 +193,11 @@ def verify_ledger_entries(path: Path) -> list:
|
|
|
63
193
|
return parse_verify_ledger(path.read_text(encoding="utf-8"))
|
|
64
194
|
|
|
65
195
|
|
|
196
|
+
def read_user_text(path: Path) -> str:
|
|
197
|
+
"""Read a user-edited text file with CRLF normalized to LF."""
|
|
198
|
+
return path.read_text(encoding="utf-8").replace("\r\n", "\n").replace("\r", "\n")
|
|
199
|
+
|
|
200
|
+
|
|
66
201
|
def parse_verify_ledger(text: str) -> list:
|
|
67
202
|
entries = []
|
|
68
203
|
for number, line in enumerate(text.splitlines(), start=1):
|
|
@@ -78,34 +213,166 @@ def parse_verify_ledger(text: str) -> list:
|
|
|
78
213
|
or not isinstance(entry.get("commit"), str)
|
|
79
214
|
or entry.get("result") not in VERIFY_RESULTS
|
|
80
215
|
or not isinstance(entry.get("recorded_at"), str)
|
|
216
|
+
or ("repo" in entry and not isinstance(entry["repo"], str))
|
|
81
217
|
):
|
|
82
218
|
raise ValueError(f"{VERIFY_LEDGER_PATH} line {number} has invalid fields")
|
|
83
219
|
entries.append(entry)
|
|
84
220
|
return entries
|
|
85
221
|
|
|
86
222
|
|
|
87
|
-
def
|
|
88
|
-
|
|
223
|
+
def _run_exact(
|
|
224
|
+
argv: Sequence[str], *, cwd: Optional[Path] = None, input: Optional[str] = None
|
|
89
225
|
) -> subprocess.CompletedProcess[str]:
|
|
90
|
-
|
|
91
|
-
|
|
226
|
+
"""subprocess.run with UTF-8 text and no newline translation.
|
|
227
|
+
|
|
228
|
+
Text-mode pipes on Windows turn "\\n" into "\\r\\n" on the way in, so text
|
|
229
|
+
handed to git hash-object would no longer match the committed blob.
|
|
230
|
+
"""
|
|
231
|
+
completed = subprocess.run(
|
|
232
|
+
argv,
|
|
92
233
|
cwd=cwd,
|
|
93
|
-
|
|
234
|
+
input=None if input is None else input.encode("utf-8", "surrogateescape"),
|
|
94
235
|
capture_output=True,
|
|
95
236
|
check=False,
|
|
96
237
|
)
|
|
238
|
+
return subprocess.CompletedProcess(
|
|
239
|
+
completed.args,
|
|
240
|
+
completed.returncode,
|
|
241
|
+
completed.stdout.decode("utf-8", "surrogateescape"),
|
|
242
|
+
completed.stderr.decode("utf-8", "surrogateescape"),
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def run_command(
|
|
247
|
+
*arguments: str, cwd: Optional[Path] = None
|
|
248
|
+
) -> subprocess.CompletedProcess[str]:
|
|
249
|
+
return _run_exact(resolve_argv(arguments), cwd=cwd)
|
|
97
250
|
|
|
98
251
|
|
|
99
252
|
def run_git(
|
|
100
253
|
repo: Path, *arguments: str, input: Optional[str] = None
|
|
101
254
|
) -> subprocess.CompletedProcess[str]:
|
|
102
|
-
return
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
255
|
+
return _run_exact(git_command(repo, *arguments), input=input)
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def git_command(repo: Path, *arguments: str) -> tuple[str, ...]:
|
|
259
|
+
"""Git argv for repo, including WSL drvfs workarounds when needed."""
|
|
260
|
+
return ("git", "-C", str(repo), *git_drvfs_config_flags(repo), *arguments)
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
def git_drvfs_config_flags(repo: Path) -> tuple[str, ...]:
|
|
264
|
+
"""Disable index preload on 9p/drvfs mounts where git commit races on index.lock (#207)."""
|
|
265
|
+
if os.name != "posix":
|
|
266
|
+
return ()
|
|
267
|
+
try:
|
|
268
|
+
root = repo.resolve()
|
|
269
|
+
mounts = Path("/proc/mounts").read_text(encoding="utf-8", errors="replace").splitlines()
|
|
270
|
+
except OSError:
|
|
271
|
+
return ()
|
|
272
|
+
best = ""
|
|
273
|
+
fstype = ""
|
|
274
|
+
for line in mounts:
|
|
275
|
+
parts = line.split()
|
|
276
|
+
if len(parts) < 3:
|
|
277
|
+
continue
|
|
278
|
+
mount_point, mount_type = parts[1], parts[2]
|
|
279
|
+
if mount_type not in {"9p", "drvfs"}:
|
|
280
|
+
continue
|
|
281
|
+
if root == Path(mount_point) or root.is_relative_to(mount_point):
|
|
282
|
+
if len(mount_point) >= len(best):
|
|
283
|
+
best, fstype = mount_point, mount_type
|
|
284
|
+
if not fstype:
|
|
285
|
+
return ()
|
|
286
|
+
return ("-c", "core.preloadindex=false", "-c", "index.threads=1")
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
def git_index_lock_retryable(stderr: str) -> bool:
|
|
290
|
+
text = stderr.casefold()
|
|
291
|
+
return "index.lock" in text and "file exists" in text
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
# OS files that are never pipeline artifacts when git ignores them.
|
|
295
|
+
# ponytail: .DS_Store only (#149); add Thumbs.db and the like when reported.
|
|
296
|
+
OS_JUNK_NAMES = frozenset({".DS_Store"})
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
def is_ignored_junk(path: Path) -> bool:
|
|
300
|
+
"""True for an OS junk file that git ignores and does not track."""
|
|
301
|
+
if path.name not in OS_JUNK_NAMES or path.is_symlink() or not path.is_file():
|
|
302
|
+
return False
|
|
303
|
+
return run_git(path.parent, "check-ignore", "-q", "--", path.name).returncode == 0
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
def git_visible_entries(directory: Path) -> set:
|
|
307
|
+
"""Top-level names under directory that git tracks or would add (not ignored)."""
|
|
308
|
+
result = run_git(directory, "ls-files", "-z", "--cached", "--others", "--exclude-standard", "--", ".")
|
|
309
|
+
if result.returncode != 0:
|
|
310
|
+
raise RuntimeError(f"git ls-files failed in {directory}: {result.stderr.strip()}")
|
|
311
|
+
return {path.split("/", 1)[0] for path in result.stdout.split("\0") if path}
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
RUNTIME_PIN_PATH = ".gsd-path/runtime.json"
|
|
315
|
+
RUNTIME_PIN_ALLOWANCE = (
|
|
316
|
+
"only runtime-pin commits (.gsd-path/runtime.json) may follow the reviewed HEAD"
|
|
317
|
+
)
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
def reviewed_head_covers(repo: Path, reviewed: str, head: str) -> bool:
|
|
321
|
+
"""True when a review of `reviewed` still covers `head`.
|
|
322
|
+
|
|
323
|
+
That is the same commit, or `reviewed` is an ancestor of `head` and every
|
|
324
|
+
commit on the first-parent path in ``reviewed..head`` is a non-merge commit
|
|
325
|
+
whose only change is ``.gsd-path/runtime.json``. An empty commit does not
|
|
326
|
+
qualify. Any other path, any merge commit, an unresolvable SHA, or a
|
|
327
|
+
non-ancestor returns False.
|
|
328
|
+
"""
|
|
329
|
+
if not reviewed or not head:
|
|
330
|
+
return False
|
|
331
|
+
resolved_repo = Path(repo)
|
|
332
|
+
resolved: List[str] = []
|
|
333
|
+
for value in (reviewed, head):
|
|
334
|
+
result = run_git(
|
|
335
|
+
resolved_repo, "rev-parse", "--verify", "--quiet", f"{value}^{{commit}}"
|
|
336
|
+
)
|
|
337
|
+
if result.returncode != 0 or not result.stdout.strip():
|
|
338
|
+
return False
|
|
339
|
+
resolved.append(result.stdout.strip())
|
|
340
|
+
reviewed_sha, head_sha = resolved
|
|
341
|
+
if reviewed_sha == head_sha:
|
|
342
|
+
return True
|
|
343
|
+
if (
|
|
344
|
+
run_git(resolved_repo, "merge-base", "--is-ancestor", reviewed_sha, head_sha).returncode
|
|
345
|
+
!= 0
|
|
346
|
+
):
|
|
347
|
+
return False
|
|
348
|
+
listing = run_git(
|
|
349
|
+
resolved_repo,
|
|
350
|
+
"rev-list",
|
|
351
|
+
"--first-parent",
|
|
352
|
+
"--parents",
|
|
353
|
+
f"{reviewed_sha}..{head_sha}",
|
|
108
354
|
)
|
|
355
|
+
if listing.returncode != 0:
|
|
356
|
+
return False
|
|
357
|
+
for line in listing.stdout.splitlines():
|
|
358
|
+
fields = line.split()
|
|
359
|
+
if len(fields) != 2:
|
|
360
|
+
return False
|
|
361
|
+
commit_sha, parent_sha = fields[0], fields[1]
|
|
362
|
+
changed = run_git(
|
|
363
|
+
resolved_repo,
|
|
364
|
+
"diff",
|
|
365
|
+
"--name-only",
|
|
366
|
+
"--no-renames",
|
|
367
|
+
"--no-ext-diff",
|
|
368
|
+
parent_sha,
|
|
369
|
+
commit_sha,
|
|
370
|
+
)
|
|
371
|
+
if changed.returncode != 0:
|
|
372
|
+
return False
|
|
373
|
+
if {path for path in changed.stdout.splitlines() if path} != {RUNTIME_PIN_PATH}:
|
|
374
|
+
return False
|
|
375
|
+
return True
|
|
109
376
|
|
|
110
377
|
|
|
111
378
|
def atomic_replace(path: Path, temporary_path: Path, content: str) -> None:
|
|
@@ -116,14 +383,15 @@ def atomic_replace(path: Path, temporary_path: Path, content: str) -> None:
|
|
|
116
383
|
temporary_path.unlink()
|
|
117
384
|
descriptor = os.open(
|
|
118
385
|
temporary_path,
|
|
119
|
-
os.O_WRONLY | os.O_CREAT | os.O_EXCL,
|
|
386
|
+
os.O_WRONLY | os.O_CREAT | os.O_EXCL | O_BINARY,
|
|
120
387
|
0o666,
|
|
121
388
|
)
|
|
122
|
-
|
|
389
|
+
# Bytes, so Windows text mode never turns "\n" into "\r\n".
|
|
390
|
+
handle = os.fdopen(descriptor, "wb")
|
|
123
391
|
descriptor = None
|
|
124
392
|
with handle:
|
|
125
|
-
handle.write(content)
|
|
126
|
-
|
|
393
|
+
handle.write(content.encode("utf-8"))
|
|
394
|
+
replace(temporary_path, path)
|
|
127
395
|
finally:
|
|
128
396
|
if descriptor is not None:
|
|
129
397
|
os.close(descriptor)
|
|
@@ -183,3 +451,336 @@ def unquote(value: str) -> str:
|
|
|
183
451
|
):
|
|
184
452
|
return cleaned[1:-1]
|
|
185
453
|
return cleaned
|
|
454
|
+
|
|
455
|
+
|
|
456
|
+
# --- Platform helpers --------------------------------------------------------
|
|
457
|
+
# Each helper keeps the POSIX behavior the scripts had before and adds the
|
|
458
|
+
# native Windows equivalent. Windows has no fcntl, cannot open a directory as
|
|
459
|
+
# a descriptor, and treats os.kill(pid, 0) as a Ctrl+C, not a probe.
|
|
460
|
+
|
|
461
|
+
# Sharing violation and access denied: another process briefly holds the file.
|
|
462
|
+
_WINDOWS_BUSY_ERRORS = (5, 32)
|
|
463
|
+
_REPLACE_ATTEMPTS = 20
|
|
464
|
+
|
|
465
|
+
|
|
466
|
+
def replace(source: Path, destination: Path) -> None:
|
|
467
|
+
"""os.replace, retried while a Windows reader briefly holds the destination."""
|
|
468
|
+
if os.name != "nt":
|
|
469
|
+
os.replace(source, destination)
|
|
470
|
+
return
|
|
471
|
+
for attempt in range(_REPLACE_ATTEMPTS):
|
|
472
|
+
try:
|
|
473
|
+
os.replace(source, destination)
|
|
474
|
+
return
|
|
475
|
+
except PermissionError as error:
|
|
476
|
+
if (getattr(error, "winerror", None) not in _WINDOWS_BUSY_ERRORS
|
|
477
|
+
or attempt == _REPLACE_ATTEMPTS - 1):
|
|
478
|
+
raise
|
|
479
|
+
time.sleep(min(0.01 * 2 ** attempt, 0.25))
|
|
480
|
+
|
|
481
|
+
|
|
482
|
+
def fsync_directory(path: Path) -> None:
|
|
483
|
+
"""Persist a directory entry change; Windows cannot open a directory to fsync it."""
|
|
484
|
+
if os.name == "nt":
|
|
485
|
+
return
|
|
486
|
+
descriptor = os.open(path, os.O_RDONLY)
|
|
487
|
+
try:
|
|
488
|
+
os.fsync(descriptor)
|
|
489
|
+
finally:
|
|
490
|
+
os.close(descriptor)
|
|
491
|
+
|
|
492
|
+
|
|
493
|
+
@contextlib.contextmanager
|
|
494
|
+
def exclusive_lock(
|
|
495
|
+
path: Path, *, blocking: bool = True, timeout: Optional[float] = None
|
|
496
|
+
) -> Iterator[BinaryIO]:
|
|
497
|
+
"""Hold an exclusive lock on the file at path, creating it when absent.
|
|
498
|
+
|
|
499
|
+
A non-blocking attempt that loses raises BlockingIOError; a blocking one
|
|
500
|
+
that outlives timeout raises TimeoutError. Windows msvcrt LK_LOCK gives up
|
|
501
|
+
after ten seconds, so both platforms poll a non-blocking lock instead.
|
|
502
|
+
"""
|
|
503
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
504
|
+
deadline = None if timeout is None else time.monotonic() + timeout
|
|
505
|
+
with open(path, "a+b") as handle:
|
|
506
|
+
if fcntl is None:
|
|
507
|
+
# msvcrt locks bytes from the current position; keep one to lock.
|
|
508
|
+
handle.seek(0, os.SEEK_END)
|
|
509
|
+
if handle.tell() == 0:
|
|
510
|
+
handle.write(b"\0")
|
|
511
|
+
handle.flush()
|
|
512
|
+
delay = 0.01
|
|
513
|
+
while True:
|
|
514
|
+
try:
|
|
515
|
+
if fcntl is None:
|
|
516
|
+
handle.seek(0)
|
|
517
|
+
msvcrt.locking(handle.fileno(), msvcrt.LK_NBLCK, 1)
|
|
518
|
+
elif blocking and deadline is None:
|
|
519
|
+
fcntl.flock(handle.fileno(), fcntl.LOCK_EX)
|
|
520
|
+
else:
|
|
521
|
+
fcntl.flock(handle.fileno(), fcntl.LOCK_EX | fcntl.LOCK_NB)
|
|
522
|
+
break
|
|
523
|
+
except OSError as error:
|
|
524
|
+
if error.errno not in (errno.EACCES, errno.EAGAIN, errno.EDEADLK):
|
|
525
|
+
raise
|
|
526
|
+
if not blocking:
|
|
527
|
+
raise BlockingIOError(errno.EAGAIN, f"lock is held: {path}") from error
|
|
528
|
+
if deadline is not None and time.monotonic() >= deadline:
|
|
529
|
+
raise TimeoutError(f"timed out waiting for lock: {path}") from error
|
|
530
|
+
time.sleep(delay)
|
|
531
|
+
delay = min(delay * 2, 0.25)
|
|
532
|
+
try:
|
|
533
|
+
yield handle
|
|
534
|
+
finally:
|
|
535
|
+
if fcntl is None:
|
|
536
|
+
handle.seek(0)
|
|
537
|
+
msvcrt.locking(handle.fileno(), msvcrt.LK_UNLCK, 1)
|
|
538
|
+
else:
|
|
539
|
+
fcntl.flock(handle.fileno(), fcntl.LOCK_UN)
|
|
540
|
+
|
|
541
|
+
|
|
542
|
+
_WAIT_OBJECT_0 = 0x0
|
|
543
|
+
_WAIT_ABANDONED = 0x80
|
|
544
|
+
_WAIT_TIMEOUT = 0x102
|
|
545
|
+
|
|
546
|
+
|
|
547
|
+
@contextlib.contextmanager
|
|
548
|
+
def directory_mutex(
|
|
549
|
+
directory: Path, *, blocking: bool = True, timeout: Optional[float] = None
|
|
550
|
+
) -> Iterator[None]:
|
|
551
|
+
"""Windows-only exclusive lock keyed on a directory's resolved path.
|
|
552
|
+
|
|
553
|
+
POSIX flocks the directory itself. Windows cannot lock a directory, and a
|
|
554
|
+
lock file inside it would be committed, so this holds a named kernel mutex
|
|
555
|
+
in the session namespace. A holder that dies releases it (WAIT_ABANDONED).
|
|
556
|
+
Contention raises BlockingIOError (non-blocking) or TimeoutError.
|
|
557
|
+
"""
|
|
558
|
+
import ctypes
|
|
559
|
+
import hashlib
|
|
560
|
+
from ctypes import wintypes
|
|
561
|
+
|
|
562
|
+
identity = os.path.normcase(str(directory.resolve()))
|
|
563
|
+
name = "Local\\gsd-path-" + hashlib.sha256(identity.encode("utf-8")).hexdigest()
|
|
564
|
+
kernel32 = ctypes.WinDLL("kernel32", use_last_error=True)
|
|
565
|
+
kernel32.CreateMutexW.restype = wintypes.HANDLE
|
|
566
|
+
kernel32.CreateMutexW.argtypes = (ctypes.c_void_p, wintypes.BOOL, wintypes.LPCWSTR)
|
|
567
|
+
kernel32.WaitForSingleObject.restype = wintypes.DWORD
|
|
568
|
+
kernel32.WaitForSingleObject.argtypes = (wintypes.HANDLE, wintypes.DWORD)
|
|
569
|
+
kernel32.ReleaseMutex.argtypes = (wintypes.HANDLE,)
|
|
570
|
+
kernel32.CloseHandle.argtypes = (wintypes.HANDLE,)
|
|
571
|
+
handle = kernel32.CreateMutexW(None, False, name)
|
|
572
|
+
if not handle:
|
|
573
|
+
raise ctypes.WinError(ctypes.get_last_error())
|
|
574
|
+
try:
|
|
575
|
+
deadline = None if timeout is None else time.monotonic() + timeout
|
|
576
|
+
while True:
|
|
577
|
+
# Wait in short slices so Ctrl+C still interrupts a blocked caller.
|
|
578
|
+
result = kernel32.WaitForSingleObject(handle, 0 if not blocking else 200)
|
|
579
|
+
if result in (_WAIT_OBJECT_0, _WAIT_ABANDONED):
|
|
580
|
+
break
|
|
581
|
+
if result != _WAIT_TIMEOUT:
|
|
582
|
+
raise ctypes.WinError(ctypes.get_last_error())
|
|
583
|
+
if not blocking:
|
|
584
|
+
raise BlockingIOError(errno.EAGAIN, f"lock is held: {directory}")
|
|
585
|
+
if deadline is not None and time.monotonic() >= deadline:
|
|
586
|
+
raise TimeoutError(f"timed out waiting for lock: {directory}")
|
|
587
|
+
try:
|
|
588
|
+
yield
|
|
589
|
+
finally:
|
|
590
|
+
kernel32.ReleaseMutex(handle)
|
|
591
|
+
finally:
|
|
592
|
+
kernel32.CloseHandle(handle)
|
|
593
|
+
|
|
594
|
+
|
|
595
|
+
def process_alive(pid: int) -> bool:
|
|
596
|
+
"""Whether pid names a running process, without signalling it.
|
|
597
|
+
|
|
598
|
+
Windows os.kill(pid, 0) sends CTRL_C_EVENT, so Windows asks the kernel.
|
|
599
|
+
status_runtime and install carry verbatim copies (they cannot import this
|
|
600
|
+
module); tests/test_common_platform.py keeps them identical.
|
|
601
|
+
"""
|
|
602
|
+
if pid <= 0:
|
|
603
|
+
return False
|
|
604
|
+
if os.name != "nt":
|
|
605
|
+
try:
|
|
606
|
+
os.kill(pid, 0)
|
|
607
|
+
except OSError as error:
|
|
608
|
+
return error.errno == errno.EPERM
|
|
609
|
+
return True
|
|
610
|
+
import ctypes
|
|
611
|
+
from ctypes import wintypes
|
|
612
|
+
|
|
613
|
+
kernel32 = ctypes.WinDLL("kernel32", use_last_error=True)
|
|
614
|
+
kernel32.OpenProcess.restype = wintypes.HANDLE
|
|
615
|
+
kernel32.OpenProcess.argtypes = (wintypes.DWORD, wintypes.BOOL, wintypes.DWORD)
|
|
616
|
+
kernel32.GetExitCodeProcess.argtypes = (wintypes.HANDLE, ctypes.POINTER(wintypes.DWORD))
|
|
617
|
+
kernel32.CloseHandle.argtypes = (wintypes.HANDLE,)
|
|
618
|
+
handle = kernel32.OpenProcess(0x1000, False, pid) # PROCESS_QUERY_LIMITED_INFORMATION
|
|
619
|
+
if not handle:
|
|
620
|
+
# ERROR_ACCESS_DENIED: a protected process exists but refuses the query.
|
|
621
|
+
return ctypes.get_last_error() == 5
|
|
622
|
+
try:
|
|
623
|
+
code = wintypes.DWORD()
|
|
624
|
+
if not kernel32.GetExitCodeProcess(handle, ctypes.byref(code)):
|
|
625
|
+
return True
|
|
626
|
+
return code.value == 259 # STILL_ACTIVE
|
|
627
|
+
finally:
|
|
628
|
+
kernel32.CloseHandle(handle)
|
|
629
|
+
|
|
630
|
+
|
|
631
|
+
def popen_group(argv: Sequence[str], **kwargs) -> subprocess.Popen:
|
|
632
|
+
"""Start argv as the leader of a new process group that kill_tree can end."""
|
|
633
|
+
if os.name == "nt":
|
|
634
|
+
kwargs["creationflags"] = (kwargs.get("creationflags", 0)
|
|
635
|
+
| subprocess.CREATE_NEW_PROCESS_GROUP)
|
|
636
|
+
else:
|
|
637
|
+
kwargs["start_new_session"] = True
|
|
638
|
+
return subprocess.Popen(list(argv), **kwargs)
|
|
639
|
+
|
|
640
|
+
|
|
641
|
+
# Windows creation flags for a background process that outlives its caller.
|
|
642
|
+
_CREATE_NO_WINDOW = 0x08000000
|
|
643
|
+
_CREATE_BREAKAWAY_FROM_JOB = 0x01000000
|
|
644
|
+
|
|
645
|
+
|
|
646
|
+
def popen_detached(argv: Sequence[str], **kwargs) -> subprocess.Popen:
|
|
647
|
+
"""Start a background process that survives its caller's exit.
|
|
648
|
+
|
|
649
|
+
POSIX starts a new session. Windows hosts may run commands in a job object
|
|
650
|
+
that kills its members when the command returns, so this breaks away when
|
|
651
|
+
the job allows it, and gives the process a hidden console that its console
|
|
652
|
+
children inherit instead of flashing new windows.
|
|
653
|
+
"""
|
|
654
|
+
if os.name != "nt":
|
|
655
|
+
return subprocess.Popen(list(argv), start_new_session=True, **kwargs)
|
|
656
|
+
flags = subprocess.CREATE_NEW_PROCESS_GROUP | _CREATE_NO_WINDOW
|
|
657
|
+
try:
|
|
658
|
+
return subprocess.Popen(list(argv), creationflags=flags | _CREATE_BREAKAWAY_FROM_JOB, **kwargs)
|
|
659
|
+
except PermissionError:
|
|
660
|
+
# The job forbids breakaway; the process still runs, bound to that job.
|
|
661
|
+
return subprocess.Popen(list(argv), creationflags=flags, **kwargs)
|
|
662
|
+
|
|
663
|
+
|
|
664
|
+
def kill_tree(process: subprocess.Popen) -> None:
|
|
665
|
+
"""Forcibly end a popen_group process and every descendant it started."""
|
|
666
|
+
if os.name == "nt":
|
|
667
|
+
subprocess.run(
|
|
668
|
+
("taskkill", "/T", "/F", "/PID", str(process.pid)),
|
|
669
|
+
capture_output=True,
|
|
670
|
+
check=False,
|
|
671
|
+
)
|
|
672
|
+
if process.poll() is None:
|
|
673
|
+
process.kill()
|
|
674
|
+
return
|
|
675
|
+
try:
|
|
676
|
+
os.killpg(process.pid, signal.SIGKILL)
|
|
677
|
+
except ProcessLookupError:
|
|
678
|
+
pass
|
|
679
|
+
|
|
680
|
+
|
|
681
|
+
class ShellNotFound(RuntimeError):
|
|
682
|
+
pass
|
|
683
|
+
|
|
684
|
+
|
|
685
|
+
def _is_wsl_launcher(path: str) -> bool:
|
|
686
|
+
folded = os.path.normcase(os.path.abspath(path))
|
|
687
|
+
system_root = os.path.normcase(os.environ.get("SystemRoot", r"C:\Windows"))
|
|
688
|
+
windows_apps = os.path.normcase(os.path.join(
|
|
689
|
+
os.environ.get("LOCALAPPDATA", ""), "Microsoft", "WindowsApps"))
|
|
690
|
+
return folded.startswith(system_root + os.sep) or (
|
|
691
|
+
bool(os.environ.get("LOCALAPPDATA")) and folded.startswith(windows_apps + os.sep))
|
|
692
|
+
|
|
693
|
+
|
|
694
|
+
def find_bash() -> str:
|
|
695
|
+
"""The bash that runs Verify commands; on Windows, Git for Windows' bash.exe.
|
|
696
|
+
|
|
697
|
+
Windows puts the WSL launcher at System32\\bash.exe, ahead of PATH, so a
|
|
698
|
+
bare "bash" would run the command in another OS. GSD_PATH_BASH overrides.
|
|
699
|
+
"""
|
|
700
|
+
if os.name != "nt":
|
|
701
|
+
return "bash"
|
|
702
|
+
override = os.environ.get("GSD_PATH_BASH")
|
|
703
|
+
if override:
|
|
704
|
+
if not os.path.isfile(override):
|
|
705
|
+
raise ShellNotFound(f"GSD_PATH_BASH is not a file: {override}")
|
|
706
|
+
return override
|
|
707
|
+
candidates = []
|
|
708
|
+
found = shutil.which("bash")
|
|
709
|
+
if found:
|
|
710
|
+
candidates.append(found)
|
|
711
|
+
git = shutil.which("git")
|
|
712
|
+
if git:
|
|
713
|
+
# <Git>\cmd\git.exe or <Git>\bin\git.exe -> <Git>\bin\bash.exe
|
|
714
|
+
candidates.append(os.path.join(os.path.dirname(os.path.dirname(git)), "bin", "bash.exe"))
|
|
715
|
+
for variable in ("ProgramFiles", "ProgramW6432", "LOCALAPPDATA"):
|
|
716
|
+
base = os.environ.get(variable)
|
|
717
|
+
if base:
|
|
718
|
+
parts = ("Programs", "Git") if variable == "LOCALAPPDATA" else ("Git",)
|
|
719
|
+
candidates.append(os.path.join(base, *parts, "bin", "bash.exe"))
|
|
720
|
+
for candidate in candidates:
|
|
721
|
+
if os.path.isfile(candidate) and not _is_wsl_launcher(candidate):
|
|
722
|
+
return candidate
|
|
723
|
+
raise ShellNotFound(
|
|
724
|
+
"Verify commands need bash; install Git for Windows or set GSD_PATH_BASH "
|
|
725
|
+
"to its bin\\bash.exe (the System32 WSL launcher is never used)"
|
|
726
|
+
)
|
|
727
|
+
|
|
728
|
+
|
|
729
|
+
def bash_argv(command: str) -> List[str]:
|
|
730
|
+
return [find_bash(), "-c", command]
|
|
731
|
+
|
|
732
|
+
|
|
733
|
+
# cmd.exe re-parses the arguments of a .cmd or .bat target, so these would
|
|
734
|
+
# let an argument escape its quoting.
|
|
735
|
+
_CMD_METACHARACTERS = frozenset('%^&|<>"!\r\n')
|
|
736
|
+
|
|
737
|
+
|
|
738
|
+
def resolve_argv(argv: Sequence[str]) -> List[str]:
|
|
739
|
+
"""argv with its program resolved the way a shell would find it.
|
|
740
|
+
|
|
741
|
+
Windows CreateProcess only appends .exe, so npm-installed CLIs such as
|
|
742
|
+
claude.cmd or codex.cmd are found through PATHEXT here. POSIX is unchanged.
|
|
743
|
+
"""
|
|
744
|
+
arguments = list(argv)
|
|
745
|
+
if os.name != "nt" or not arguments:
|
|
746
|
+
return arguments
|
|
747
|
+
program = shutil.which(arguments[0])
|
|
748
|
+
if program is None:
|
|
749
|
+
raise FileNotFoundError(f"command not found: {arguments[0]}")
|
|
750
|
+
if program.lower().endswith((".cmd", ".bat")):
|
|
751
|
+
for argument in arguments[1:]:
|
|
752
|
+
if _CMD_METACHARACTERS.intersection(argument):
|
|
753
|
+
raise ValueError(
|
|
754
|
+
f"argument {argument!r} is unsafe to pass to {program}; "
|
|
755
|
+
"cmd.exe would reinterpret it"
|
|
756
|
+
)
|
|
757
|
+
return [program, *arguments[1:]]
|
|
758
|
+
|
|
759
|
+
|
|
760
|
+
def split_command(text: str) -> List[str]:
|
|
761
|
+
"""Split a command string with POSIX quoting.
|
|
762
|
+
|
|
763
|
+
Agents write these in a POSIX shell (Git Bash on Windows), so single and
|
|
764
|
+
double quotes group as in sh. On Windows backslash is not an escape, so a
|
|
765
|
+
path such as C:\\Python312\\python.exe keeps its separators.
|
|
766
|
+
"""
|
|
767
|
+
if os.name != "nt":
|
|
768
|
+
return shlex.split(text)
|
|
769
|
+
lexer = shlex.shlex(text, posix=True)
|
|
770
|
+
lexer.whitespace_split = True
|
|
771
|
+
lexer.escape = ""
|
|
772
|
+
lexer.commenters = ""
|
|
773
|
+
return list(lexer)
|
|
774
|
+
|
|
775
|
+
|
|
776
|
+
def rmtree_force(path: Path) -> None:
|
|
777
|
+
"""shutil.rmtree that also removes read-only files, such as git objects on Windows."""
|
|
778
|
+
|
|
779
|
+
def clear_readonly(function, target, _error) -> None:
|
|
780
|
+
os.chmod(target, 0o700)
|
|
781
|
+
function(target)
|
|
782
|
+
|
|
783
|
+
if sys.version_info >= (3, 12):
|
|
784
|
+
shutil.rmtree(path, onexc=clear_readonly)
|
|
785
|
+
else:
|
|
786
|
+
shutil.rmtree(path, onerror=clear_readonly)
|