@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
|
@@ -12,12 +12,14 @@ Verdicts:
|
|
|
12
12
|
- owned — `.project/` is a real directory with a regular `STATE.md` and no
|
|
13
13
|
unsafe project paths; route by that file, not detection
|
|
14
14
|
- orphan — `.project/` is unsafe, or it contains an entry without a safe
|
|
15
|
-
regular `STATE.md`; the initializer's regular temporary state file
|
|
16
|
-
|
|
17
|
-
- brownfield — no owned state, `.project/` empty
|
|
18
|
-
in-scope signal (package/build
|
|
19
|
-
git, or substantive system
|
|
20
|
-
|
|
15
|
+
regular `STATE.md`; the initializer's regular temporary state file and an
|
|
16
|
+
ignored, untracked regular `.DS_Store` are exceptions
|
|
17
|
+
- brownfield — no owned state, `.project/` empty, absent, or containing only
|
|
18
|
+
an ignored `.DS_Store`, and at least one in-scope signal (package/build
|
|
19
|
+
manifest, source file, tracked signal in git, or substantive system
|
|
20
|
+
documentation)
|
|
21
|
+
- greenfield — no owned state, `.project/` empty, absent, or containing only
|
|
22
|
+
an ignored `.DS_Store`, and no signal
|
|
21
23
|
|
|
22
24
|
Ignored path components: `.git`, `node_modules`, vendored/generated trees,
|
|
23
25
|
build output, and managed GSD Path installation artifacts. `.project/` is
|
|
@@ -41,6 +43,11 @@ from datetime import date
|
|
|
41
43
|
from pathlib import Path, PurePosixPath, PureWindowsPath
|
|
42
44
|
from typing import Iterable, NamedTuple, Optional, Sequence
|
|
43
45
|
|
|
46
|
+
try:
|
|
47
|
+
from . import _common
|
|
48
|
+
except ImportError:
|
|
49
|
+
import _common
|
|
50
|
+
|
|
44
51
|
try:
|
|
45
52
|
import fcntl
|
|
46
53
|
|
|
@@ -48,10 +55,22 @@ try:
|
|
|
48
55
|
fcntl.flock(fd, fcntl.LOCK_EX)
|
|
49
56
|
|
|
50
57
|
except ImportError: # Windows does not provide POSIX file locks.
|
|
58
|
+
import errno
|
|
51
59
|
import msvcrt
|
|
60
|
+
import time
|
|
52
61
|
|
|
53
62
|
def lock_exclusive(fd: int) -> None:
|
|
54
|
-
|
|
63
|
+
delay = 0.01
|
|
64
|
+
while True:
|
|
65
|
+
try:
|
|
66
|
+
os.lseek(fd, 0, os.SEEK_SET)
|
|
67
|
+
msvcrt.locking(fd, msvcrt.LK_NBLCK, 1)
|
|
68
|
+
return
|
|
69
|
+
except OSError as error:
|
|
70
|
+
if error.errno not in (errno.EACCES, errno.EAGAIN, errno.EDEADLK):
|
|
71
|
+
raise
|
|
72
|
+
time.sleep(delay)
|
|
73
|
+
delay = min(delay * 2, 0.25)
|
|
55
74
|
|
|
56
75
|
|
|
57
76
|
IGNORE_DIRS = {
|
|
@@ -210,16 +229,20 @@ HTML_BLOCK_START = re.compile(
|
|
|
210
229
|
re.IGNORECASE,
|
|
211
230
|
)
|
|
212
231
|
REPARSE_NAME_SURROGATE = 0x20000000
|
|
213
|
-
|
|
232
|
+
_POSIX_ANCHORED_EVIDENCE_SUPPORTED = (
|
|
214
233
|
hasattr(os, "O_DIRECTORY")
|
|
215
234
|
and hasattr(os, "O_NOFOLLOW")
|
|
216
235
|
and os.open in os.supports_dir_fd
|
|
217
236
|
and os.stat in os.supports_dir_fd
|
|
218
237
|
and os.stat in os.supports_follow_symlinks
|
|
219
238
|
)
|
|
239
|
+
WINDOWS_ANCHORED_EVIDENCE_SUPPORTED = os.name == "nt"
|
|
240
|
+
ANCHORED_EVIDENCE_SUPPORTED = (
|
|
241
|
+
_POSIX_ANCHORED_EVIDENCE_SUPPORTED or WINDOWS_ANCHORED_EVIDENCE_SUPPORTED
|
|
242
|
+
)
|
|
220
243
|
LISTDIR_DIR_FD_SUPPORTED = os.listdir in getattr(os, "supports_fd", ())
|
|
221
244
|
ANCHORED_STATE_CREATE_SUPPORTED = (
|
|
222
|
-
|
|
245
|
+
_POSIX_ANCHORED_EVIDENCE_SUPPORTED
|
|
223
246
|
and LISTDIR_DIR_FD_SUPPORTED
|
|
224
247
|
and os.mkdir in getattr(os, "supports_dir_fd", ())
|
|
225
248
|
and os.unlink in getattr(os, "supports_dir_fd", ())
|
|
@@ -262,11 +285,12 @@ def is_link_like_status(status: os.stat_result) -> bool:
|
|
|
262
285
|
def is_link_like(path: Path, status: os.stat_result) -> bool:
|
|
263
286
|
if is_link_like_status(status):
|
|
264
287
|
return True
|
|
265
|
-
|
|
266
|
-
if os.name != "nt" or isjunction is None:
|
|
288
|
+
if os.name != "nt":
|
|
267
289
|
return False
|
|
268
290
|
try:
|
|
269
|
-
return
|
|
291
|
+
return is_link_like_status(os.lstat(path))
|
|
292
|
+
except FileNotFoundError:
|
|
293
|
+
return False
|
|
270
294
|
except OSError as error:
|
|
271
295
|
raise DetectError(f"cannot inspect link-like evidence: {path}: {error}") from error
|
|
272
296
|
|
|
@@ -325,7 +349,7 @@ def is_managed_pipeline_directory(relative: str, root: Path) -> bool:
|
|
|
325
349
|
return is_verified_skill_bundle(relative, root)
|
|
326
350
|
|
|
327
351
|
|
|
328
|
-
def is_verified_skill_bundle_at(relative: str, directory_fd: int) -> bool:
|
|
352
|
+
def is_verified_skill_bundle_at(relative: str, directory_fd: int, root: Path) -> bool:
|
|
329
353
|
parts = PurePosixPath(relative).parts
|
|
330
354
|
if not parts:
|
|
331
355
|
return False
|
|
@@ -335,19 +359,45 @@ def is_verified_skill_bundle_at(relative: str, directory_fd: int) -> bool:
|
|
|
335
359
|
return False
|
|
336
360
|
if is_verified_installer_bundle(parts):
|
|
337
361
|
return True
|
|
362
|
+
if _POSIX_ANCHORED_EVIDENCE_SUPPORTED:
|
|
363
|
+
try:
|
|
364
|
+
status = os.stat(
|
|
365
|
+
"SKILL.md",
|
|
366
|
+
dir_fd=directory_fd,
|
|
367
|
+
follow_symlinks=False,
|
|
368
|
+
)
|
|
369
|
+
except FileNotFoundError:
|
|
370
|
+
return False
|
|
371
|
+
except OSError as error:
|
|
372
|
+
raise DetectError(
|
|
373
|
+
f"cannot inspect managed skill bundle: {relative}: {error}"
|
|
374
|
+
) from error
|
|
375
|
+
return not is_link_like_status(status) and stat.S_ISREG(status.st_mode)
|
|
376
|
+
skill_fd = None
|
|
338
377
|
try:
|
|
339
|
-
|
|
378
|
+
win = get_windows_handles()
|
|
379
|
+
skill_fd = win.open_relative(
|
|
380
|
+
directory_fd,
|
|
340
381
|
"SKILL.md",
|
|
341
|
-
|
|
342
|
-
|
|
382
|
+
_FILE_READ_ATTRIBUTES,
|
|
383
|
+
_PIN_SHARE,
|
|
384
|
+
_FILE_OPEN,
|
|
385
|
+
0,
|
|
343
386
|
)
|
|
387
|
+
status = os.fstat(skill_fd)
|
|
344
388
|
except FileNotFoundError:
|
|
345
389
|
return False
|
|
346
390
|
except OSError as error:
|
|
391
|
+
if getattr(error, "winerror", None) == 2:
|
|
392
|
+
return False
|
|
347
393
|
raise DetectError(
|
|
348
394
|
f"cannot inspect managed skill bundle: {relative}: {error}"
|
|
349
395
|
) from error
|
|
350
|
-
|
|
396
|
+
finally:
|
|
397
|
+
if skill_fd is not None:
|
|
398
|
+
os.close(skill_fd)
|
|
399
|
+
skill = root.joinpath(*parts) / "SKILL.md"
|
|
400
|
+
return not is_link_like(skill, status) and stat.S_ISREG(status.st_mode)
|
|
351
401
|
|
|
352
402
|
|
|
353
403
|
def is_fixed_managed_pipeline_artifact(relative: str) -> bool:
|
|
@@ -416,13 +466,20 @@ def read_regular_evidence(
|
|
|
416
466
|
) from error
|
|
417
467
|
if not relative.parts:
|
|
418
468
|
raise DetectError(f"{evidence_name} evidence path is empty: {path}")
|
|
419
|
-
if
|
|
469
|
+
if _POSIX_ANCHORED_EVIDENCE_SUPPORTED:
|
|
420
470
|
return read_anchored_evidence(
|
|
421
471
|
relative,
|
|
422
472
|
root,
|
|
423
473
|
missing_ok=missing_ok,
|
|
424
474
|
evidence_name=evidence_name,
|
|
425
475
|
)
|
|
476
|
+
if WINDOWS_ANCHORED_EVIDENCE_SUPPORTED:
|
|
477
|
+
return read_windows_anchored_evidence(
|
|
478
|
+
relative,
|
|
479
|
+
root,
|
|
480
|
+
missing_ok=missing_ok,
|
|
481
|
+
evidence_name=evidence_name,
|
|
482
|
+
)
|
|
426
483
|
return read_lstat_open_evidence(
|
|
427
484
|
relative,
|
|
428
485
|
root,
|
|
@@ -440,6 +497,17 @@ def directory_flags() -> int:
|
|
|
440
497
|
)
|
|
441
498
|
|
|
442
499
|
|
|
500
|
+
def listdir_anchored(dir_fd: int) -> list[str]:
|
|
501
|
+
"""List names through a fresh directory descriptor under dir_fd."""
|
|
502
|
+
fresh = os.open(".", directory_flags(), dir_fd=dir_fd)
|
|
503
|
+
try:
|
|
504
|
+
if not os.path.samestat(os.fstat(fresh), os.fstat(dir_fd)):
|
|
505
|
+
raise DetectError("anchored directory listing lost its identity")
|
|
506
|
+
return os.listdir(fresh)
|
|
507
|
+
finally:
|
|
508
|
+
os.close(fresh)
|
|
509
|
+
|
|
510
|
+
|
|
443
511
|
def file_flags() -> int:
|
|
444
512
|
return (
|
|
445
513
|
os.O_RDONLY
|
|
@@ -759,9 +827,12 @@ def raise_walk_error(error: OSError) -> None:
|
|
|
759
827
|
|
|
760
828
|
|
|
761
829
|
def iter_worktree_files(root: Path) -> Iterable[str]:
|
|
762
|
-
if
|
|
830
|
+
if _POSIX_ANCHORED_EVIDENCE_SUPPORTED and LISTDIR_DIR_FD_SUPPORTED:
|
|
763
831
|
yield from iter_worktree_files_anchored(root)
|
|
764
832
|
return
|
|
833
|
+
if WINDOWS_ANCHORED_EVIDENCE_SUPPORTED:
|
|
834
|
+
yield from iter_worktree_files_windows_anchored(root)
|
|
835
|
+
return
|
|
765
836
|
for dirpath, dirnames, filenames in os.walk(
|
|
766
837
|
root,
|
|
767
838
|
followlinks=False,
|
|
@@ -819,7 +890,7 @@ def iter_from_dir_fd(
|
|
|
819
890
|
dir_fd: int, relative_dir: str, root: Path
|
|
820
891
|
) -> Iterable[str]:
|
|
821
892
|
try:
|
|
822
|
-
names =
|
|
893
|
+
names = listdir_anchored(dir_fd)
|
|
823
894
|
except OSError as error:
|
|
824
895
|
raise DetectError(f"cannot traverse filesystem evidence: {error}") from error
|
|
825
896
|
for name in names:
|
|
@@ -855,7 +926,7 @@ def iter_from_dir_fd(
|
|
|
855
926
|
raise DetectError(
|
|
856
927
|
f"filesystem evidence changed while traversing: {relative}"
|
|
857
928
|
)
|
|
858
|
-
if is_verified_skill_bundle_at(relative, child):
|
|
929
|
+
if is_verified_skill_bundle_at(relative, child, root):
|
|
859
930
|
continue
|
|
860
931
|
yield from iter_from_dir_fd(child, relative, root)
|
|
861
932
|
finally:
|
|
@@ -961,6 +1032,26 @@ def is_staged_skill_bundle_artifact(
|
|
|
961
1032
|
)
|
|
962
1033
|
|
|
963
1034
|
|
|
1035
|
+
def ignored_junk(directory: Path, name: str, status: os.stat_result) -> bool:
|
|
1036
|
+
"""Ignored, untracked OS junk. The caller's no-follow status decides the file type."""
|
|
1037
|
+
if (
|
|
1038
|
+
name not in _common.OS_JUNK_NAMES
|
|
1039
|
+
or is_link_like_status(status)
|
|
1040
|
+
or not stat.S_ISREG(status.st_mode)
|
|
1041
|
+
):
|
|
1042
|
+
return False
|
|
1043
|
+
try:
|
|
1044
|
+
result = subprocess.run(
|
|
1045
|
+
["git", "-C", str(directory), "check-ignore", "-q", "--", name],
|
|
1046
|
+
capture_output=True,
|
|
1047
|
+
check=False,
|
|
1048
|
+
env=git_environment(),
|
|
1049
|
+
)
|
|
1050
|
+
except OSError:
|
|
1051
|
+
return False
|
|
1052
|
+
return result.returncode == 0
|
|
1053
|
+
|
|
1054
|
+
|
|
964
1055
|
def project_path_inventory(
|
|
965
1056
|
project: Path, root: Path
|
|
966
1057
|
) -> tuple[tuple[str, ...], tuple[str, ...]]:
|
|
@@ -1001,14 +1092,18 @@ def project_path_inventory(
|
|
|
1001
1092
|
posix_relative(current / name, root) for name in linked_directories
|
|
1002
1093
|
)
|
|
1003
1094
|
dirnames[:] = [name for name in dirnames if name not in linked_directories]
|
|
1095
|
+
kept = []
|
|
1004
1096
|
for name in filenames:
|
|
1005
1097
|
path = current / name
|
|
1006
|
-
relative = posix_relative(path, root)
|
|
1007
1098
|
status = lstat_evidence(path, missing_ok=False)
|
|
1099
|
+
if ignored_junk(current, name, status):
|
|
1100
|
+
continue
|
|
1101
|
+
kept.append(name)
|
|
1102
|
+
relative = posix_relative(path, root)
|
|
1008
1103
|
paths.append(relative)
|
|
1009
1104
|
if is_link_like(path, status) or not stat.S_ISREG(status.st_mode):
|
|
1010
1105
|
unsafe_paths.append(relative)
|
|
1011
|
-
if not
|
|
1106
|
+
if not kept and not dirnames and current != project:
|
|
1012
1107
|
paths.append(posix_relative(current, root))
|
|
1013
1108
|
if not paths:
|
|
1014
1109
|
try:
|
|
@@ -1017,7 +1112,11 @@ def project_path_inventory(
|
|
|
1017
1112
|
raise DetectError(
|
|
1018
1113
|
f"cannot list project evidence: {project}: {error}"
|
|
1019
1114
|
) from error
|
|
1020
|
-
paths = [
|
|
1115
|
+
paths = [
|
|
1116
|
+
posix_relative(project / name, root)
|
|
1117
|
+
for name in names
|
|
1118
|
+
if not ignored_junk(project, name, lstat_evidence(project / name, missing_ok=False))
|
|
1119
|
+
]
|
|
1021
1120
|
return tuple(sorted(paths)), tuple(sorted(unsafe_paths))
|
|
1022
1121
|
|
|
1023
1122
|
|
|
@@ -1093,7 +1192,7 @@ def classify(repo: Path) -> dict:
|
|
|
1093
1192
|
missing_ok=True,
|
|
1094
1193
|
)
|
|
1095
1194
|
if (
|
|
1096
|
-
|
|
1195
|
+
STATE_CREATE_SUPPORTED
|
|
1097
1196
|
and orphan_paths == (f".project/{STATE_TEMP_NAME}",)
|
|
1098
1197
|
and not unsafe_paths
|
|
1099
1198
|
and temporary_state_status is not None
|
|
@@ -1265,12 +1364,28 @@ def close_file_descriptors(
|
|
|
1265
1364
|
pass
|
|
1266
1365
|
|
|
1267
1366
|
|
|
1367
|
+
def project_entries(project_fd: int, project: Path) -> set:
|
|
1368
|
+
"""Names in .project without ignored OS junk (Finder may add .DS_Store at any time)."""
|
|
1369
|
+
entries = set(listdir_anchored(project_fd))
|
|
1370
|
+
for name in entries & _common.OS_JUNK_NAMES:
|
|
1371
|
+
try:
|
|
1372
|
+
status = os.stat(name, dir_fd=project_fd, follow_symlinks=False)
|
|
1373
|
+
except FileNotFoundError:
|
|
1374
|
+
continue # Keep the name: the caller's comparison then fails closed.
|
|
1375
|
+
if ignored_junk(project, name, status):
|
|
1376
|
+
entries.discard(name)
|
|
1377
|
+
return entries
|
|
1378
|
+
|
|
1379
|
+
|
|
1268
1380
|
def write_state_anchored(
|
|
1269
1381
|
root: Path,
|
|
1270
1382
|
content: str,
|
|
1271
1383
|
expected_root: os.stat_result,
|
|
1272
1384
|
expected_project: Optional[os.stat_result],
|
|
1273
1385
|
) -> None:
|
|
1386
|
+
if WINDOWS_STATE_CREATE_SUPPORTED:
|
|
1387
|
+
write_state_windows(root, content, expected_root, expected_project)
|
|
1388
|
+
return
|
|
1274
1389
|
if not ANCHORED_STATE_CREATE_SUPPORTED:
|
|
1275
1390
|
raise DetectError("anchored no-follow STATE.md creation is unavailable")
|
|
1276
1391
|
payload = content.encode("utf-8")
|
|
@@ -1333,7 +1448,7 @@ def write_state_anchored(
|
|
|
1333
1448
|
):
|
|
1334
1449
|
raise DetectError(".project changed after classification")
|
|
1335
1450
|
lock_exclusive(project_fd)
|
|
1336
|
-
entries =
|
|
1451
|
+
entries = project_entries(project_fd, root / ".project")
|
|
1337
1452
|
if entries not in (set(), {STATE_TEMP_NAME}):
|
|
1338
1453
|
raise DetectError(".project changed after classification")
|
|
1339
1454
|
create = (
|
|
@@ -1362,12 +1477,12 @@ def write_state_anchored(
|
|
|
1362
1477
|
follow_symlinks=False,
|
|
1363
1478
|
)
|
|
1364
1479
|
except FileNotFoundError as error:
|
|
1365
|
-
if "STATE.md" in
|
|
1480
|
+
if "STATE.md" in listdir_anchored(project_fd):
|
|
1366
1481
|
raise DetectError("STATE.md already exists") from error
|
|
1367
1482
|
raise DetectError("temporary STATE.md disappeared") from error
|
|
1368
1483
|
if not os.path.samestat(created_status, named_temporary):
|
|
1369
1484
|
raise DetectError("temporary STATE.md changed before writing")
|
|
1370
|
-
if "STATE.md" in
|
|
1485
|
+
if "STATE.md" in listdir_anchored(project_fd):
|
|
1371
1486
|
raise DetectError("STATE.md already exists")
|
|
1372
1487
|
os.ftruncate(state_fd, 0)
|
|
1373
1488
|
write_all(state_fd, payload)
|
|
@@ -1400,7 +1515,7 @@ def write_state_anchored(
|
|
|
1400
1515
|
or not os.path.samestat(created_status, current_state)
|
|
1401
1516
|
):
|
|
1402
1517
|
raise DetectError("temporary STATE.md changed while writing")
|
|
1403
|
-
if
|
|
1518
|
+
if project_entries(project_fd, root / ".project") != {STATE_TEMP_NAME}:
|
|
1404
1519
|
raise DetectError(".project contents changed while creating STATE.md")
|
|
1405
1520
|
try:
|
|
1406
1521
|
os.link(
|
|
@@ -1420,7 +1535,7 @@ def write_state_anchored(
|
|
|
1420
1535
|
published = os.stat("STATE.md", dir_fd=project_fd, follow_symlinks=False)
|
|
1421
1536
|
if published.st_nlink != 1 or not os.path.samestat(created_status, published):
|
|
1422
1537
|
raise DetectError("STATE.md changed while publishing")
|
|
1423
|
-
if
|
|
1538
|
+
if project_entries(project_fd, root / ".project") != {"STATE.md"}:
|
|
1424
1539
|
raise DetectError(".project contents changed while publishing STATE.md")
|
|
1425
1540
|
os.fsync(project_fd)
|
|
1426
1541
|
closing_state_fd = state_fd
|
|
@@ -1448,6 +1563,446 @@ def write_state_anchored(
|
|
|
1448
1563
|
close_file_descriptors(state_fd, project_fd, root_fd)
|
|
1449
1564
|
|
|
1450
1565
|
|
|
1566
|
+
# --- Windows anchored creation ------------------------------------------------
|
|
1567
|
+
# Windows has no dir_fd, O_NOFOLLOW, or O_DIRECTORY. Handles give the same
|
|
1568
|
+
# guarantee (docs/adr/0004-windows-anchored-state.md):
|
|
1569
|
+
# - each directory is opened without following a reparse point and held
|
|
1570
|
+
# without FILE_SHARE_DELETE, so it cannot be renamed or replaced while held;
|
|
1571
|
+
# - children are opened relative to the held parent handle (NtCreateFile with
|
|
1572
|
+
# a RootDirectory), so no path is resolved again after it was checked;
|
|
1573
|
+
# - STATE.md is published by renaming the temporary file's own handle without
|
|
1574
|
+
# replacing an existing name, and rollback deletes through that handle.
|
|
1575
|
+
|
|
1576
|
+
_FILE_LIST_DIRECTORY = 0x0001
|
|
1577
|
+
_FILE_TRAVERSE = 0x0020
|
|
1578
|
+
_FILE_READ_ATTRIBUTES = 0x0080
|
|
1579
|
+
_DELETE = 0x00010000
|
|
1580
|
+
_SYNCHRONIZE = 0x00100000
|
|
1581
|
+
_GENERIC_WRITE = 0x40000000
|
|
1582
|
+
_GENERIC_READ = 0x80000000
|
|
1583
|
+
_FILE_SHARE_READ = 0x1
|
|
1584
|
+
_FILE_SHARE_WRITE = 0x2
|
|
1585
|
+
_OPEN_EXISTING = 3
|
|
1586
|
+
_FILE_FLAG_OPEN_REPARSE_POINT = 0x00200000
|
|
1587
|
+
_FILE_FLAG_BACKUP_SEMANTICS = 0x02000000
|
|
1588
|
+
_FILE_OPEN = 1
|
|
1589
|
+
_FILE_CREATE = 2
|
|
1590
|
+
_FILE_OPEN_IF = 3
|
|
1591
|
+
_FILE_DIRECTORY_FILE = 0x00000001
|
|
1592
|
+
_FILE_SYNCHRONOUS_IO_NONALERT = 0x00000020
|
|
1593
|
+
_FILE_NON_DIRECTORY_FILE = 0x00000040
|
|
1594
|
+
_FILE_OPEN_REPARSE_POINT = 0x00200000
|
|
1595
|
+
_OBJ_CASE_INSENSITIVE = 0x00000040
|
|
1596
|
+
_FILE_RENAME_INFORMATION = 10
|
|
1597
|
+
_FILE_DISPOSITION_INFO = 4
|
|
1598
|
+
_DIRECTORY_ACCESS = _FILE_LIST_DIRECTORY | _FILE_TRAVERSE | _FILE_READ_ATTRIBUTES | _SYNCHRONIZE
|
|
1599
|
+
# Readers and writers may share a held directory; nobody may delete or rename it.
|
|
1600
|
+
_PIN_SHARE = _FILE_SHARE_READ | _FILE_SHARE_WRITE
|
|
1601
|
+
|
|
1602
|
+
WINDOWS_STATE_CREATE_SUPPORTED = os.name == "nt"
|
|
1603
|
+
STATE_CREATE_SUPPORTED = ANCHORED_STATE_CREATE_SUPPORTED or WINDOWS_STATE_CREATE_SUPPORTED
|
|
1604
|
+
|
|
1605
|
+
|
|
1606
|
+
class _WindowsHandles:
|
|
1607
|
+
"""kernel32/ntdll calls for the Windows anchored create, bound on first use."""
|
|
1608
|
+
|
|
1609
|
+
def __init__(self) -> None:
|
|
1610
|
+
import ctypes
|
|
1611
|
+
import msvcrt
|
|
1612
|
+
from ctypes import wintypes
|
|
1613
|
+
|
|
1614
|
+
class UnicodeString(ctypes.Structure):
|
|
1615
|
+
_fields_ = [("Length", wintypes.USHORT), ("MaximumLength", wintypes.USHORT),
|
|
1616
|
+
("Buffer", wintypes.LPWSTR)]
|
|
1617
|
+
|
|
1618
|
+
class ObjectAttributes(ctypes.Structure):
|
|
1619
|
+
_fields_ = [("Length", wintypes.ULONG), ("RootDirectory", wintypes.HANDLE),
|
|
1620
|
+
("ObjectName", ctypes.POINTER(UnicodeString)),
|
|
1621
|
+
("Attributes", wintypes.ULONG), ("SecurityDescriptor", ctypes.c_void_p),
|
|
1622
|
+
("SecurityQualityOfService", ctypes.c_void_p)]
|
|
1623
|
+
|
|
1624
|
+
class IoStatusBlock(ctypes.Structure):
|
|
1625
|
+
_fields_ = [("Status", ctypes.c_void_p), ("Information", ctypes.c_size_t)]
|
|
1626
|
+
|
|
1627
|
+
self.ctypes, self.wintypes, self.msvcrt = ctypes, wintypes, msvcrt
|
|
1628
|
+
self.UnicodeString, self.ObjectAttributes, self.IoStatusBlock = (
|
|
1629
|
+
UnicodeString, ObjectAttributes, IoStatusBlock)
|
|
1630
|
+
self.kernel32 = ctypes.WinDLL("kernel32", use_last_error=True)
|
|
1631
|
+
self.ntdll = ctypes.WinDLL("ntdll")
|
|
1632
|
+
self.kernel32.CreateFileW.restype = wintypes.HANDLE
|
|
1633
|
+
self.kernel32.CreateFileW.argtypes = (
|
|
1634
|
+
wintypes.LPCWSTR, wintypes.DWORD, wintypes.DWORD, ctypes.c_void_p,
|
|
1635
|
+
wintypes.DWORD, wintypes.DWORD, wintypes.HANDLE)
|
|
1636
|
+
self.kernel32.SetFileInformationByHandle.argtypes = (
|
|
1637
|
+
wintypes.HANDLE, ctypes.c_int, ctypes.c_void_p, wintypes.DWORD)
|
|
1638
|
+
self.ntdll.NtCreateFile.restype = ctypes.c_long
|
|
1639
|
+
self.ntdll.NtCreateFile.argtypes = (
|
|
1640
|
+
ctypes.POINTER(wintypes.HANDLE), wintypes.DWORD, ctypes.POINTER(ObjectAttributes),
|
|
1641
|
+
ctypes.POINTER(IoStatusBlock), ctypes.c_void_p, wintypes.ULONG, wintypes.ULONG,
|
|
1642
|
+
wintypes.ULONG, wintypes.ULONG, ctypes.c_void_p, wintypes.ULONG)
|
|
1643
|
+
self.ntdll.RtlNtStatusToDosError.restype = wintypes.ULONG
|
|
1644
|
+
self.ntdll.RtlNtStatusToDosError.argtypes = (ctypes.c_long,)
|
|
1645
|
+
self.ntdll.NtSetInformationFile.restype = ctypes.c_long
|
|
1646
|
+
self.ntdll.NtSetInformationFile.argtypes = (
|
|
1647
|
+
wintypes.HANDLE, ctypes.POINTER(IoStatusBlock), ctypes.c_void_p, wintypes.ULONG,
|
|
1648
|
+
ctypes.c_int)
|
|
1649
|
+
|
|
1650
|
+
def adopt(self, handle: int) -> int:
|
|
1651
|
+
"""A CRT descriptor that owns handle, so os.fstat, os.write, and os.close apply."""
|
|
1652
|
+
try:
|
|
1653
|
+
descriptor = self.msvcrt.open_osfhandle(handle, 0)
|
|
1654
|
+
except OSError:
|
|
1655
|
+
self.kernel32.CloseHandle(self.wintypes.HANDLE(handle))
|
|
1656
|
+
raise
|
|
1657
|
+
# The CRT adopts handles in text mode, where os.write turns "\n" into "\r\n".
|
|
1658
|
+
self.msvcrt.setmode(descriptor, os.O_BINARY)
|
|
1659
|
+
return descriptor
|
|
1660
|
+
|
|
1661
|
+
def open_root(self, path: Path) -> int:
|
|
1662
|
+
handle = self.kernel32.CreateFileW(
|
|
1663
|
+
str(path), _DIRECTORY_ACCESS, _PIN_SHARE, None, _OPEN_EXISTING,
|
|
1664
|
+
_FILE_FLAG_BACKUP_SEMANTICS | _FILE_FLAG_OPEN_REPARSE_POINT, None)
|
|
1665
|
+
if handle is None or handle == self.wintypes.HANDLE(-1).value:
|
|
1666
|
+
raise self.ctypes.WinError(self.ctypes.get_last_error())
|
|
1667
|
+
return self.adopt(handle)
|
|
1668
|
+
|
|
1669
|
+
def open_relative(
|
|
1670
|
+
self, parent_fd: int, name: str, access: int, share: int, disposition: int, options: int
|
|
1671
|
+
) -> int:
|
|
1672
|
+
"""Open name inside the directory held by parent_fd, never following a reparse point."""
|
|
1673
|
+
ctypes = self.ctypes
|
|
1674
|
+
buffer = ctypes.create_unicode_buffer(name)
|
|
1675
|
+
length = len(name) * ctypes.sizeof(ctypes.c_wchar)
|
|
1676
|
+
object_name = self.UnicodeString(length, length + ctypes.sizeof(ctypes.c_wchar),
|
|
1677
|
+
ctypes.cast(buffer, self.wintypes.LPWSTR))
|
|
1678
|
+
attributes = self.ObjectAttributes(
|
|
1679
|
+
ctypes.sizeof(self.ObjectAttributes), self.msvcrt.get_osfhandle(parent_fd),
|
|
1680
|
+
ctypes.pointer(object_name), _OBJ_CASE_INSENSITIVE, None, None)
|
|
1681
|
+
handle = self.wintypes.HANDLE()
|
|
1682
|
+
status = self.ntdll.NtCreateFile(
|
|
1683
|
+
ctypes.byref(handle), access | _SYNCHRONIZE, ctypes.byref(attributes),
|
|
1684
|
+
ctypes.byref(self.IoStatusBlock()), None, 0, share, disposition,
|
|
1685
|
+
options | _FILE_SYNCHRONOUS_IO_NONALERT | _FILE_OPEN_REPARSE_POINT, None, 0)
|
|
1686
|
+
if status < 0:
|
|
1687
|
+
raise ctypes.WinError(self.ntdll.RtlNtStatusToDosError(status))
|
|
1688
|
+
return self.adopt(handle.value)
|
|
1689
|
+
|
|
1690
|
+
def rename_no_replace(self, fd: int, directory_fd: int, name: str) -> None:
|
|
1691
|
+
"""Rename the file held by fd to name inside the directory held by directory_fd.
|
|
1692
|
+
|
|
1693
|
+
The renameat equivalent; fails when name already exists.
|
|
1694
|
+
"""
|
|
1695
|
+
ctypes, wintypes = self.ctypes, self.wintypes
|
|
1696
|
+
|
|
1697
|
+
class RenameInformation(ctypes.Structure):
|
|
1698
|
+
_fields_ = [("ReplaceIfExists", wintypes.BOOLEAN), ("RootDirectory", wintypes.HANDLE),
|
|
1699
|
+
("FileNameLength", wintypes.ULONG),
|
|
1700
|
+
("FileName", wintypes.WCHAR * (len(name) + 1))]
|
|
1701
|
+
|
|
1702
|
+
info = RenameInformation(False, self.msvcrt.get_osfhandle(directory_fd),
|
|
1703
|
+
len(name) * ctypes.sizeof(ctypes.c_wchar), name)
|
|
1704
|
+
status = self.ntdll.NtSetInformationFile(
|
|
1705
|
+
self.msvcrt.get_osfhandle(fd), ctypes.byref(self.IoStatusBlock()),
|
|
1706
|
+
ctypes.byref(info), ctypes.sizeof(info), _FILE_RENAME_INFORMATION)
|
|
1707
|
+
if status < 0:
|
|
1708
|
+
raise ctypes.WinError(self.ntdll.RtlNtStatusToDosError(status))
|
|
1709
|
+
|
|
1710
|
+
def delete_on_close(self, fd: int) -> None:
|
|
1711
|
+
"""Delete exactly the file held by fd when it closes, whatever its name is now."""
|
|
1712
|
+
ctypes = self.ctypes
|
|
1713
|
+
flag = ctypes.c_ubyte(1)
|
|
1714
|
+
if not self.kernel32.SetFileInformationByHandle(
|
|
1715
|
+
self.msvcrt.get_osfhandle(fd), _FILE_DISPOSITION_INFO,
|
|
1716
|
+
ctypes.byref(flag), ctypes.sizeof(flag)):
|
|
1717
|
+
raise ctypes.WinError(ctypes.get_last_error())
|
|
1718
|
+
|
|
1719
|
+
|
|
1720
|
+
_WINDOWS_HANDLES: Optional[_WindowsHandles] = None
|
|
1721
|
+
|
|
1722
|
+
|
|
1723
|
+
def get_windows_handles() -> _WindowsHandles:
|
|
1724
|
+
global _WINDOWS_HANDLES
|
|
1725
|
+
if _WINDOWS_HANDLES is None:
|
|
1726
|
+
_WINDOWS_HANDLES = _WindowsHandles()
|
|
1727
|
+
return _WINDOWS_HANDLES
|
|
1728
|
+
|
|
1729
|
+
|
|
1730
|
+
def _windows_missing_ok(error: OSError) -> bool:
|
|
1731
|
+
return getattr(error, "winerror", None) in {2, 3}
|
|
1732
|
+
|
|
1733
|
+
|
|
1734
|
+
def read_windows_anchored_evidence(
|
|
1735
|
+
relative: Path,
|
|
1736
|
+
root: Path,
|
|
1737
|
+
*,
|
|
1738
|
+
missing_ok: bool,
|
|
1739
|
+
evidence_name: str,
|
|
1740
|
+
) -> Optional[str]:
|
|
1741
|
+
win = get_windows_handles()
|
|
1742
|
+
descriptors: list[int] = []
|
|
1743
|
+
try:
|
|
1744
|
+
descriptors.append(win.open_root(root))
|
|
1745
|
+
opened_root = os.fstat(descriptors[-1])
|
|
1746
|
+
if not stat.S_ISDIR(opened_root.st_mode):
|
|
1747
|
+
raise DetectError(f"repo evidence is not a directory: {root}")
|
|
1748
|
+
for index, part in enumerate(relative.parts):
|
|
1749
|
+
current = root.joinpath(*relative.parts[: index + 1])
|
|
1750
|
+
final = index == len(relative.parts) - 1
|
|
1751
|
+
access = _GENERIC_READ if final else _DIRECTORY_ACCESS
|
|
1752
|
+
options = _FILE_NON_DIRECTORY_FILE if final else _FILE_DIRECTORY_FILE
|
|
1753
|
+
try:
|
|
1754
|
+
descriptor = win.open_relative(
|
|
1755
|
+
descriptors[-1],
|
|
1756
|
+
part,
|
|
1757
|
+
access,
|
|
1758
|
+
_PIN_SHARE,
|
|
1759
|
+
_FILE_OPEN,
|
|
1760
|
+
options,
|
|
1761
|
+
)
|
|
1762
|
+
except OSError as error:
|
|
1763
|
+
if missing_ok and _windows_missing_ok(error):
|
|
1764
|
+
return None
|
|
1765
|
+
raise DetectError(
|
|
1766
|
+
f"filesystem evidence disappeared: {root / relative}"
|
|
1767
|
+
) from error
|
|
1768
|
+
descriptors.append(descriptor)
|
|
1769
|
+
actual = os.fstat(descriptor)
|
|
1770
|
+
if is_link_like(current, actual):
|
|
1771
|
+
raise DetectError(
|
|
1772
|
+
f"link-like {evidence_name} evidence is not allowed: {current}"
|
|
1773
|
+
)
|
|
1774
|
+
if final and not stat.S_ISREG(actual.st_mode):
|
|
1775
|
+
raise DetectError(
|
|
1776
|
+
f"{evidence_name} evidence is not a regular file: {current}"
|
|
1777
|
+
)
|
|
1778
|
+
if not final and not stat.S_ISDIR(actual.st_mode):
|
|
1779
|
+
raise DetectError(
|
|
1780
|
+
f"{evidence_name} evidence parent is not a directory: {current}"
|
|
1781
|
+
)
|
|
1782
|
+
with os.fdopen(descriptors.pop(), "rb") as stream:
|
|
1783
|
+
data = stream.read()
|
|
1784
|
+
except DetectError:
|
|
1785
|
+
raise
|
|
1786
|
+
except OSError as error:
|
|
1787
|
+
raise DetectError(
|
|
1788
|
+
f"cannot read {evidence_name} evidence: {root / relative}"
|
|
1789
|
+
) from error
|
|
1790
|
+
finally:
|
|
1791
|
+
for descriptor in reversed(descriptors):
|
|
1792
|
+
os.close(descriptor)
|
|
1793
|
+
try:
|
|
1794
|
+
return data.decode("utf-8")
|
|
1795
|
+
except UnicodeDecodeError as error:
|
|
1796
|
+
raise DetectError(
|
|
1797
|
+
f"cannot read {evidence_name} evidence: {root / relative}"
|
|
1798
|
+
) from error
|
|
1799
|
+
|
|
1800
|
+
|
|
1801
|
+
def iter_worktree_files_windows_anchored(root: Path) -> Iterable[str]:
|
|
1802
|
+
win = get_windows_handles()
|
|
1803
|
+
try:
|
|
1804
|
+
root_fd = win.open_root(root)
|
|
1805
|
+
except OSError as error:
|
|
1806
|
+
raise DetectError(f"cannot traverse filesystem evidence: {error}") from error
|
|
1807
|
+
try:
|
|
1808
|
+
root_status = os.fstat(root_fd)
|
|
1809
|
+
if not stat.S_ISDIR(root_status.st_mode):
|
|
1810
|
+
raise DetectError(f"repo evidence is not a directory: {root}")
|
|
1811
|
+
yield from iter_from_windows_dir_fd(root_fd, "", root, win)
|
|
1812
|
+
finally:
|
|
1813
|
+
os.close(root_fd)
|
|
1814
|
+
|
|
1815
|
+
|
|
1816
|
+
def iter_from_windows_dir_fd(
|
|
1817
|
+
dir_fd: int, relative_dir: str, root: Path, win: _WindowsHandles
|
|
1818
|
+
) -> Iterable[str]:
|
|
1819
|
+
scan_dir = root if not relative_dir else root.joinpath(*PurePosixPath(relative_dir).parts)
|
|
1820
|
+
try:
|
|
1821
|
+
# Windows CPython cannot listdir(dir_fd); paths come from the pinned walk.
|
|
1822
|
+
names = os.listdir(scan_dir)
|
|
1823
|
+
except OSError as error:
|
|
1824
|
+
raise DetectError(f"cannot traverse filesystem evidence: {error}") from error
|
|
1825
|
+
for name in names:
|
|
1826
|
+
relative = f"{relative_dir}/{name}" if relative_dir else name
|
|
1827
|
+
if is_ignored(relative) or name == ".project":
|
|
1828
|
+
continue
|
|
1829
|
+
path = root.joinpath(*PurePosixPath(relative).parts)
|
|
1830
|
+
try:
|
|
1831
|
+
child_fd = win.open_relative(
|
|
1832
|
+
dir_fd,
|
|
1833
|
+
name,
|
|
1834
|
+
_DIRECTORY_ACCESS,
|
|
1835
|
+
_PIN_SHARE,
|
|
1836
|
+
_FILE_OPEN,
|
|
1837
|
+
_FILE_DIRECTORY_FILE,
|
|
1838
|
+
)
|
|
1839
|
+
except OSError:
|
|
1840
|
+
child_fd = None
|
|
1841
|
+
else:
|
|
1842
|
+
status = os.fstat(child_fd)
|
|
1843
|
+
if is_link_like(path, status):
|
|
1844
|
+
os.close(child_fd)
|
|
1845
|
+
continue
|
|
1846
|
+
if stat.S_ISDIR(status.st_mode):
|
|
1847
|
+
try:
|
|
1848
|
+
if is_verified_skill_bundle_at(relative, child_fd, root):
|
|
1849
|
+
continue
|
|
1850
|
+
yield from iter_from_windows_dir_fd(
|
|
1851
|
+
child_fd, relative, root, win
|
|
1852
|
+
)
|
|
1853
|
+
finally:
|
|
1854
|
+
os.close(child_fd)
|
|
1855
|
+
continue
|
|
1856
|
+
os.close(child_fd)
|
|
1857
|
+
try:
|
|
1858
|
+
file_fd = win.open_relative(
|
|
1859
|
+
dir_fd,
|
|
1860
|
+
name,
|
|
1861
|
+
_FILE_READ_ATTRIBUTES,
|
|
1862
|
+
_PIN_SHARE,
|
|
1863
|
+
_FILE_OPEN,
|
|
1864
|
+
_FILE_NON_DIRECTORY_FILE,
|
|
1865
|
+
)
|
|
1866
|
+
except OSError as error:
|
|
1867
|
+
raise DetectError(
|
|
1868
|
+
f"cannot inspect filesystem evidence: {relative}: {error}"
|
|
1869
|
+
) from error
|
|
1870
|
+
try:
|
|
1871
|
+
status = os.fstat(file_fd)
|
|
1872
|
+
if is_link_like(path, status) or not stat.S_ISREG(status.st_mode):
|
|
1873
|
+
continue
|
|
1874
|
+
if is_fixed_managed_pipeline_artifact(relative):
|
|
1875
|
+
continue
|
|
1876
|
+
yield relative
|
|
1877
|
+
finally:
|
|
1878
|
+
os.close(file_fd)
|
|
1879
|
+
|
|
1880
|
+
|
|
1881
|
+
def windows_project_entries(project: Path) -> set:
|
|
1882
|
+
"""project_entries for a .project held open without delete sharing."""
|
|
1883
|
+
entries = set(os.listdir(project))
|
|
1884
|
+
for name in entries & _common.OS_JUNK_NAMES:
|
|
1885
|
+
try:
|
|
1886
|
+
status = os.stat(project / name, follow_symlinks=False)
|
|
1887
|
+
except FileNotFoundError:
|
|
1888
|
+
continue
|
|
1889
|
+
if ignored_junk(project, name, status):
|
|
1890
|
+
entries.discard(name)
|
|
1891
|
+
return entries
|
|
1892
|
+
|
|
1893
|
+
|
|
1894
|
+
def write_state_windows(
|
|
1895
|
+
root: Path,
|
|
1896
|
+
content: str,
|
|
1897
|
+
expected_root: os.stat_result,
|
|
1898
|
+
expected_project: Optional[os.stat_result],
|
|
1899
|
+
) -> None:
|
|
1900
|
+
win = _WindowsHandles()
|
|
1901
|
+
payload = content.encode("utf-8")
|
|
1902
|
+
project = root / ".project"
|
|
1903
|
+
root_fd = project_fd = state_fd = None
|
|
1904
|
+
delete_on_failure = False
|
|
1905
|
+
# The same lock pipeline_state takes for this track.
|
|
1906
|
+
with _common.directory_mutex(project):
|
|
1907
|
+
try:
|
|
1908
|
+
try:
|
|
1909
|
+
root_fd = win.open_root(root)
|
|
1910
|
+
except OSError as error:
|
|
1911
|
+
raise DetectError(f"cannot open repo for STATE.md creation: {error}") from error
|
|
1912
|
+
opened_root = os.fstat(root_fd)
|
|
1913
|
+
if (
|
|
1914
|
+
is_link_like(root, opened_root)
|
|
1915
|
+
or not stat.S_ISDIR(opened_root.st_mode)
|
|
1916
|
+
or not os.path.samestat(expected_root, opened_root)
|
|
1917
|
+
):
|
|
1918
|
+
raise DetectError("repo changed after classification")
|
|
1919
|
+
try:
|
|
1920
|
+
project_fd = win.open_relative(
|
|
1921
|
+
root_fd, ".project", _DIRECTORY_ACCESS, _PIN_SHARE,
|
|
1922
|
+
_FILE_CREATE if expected_project is None else _FILE_OPEN,
|
|
1923
|
+
_FILE_DIRECTORY_FILE)
|
|
1924
|
+
except FileExistsError as error:
|
|
1925
|
+
raise DetectError(".project changed after classification") from error
|
|
1926
|
+
except OSError as error:
|
|
1927
|
+
raise DetectError(f"cannot open .project for STATE.md creation: {error}") from error
|
|
1928
|
+
opened_project = os.fstat(project_fd)
|
|
1929
|
+
if is_link_like(project, opened_project) or not stat.S_ISDIR(opened_project.st_mode):
|
|
1930
|
+
raise DetectError("cannot create state through a non-directory .project")
|
|
1931
|
+
if expected_project is not None and not os.path.samestat(expected_project, opened_project):
|
|
1932
|
+
raise DetectError(".project changed after classification")
|
|
1933
|
+
if windows_project_entries(project) not in (set(), {STATE_TEMP_NAME}):
|
|
1934
|
+
raise DetectError(".project changed after classification")
|
|
1935
|
+
try:
|
|
1936
|
+
# Share mode 0: nothing else may open the temporary file while it is held.
|
|
1937
|
+
state_fd = win.open_relative(
|
|
1938
|
+
project_fd, STATE_TEMP_NAME, _GENERIC_READ | _GENERIC_WRITE | _DELETE,
|
|
1939
|
+
0, _FILE_OPEN_IF, _FILE_NON_DIRECTORY_FILE)
|
|
1940
|
+
except OSError as error:
|
|
1941
|
+
raise DetectError(f"cannot reserve temporary STATE.md: {error}") from error
|
|
1942
|
+
created_status = os.fstat(state_fd)
|
|
1943
|
+
if is_link_like(project / STATE_TEMP_NAME, created_status) or not stat.S_ISREG(
|
|
1944
|
+
created_status.st_mode
|
|
1945
|
+
):
|
|
1946
|
+
raise DetectError("temporary STATE.md is not a regular file")
|
|
1947
|
+
if created_status.st_nlink != 1:
|
|
1948
|
+
raise DetectError("temporary STATE.md has an unexpected link count")
|
|
1949
|
+
delete_on_failure = True
|
|
1950
|
+
if "STATE.md" in os.listdir(project):
|
|
1951
|
+
raise DetectError("STATE.md already exists")
|
|
1952
|
+
os.ftruncate(state_fd, 0)
|
|
1953
|
+
write_all(state_fd, payload)
|
|
1954
|
+
os.fsync(state_fd)
|
|
1955
|
+
current_root = lstat_evidence(root, missing_ok=False)
|
|
1956
|
+
current_project = lstat_evidence(project, missing_ok=False)
|
|
1957
|
+
if (
|
|
1958
|
+
is_link_like(root, current_root)
|
|
1959
|
+
or not os.path.samestat(opened_root, current_root)
|
|
1960
|
+
or is_link_like(project, current_project)
|
|
1961
|
+
or not os.path.samestat(opened_project, current_project)
|
|
1962
|
+
):
|
|
1963
|
+
raise DetectError("project identity changed while creating STATE.md")
|
|
1964
|
+
current_state = os.fstat(state_fd)
|
|
1965
|
+
if current_state.st_nlink != 1 or not os.path.samestat(created_status, current_state):
|
|
1966
|
+
raise DetectError("temporary STATE.md changed while writing")
|
|
1967
|
+
if windows_project_entries(project) != {STATE_TEMP_NAME}:
|
|
1968
|
+
raise DetectError(".project contents changed while creating STATE.md")
|
|
1969
|
+
try:
|
|
1970
|
+
win.rename_no_replace(state_fd, project_fd, "STATE.md")
|
|
1971
|
+
except OSError as error:
|
|
1972
|
+
if "STATE.md" in os.listdir(project):
|
|
1973
|
+
raise DetectError("STATE.md already exists") from error
|
|
1974
|
+
raise DetectError(f"cannot publish STATE.md: {error}") from error
|
|
1975
|
+
published = lstat_evidence(project / "STATE.md", missing_ok=False)
|
|
1976
|
+
if (
|
|
1977
|
+
is_link_like(project / "STATE.md", published)
|
|
1978
|
+
or published.st_nlink != 1
|
|
1979
|
+
or not os.path.samestat(created_status, published)
|
|
1980
|
+
):
|
|
1981
|
+
raise DetectError("STATE.md changed while publishing")
|
|
1982
|
+
if windows_project_entries(project) != {"STATE.md"}:
|
|
1983
|
+
raise DetectError(".project contents changed while publishing STATE.md")
|
|
1984
|
+
delete_on_failure = False
|
|
1985
|
+
closing_state_fd, state_fd = state_fd, None
|
|
1986
|
+
try:
|
|
1987
|
+
os.close(closing_state_fd)
|
|
1988
|
+
except OSError as error:
|
|
1989
|
+
raise DetectError(f"cannot close STATE.md: {error}") from error
|
|
1990
|
+
except BaseException as error:
|
|
1991
|
+
if delete_on_failure and state_fd is not None:
|
|
1992
|
+
try:
|
|
1993
|
+
win.delete_on_close(state_fd)
|
|
1994
|
+
except OSError as rollback_error:
|
|
1995
|
+
raise DetectError(
|
|
1996
|
+
f"cannot roll back STATE.md: {rollback_error}") from error
|
|
1997
|
+
if isinstance(error, DetectError):
|
|
1998
|
+
raise
|
|
1999
|
+
if isinstance(error, OSError):
|
|
2000
|
+
raise DetectError(f"cannot create STATE.md: {error}") from error
|
|
2001
|
+
raise
|
|
2002
|
+
finally:
|
|
2003
|
+
close_file_descriptors(state_fd, project_fd, root_fd)
|
|
2004
|
+
|
|
2005
|
+
|
|
1451
2006
|
def initialize(
|
|
1452
2007
|
repo: Path, template: Path, phase: Optional[str] = None, *, require_git: bool = False
|
|
1453
2008
|
) -> dict:
|
|
@@ -1471,7 +2026,7 @@ def initialize(
|
|
|
1471
2026
|
if require_git:
|
|
1472
2027
|
git = subprocess.run(
|
|
1473
2028
|
["git", "-C", str(root), "rev-parse", "--is-inside-work-tree"],
|
|
1474
|
-
env=git_environment(), capture_output=True,
|
|
2029
|
+
env=git_environment(), capture_output=True, encoding="utf-8", errors="replace", check=False,
|
|
1475
2030
|
)
|
|
1476
2031
|
if git.returncode != 0 or git.stdout.strip() != "true":
|
|
1477
2032
|
return {**payload, "route": "setup-repository", "wrote_state": False}
|
|
@@ -1482,7 +2037,7 @@ def initialize(
|
|
|
1482
2037
|
raise DetectError(
|
|
1483
2038
|
f"initialize phase {phase} does not match verdict {payload['verdict']}"
|
|
1484
2039
|
)
|
|
1485
|
-
if not
|
|
2040
|
+
if not STATE_CREATE_SUPPORTED:
|
|
1486
2041
|
payload["wrote_state"] = False
|
|
1487
2042
|
payload["error"] = "anchored no-follow STATE.md creation is unavailable"
|
|
1488
2043
|
return payload
|