@opengsd/gsd-path 1.3.0 → 1.4.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 +294 -0
- package/DOCS.md +19 -10
- package/FULL.md +9 -3
- package/GUIDE.md +1 -1
- package/HOOKS.md +9 -6
- package/QUICK.md +4 -1
- package/README.md +63 -11
- package/RUNTIME.md +1 -1
- package/UPDATE.md +61 -12
- package/WORKFLOW.md +22 -6
- package/package.json +9 -8
- package/scripts/_common.py +472 -19
- package/scripts/archive_milestone.py +146 -73
- package/scripts/bootstrap_repository.py +8 -5
- package/scripts/build_recovery.py +4 -1
- package/scripts/build_state.py +66 -23
- package/scripts/check_docs_audit.py +14 -3
- package/scripts/check_handoffs.py +116 -20
- package/scripts/check_task_briefs.py +60 -8
- 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 +345 -15
- package/scripts/discussion_records.py +19 -13
- package/scripts/discussion_validate.py +66 -21
- package/scripts/dispatch_driver.py +111 -26
- package/scripts/git_guard.py +116 -29
- package/scripts/guard_hook.py +151 -29
- package/scripts/install.mjs +55 -39
- package/scripts/install.py +564 -41
- package/scripts/integration.py +434 -24
- package/scripts/isolation.py +1077 -28
- package/scripts/lean_verification.py +99 -19
- package/scripts/loop_run.py +20 -30
- package/scripts/members.py +704 -0
- package/scripts/migrate_core.py +2 -2
- package/scripts/path_config.py +2 -2
- package/scripts/pipeline_diagnose.py +82 -31
- package/scripts/pipeline_git.py +61 -22
- package/scripts/pipeline_state.py +176 -54
- package/scripts/pipeline_undo.py +117 -22
- package/scripts/promote_lookahead.py +1 -1
- package/scripts/review_panel.py +1 -1
- package/scripts/runtime_store.py +49 -11
- package/scripts/skill-resources.json +28 -3
- package/scripts/state_checkpoint.py +140 -31
- package/scripts/state_promote.py +2 -0
- package/scripts/status_runtime.py +36 -6
- package/scripts/sync_skill_resources.py +8 -7
- package/scripts/token_budget.py +12 -8
- package/scripts/workflow_run.py +103 -29
- package/scripts/worktree_paths.py +18 -8
- package/skills/gsd-path/BUILD.md +7 -1
- package/skills/gsd-path/DECIDE.md +11 -4
- package/skills/gsd-path/DEFINE.md +8 -1
- package/skills/gsd-path/DOCS-AUDIT.md +6 -4
- package/skills/gsd-path/INSPECT.md +5 -4
- package/skills/gsd-path/PLAN.md +7 -1
- package/skills/gsd-path/RESEARCH.md +2 -1
- package/skills/gsd-path/ROADMAP.md +2 -1
- package/skills/gsd-path/SHIP.md +27 -10
- package/skills/gsd-path/SKILL.md +45 -1
- package/skills/gsd-path/references/build-native.md +47 -14
- package/skills/gsd-path/references/codebase-mapper.md +2 -1
- package/skills/gsd-path/references/docs-auditor.md +2 -1
- package/skills/gsd-path/references/operating-rules.md +214 -0
- package/skills/gsd-path/references/planner.md +8 -0
- package/skills/gsd-path/references/reviewer-wave.md +3 -1
- package/skills/gsd-path/scripts/__pycache__/_common.cpython-312.pyc +0 -0
- package/skills/gsd-path/scripts/__pycache__/check_handoffs.cpython-312.pyc +0 -0
- package/skills/gsd-path/scripts/_common.py +472 -19
- package/skills/gsd-path/scripts/archive_milestone.py +146 -73
- package/skills/gsd-path/scripts/bootstrap_repository.py +8 -5
- package/skills/gsd-path/scripts/build_recovery.py +4 -1
- package/skills/gsd-path/scripts/build_state.py +66 -23
- package/skills/gsd-path/scripts/check_docs_audit.py +14 -3
- package/skills/gsd-path/scripts/check_handoffs.py +116 -20
- package/skills/gsd-path/scripts/check_task_briefs.py +60 -8
- package/skills/gsd-path/scripts/check_update.py +2 -2
- package/skills/gsd-path/scripts/detect_project.py +345 -15
- package/skills/gsd-path/scripts/discussion_records.py +19 -13
- package/skills/gsd-path/scripts/discussion_validate.py +66 -21
- package/skills/gsd-path/scripts/dispatch_driver.py +111 -26
- package/skills/gsd-path/scripts/git_guard.py +116 -29
- package/skills/gsd-path/scripts/integration.py +434 -24
- package/skills/gsd-path/scripts/isolation.py +1077 -28
- package/skills/gsd-path/scripts/lean_verification.py +99 -19
- package/skills/gsd-path/scripts/loop_run.py +20 -30
- package/skills/gsd-path/scripts/members.py +704 -0
- package/skills/gsd-path/scripts/path_config.py +2 -2
- package/skills/gsd-path/scripts/pipeline_git.py +61 -22
- package/skills/gsd-path/scripts/pipeline_state.py +176 -54
- 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 +140 -31
- package/skills/gsd-path/scripts/state_promote.py +2 -0
- package/skills/gsd-path/scripts/status_runtime.py +36 -6
- package/skills/gsd-path/scripts/token_budget.py +12 -8
- package/skills/gsd-path/scripts/workflow_run.py +103 -29
- package/skills/gsd-path/scripts/worktree_paths.py +18 -8
- package/skills/gsd-path/templates/codebase.md +1 -1
- package/skills/gsd-path/templates/docs-audit.md +1 -1
- package/skills/gsd-path/templates/final-review.md +5 -2
- package/skills/gsd-path/templates/gap-review.md +2 -1
- package/skills/gsd-path/templates/task.md +1 -0
- package/skills/gsd-path/templates/wave-review.md +3 -1
- package/skills/gsd-path-build/SKILL.md +7 -1
- package/skills/gsd-path-build/references/build-native.md +47 -14
- package/skills/gsd-path-build/references/operating-rules.md +214 -0
- package/skills/gsd-path-build/references/reviewer-wave.md +3 -1
- package/skills/gsd-path-build/scripts/__pycache__/_common.cpython-312.pyc +0 -0
- package/skills/gsd-path-build/scripts/__pycache__/check_handoffs.cpython-312.pyc +0 -0
- package/skills/gsd-path-build/scripts/_common.py +472 -19
- package/skills/gsd-path-build/scripts/archive_milestone.py +146 -73
- package/skills/gsd-path-build/scripts/build_recovery.py +4 -1
- package/skills/gsd-path-build/scripts/build_state.py +66 -23
- package/skills/gsd-path-build/scripts/check_handoffs.py +116 -20
- package/skills/gsd-path-build/scripts/check_task_briefs.py +60 -8
- package/skills/gsd-path-build/scripts/detect_project.py +345 -15
- package/skills/gsd-path-build/scripts/discussion_records.py +19 -13
- package/skills/gsd-path-build/scripts/discussion_validate.py +66 -21
- package/skills/gsd-path-build/scripts/dispatch_driver.py +111 -26
- package/skills/gsd-path-build/scripts/integration.py +434 -24
- package/skills/gsd-path-build/scripts/isolation.py +1077 -28
- package/skills/gsd-path-build/scripts/loop_run.py +20 -30
- package/skills/gsd-path-build/scripts/members.py +704 -0
- package/skills/gsd-path-build/scripts/path_config.py +2 -2
- package/skills/gsd-path-build/scripts/pipeline_git.py +61 -22
- package/skills/gsd-path-build/scripts/pipeline_state.py +176 -54
- 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 +140 -31
- package/skills/gsd-path-build/scripts/state_promote.py +2 -0
- package/skills/gsd-path-build/scripts/token_budget.py +12 -8
- package/skills/gsd-path-build/scripts/workflow_run.py +103 -29
- package/skills/gsd-path-build/scripts/worktree_paths.py +18 -8
- package/skills/gsd-path-build/templates/task.md +1 -0
- package/skills/gsd-path-build/templates/wave-review.md +3 -1
- package/skills/gsd-path-decide/SKILL.md +11 -4
- package/skills/gsd-path-decide/references/operating-rules.md +214 -0
- package/skills/gsd-path-decide/scripts/_common.py +472 -19
- package/skills/gsd-path-decide/scripts/archive_milestone.py +146 -73
- package/skills/gsd-path-decide/scripts/build_recovery.py +4 -1
- package/skills/gsd-path-decide/scripts/check_handoffs.py +116 -20
- package/skills/gsd-path-decide/scripts/check_task_briefs.py +60 -8
- package/skills/gsd-path-decide/scripts/detect_project.py +345 -15
- package/skills/gsd-path-decide/scripts/discussion_records.py +19 -13
- package/skills/gsd-path-decide/scripts/discussion_validate.py +66 -21
- package/skills/gsd-path-decide/scripts/integration.py +434 -24
- package/skills/gsd-path-decide/scripts/isolation.py +1077 -28
- package/skills/gsd-path-decide/scripts/loop_run.py +20 -30
- package/skills/gsd-path-decide/scripts/members.py +704 -0
- package/skills/gsd-path-decide/scripts/path_config.py +2 -2
- package/skills/gsd-path-decide/scripts/pipeline_git.py +61 -22
- package/skills/gsd-path-decide/scripts/pipeline_state.py +176 -54
- 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 +140 -31
- package/skills/gsd-path-decide/scripts/state_promote.py +2 -0
- package/skills/gsd-path-decide/scripts/worktree_paths.py +18 -8
- package/skills/gsd-path-define/SKILL.md +8 -1
- package/skills/gsd-path-define/references/operating-rules.md +214 -0
- package/skills/gsd-path-define/scripts/_common.py +472 -19
- package/skills/gsd-path-define/scripts/archive_milestone.py +146 -73
- package/skills/gsd-path-define/scripts/build_recovery.py +4 -1
- package/skills/gsd-path-define/scripts/check_handoffs.py +116 -20
- package/skills/gsd-path-define/scripts/check_task_briefs.py +60 -8
- package/skills/gsd-path-define/scripts/detect_project.py +345 -15
- package/skills/gsd-path-define/scripts/discussion_records.py +19 -13
- package/skills/gsd-path-define/scripts/discussion_validate.py +66 -21
- package/skills/gsd-path-define/scripts/integration.py +434 -24
- package/skills/gsd-path-define/scripts/isolation.py +1077 -28
- package/skills/gsd-path-define/scripts/loop_run.py +20 -30
- package/skills/gsd-path-define/scripts/members.py +704 -0
- package/skills/gsd-path-define/scripts/path_config.py +2 -2
- package/skills/gsd-path-define/scripts/pipeline_git.py +61 -22
- package/skills/gsd-path-define/scripts/pipeline_state.py +176 -54
- 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 +140 -31
- package/skills/gsd-path-define/scripts/state_promote.py +2 -0
- package/skills/gsd-path-define/scripts/worktree_paths.py +18 -8
- 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 +472 -19
- package/skills/gsd-path-discuss/scripts/discussion_records.py +19 -13
- package/skills/gsd-path-discuss/scripts/loop_run.py +20 -30
- package/skills/gsd-path-discuss/scripts/path_config.py +2 -2
- package/skills/gsd-path-discuss/scripts/review_panel.py +1 -1
- package/skills/gsd-path-discuss/scripts/worktree_paths.py +18 -8
- package/skills/gsd-path-docs-audit/SKILL.md +6 -4
- package/skills/gsd-path-docs-audit/references/docs-auditor.md +2 -1
- package/skills/gsd-path-docs-audit/references/operating-rules.md +214 -0
- package/skills/gsd-path-docs-audit/scripts/_common.py +472 -19
- package/skills/gsd-path-docs-audit/scripts/archive_milestone.py +146 -73
- package/skills/gsd-path-docs-audit/scripts/build_recovery.py +4 -1
- package/skills/gsd-path-docs-audit/scripts/check_docs_audit.py +14 -3
- package/skills/gsd-path-docs-audit/scripts/check_handoffs.py +116 -20
- package/skills/gsd-path-docs-audit/scripts/check_task_briefs.py +60 -8
- package/skills/gsd-path-docs-audit/scripts/detect_project.py +345 -15
- package/skills/gsd-path-docs-audit/scripts/discussion_records.py +19 -13
- package/skills/gsd-path-docs-audit/scripts/discussion_validate.py +66 -21
- package/skills/gsd-path-docs-audit/scripts/integration.py +434 -24
- package/skills/gsd-path-docs-audit/scripts/isolation.py +1077 -28
- package/skills/gsd-path-docs-audit/scripts/loop_run.py +20 -30
- package/skills/gsd-path-docs-audit/scripts/members.py +704 -0
- package/skills/gsd-path-docs-audit/scripts/path_config.py +2 -2
- package/skills/gsd-path-docs-audit/scripts/pipeline_git.py +61 -22
- package/skills/gsd-path-docs-audit/scripts/pipeline_state.py +176 -54
- 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 +140 -31
- package/skills/gsd-path-docs-audit/scripts/state_promote.py +2 -0
- package/skills/gsd-path-docs-audit/scripts/worktree_paths.py +18 -8
- package/skills/gsd-path-docs-audit/templates/docs-audit.md +1 -1
- 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 +472 -19
- package/skills/gsd-path-forensics/scripts/discussion_records.py +19 -13
- package/skills/gsd-path-forensics/scripts/loop_run.py +20 -30
- package/skills/gsd-path-forensics/scripts/path_config.py +2 -2
- package/skills/gsd-path-forensics/scripts/pipeline_diagnose.py +82 -31
- package/skills/gsd-path-forensics/scripts/pipeline_undo.py +117 -22
- package/skills/gsd-path-forensics/scripts/review_panel.py +1 -1
- package/skills/gsd-path-forensics/scripts/worktree_paths.py +18 -8
- 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/docs-auditor.md +2 -1
- package/skills/gsd-path-inspect/references/operating-rules.md +214 -0
- package/skills/gsd-path-inspect/scripts/__pycache__/_common.cpython-312.pyc +0 -0
- package/skills/gsd-path-inspect/scripts/_common.py +472 -19
- package/skills/gsd-path-inspect/scripts/archive_milestone.py +146 -73
- package/skills/gsd-path-inspect/scripts/build_recovery.py +4 -1
- package/skills/gsd-path-inspect/scripts/check_docs_audit.py +14 -3
- package/skills/gsd-path-inspect/scripts/check_handoffs.py +116 -20
- package/skills/gsd-path-inspect/scripts/check_task_briefs.py +60 -8
- package/skills/gsd-path-inspect/scripts/detect_project.py +345 -15
- package/skills/gsd-path-inspect/scripts/discussion_records.py +19 -13
- package/skills/gsd-path-inspect/scripts/discussion_validate.py +66 -21
- package/skills/gsd-path-inspect/scripts/integration.py +434 -24
- package/skills/gsd-path-inspect/scripts/isolation.py +1077 -28
- package/skills/gsd-path-inspect/scripts/loop_run.py +20 -30
- package/skills/gsd-path-inspect/scripts/members.py +704 -0
- package/skills/gsd-path-inspect/scripts/path_config.py +2 -2
- package/skills/gsd-path-inspect/scripts/pipeline_git.py +61 -22
- package/skills/gsd-path-inspect/scripts/pipeline_state.py +176 -54
- 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 +140 -31
- package/skills/gsd-path-inspect/scripts/state_promote.py +2 -0
- package/skills/gsd-path-inspect/scripts/workflow_run.py +103 -29
- package/skills/gsd-path-inspect/scripts/worktree_paths.py +18 -8
- package/skills/gsd-path-inspect/templates/codebase.md +1 -1
- package/skills/gsd-path-inspect/templates/docs-audit.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 +472 -19
- package/skills/gsd-path-loop/scripts/discussion_records.py +19 -13
- package/skills/gsd-path-loop/scripts/loop_run.py +20 -30
- package/skills/gsd-path-loop/scripts/path_config.py +2 -2
- package/skills/gsd-path-loop/scripts/review_panel.py +1 -1
- package/skills/gsd-path-loop/scripts/worktree_paths.py +18 -8
- 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 +472 -19
- 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 +20 -30
- package/skills/gsd-path-migrate/scripts/migrate_core.py +2 -2
- package/skills/gsd-path-migrate/scripts/path_config.py +2 -2
- package/skills/gsd-path-migrate/scripts/review_panel.py +1 -1
- package/skills/gsd-path-plan/SKILL.md +7 -1
- package/skills/gsd-path-plan/references/operating-rules.md +214 -0
- package/skills/gsd-path-plan/references/planner.md +8 -0
- package/skills/gsd-path-plan/scripts/__pycache__/_common.cpython-312.pyc +0 -0
- package/skills/gsd-path-plan/scripts/_common.py +472 -19
- package/skills/gsd-path-plan/scripts/archive_milestone.py +146 -73
- package/skills/gsd-path-plan/scripts/build_recovery.py +4 -1
- package/skills/gsd-path-plan/scripts/check_handoffs.py +116 -20
- package/skills/gsd-path-plan/scripts/check_task_briefs.py +60 -8
- package/skills/gsd-path-plan/scripts/detect_project.py +345 -15
- package/skills/gsd-path-plan/scripts/discussion_records.py +19 -13
- package/skills/gsd-path-plan/scripts/discussion_validate.py +66 -21
- package/skills/gsd-path-plan/scripts/integration.py +434 -24
- package/skills/gsd-path-plan/scripts/isolation.py +1077 -28
- package/skills/gsd-path-plan/scripts/loop_run.py +20 -30
- package/skills/gsd-path-plan/scripts/members.py +704 -0
- package/skills/gsd-path-plan/scripts/path_config.py +2 -2
- package/skills/gsd-path-plan/scripts/pipeline_git.py +61 -22
- package/skills/gsd-path-plan/scripts/pipeline_state.py +176 -54
- 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 +140 -31
- package/skills/gsd-path-plan/scripts/state_promote.py +2 -0
- package/skills/gsd-path-plan/scripts/workflow_run.py +103 -29
- package/skills/gsd-path-plan/scripts/worktree_paths.py +18 -8
- package/skills/gsd-path-plan/templates/task.md +1 -0
- package/skills/gsd-path-research/SKILL.md +2 -1
- package/skills/gsd-path-research/references/operating-rules.md +214 -0
- package/skills/gsd-path-research/scripts/_common.py +472 -19
- package/skills/gsd-path-research/scripts/archive_milestone.py +146 -73
- package/skills/gsd-path-research/scripts/build_recovery.py +4 -1
- package/skills/gsd-path-research/scripts/check_handoffs.py +116 -20
- package/skills/gsd-path-research/scripts/check_task_briefs.py +60 -8
- package/skills/gsd-path-research/scripts/detect_project.py +345 -15
- package/skills/gsd-path-research/scripts/discussion_records.py +19 -13
- package/skills/gsd-path-research/scripts/discussion_validate.py +66 -21
- package/skills/gsd-path-research/scripts/integration.py +434 -24
- package/skills/gsd-path-research/scripts/isolation.py +1077 -28
- package/skills/gsd-path-research/scripts/loop_run.py +20 -30
- package/skills/gsd-path-research/scripts/members.py +704 -0
- package/skills/gsd-path-research/scripts/path_config.py +2 -2
- package/skills/gsd-path-research/scripts/pipeline_git.py +61 -22
- package/skills/gsd-path-research/scripts/pipeline_state.py +176 -54
- 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 +140 -31
- package/skills/gsd-path-research/scripts/state_promote.py +2 -0
- package/skills/gsd-path-research/scripts/worktree_paths.py +18 -8
- package/skills/gsd-path-roadmap/SKILL.md +2 -1
- package/skills/gsd-path-roadmap/references/operating-rules.md +214 -0
- package/skills/gsd-path-roadmap/scripts/_common.py +472 -19
- package/skills/gsd-path-roadmap/scripts/archive_milestone.py +146 -73
- package/skills/gsd-path-roadmap/scripts/build_recovery.py +4 -1
- package/skills/gsd-path-roadmap/scripts/check_handoffs.py +116 -20
- package/skills/gsd-path-roadmap/scripts/check_task_briefs.py +60 -8
- package/skills/gsd-path-roadmap/scripts/detect_project.py +345 -15
- package/skills/gsd-path-roadmap/scripts/discussion_records.py +19 -13
- package/skills/gsd-path-roadmap/scripts/discussion_validate.py +66 -21
- package/skills/gsd-path-roadmap/scripts/integration.py +434 -24
- package/skills/gsd-path-roadmap/scripts/isolation.py +1077 -28
- package/skills/gsd-path-roadmap/scripts/loop_run.py +20 -30
- package/skills/gsd-path-roadmap/scripts/members.py +704 -0
- package/skills/gsd-path-roadmap/scripts/path_config.py +2 -2
- package/skills/gsd-path-roadmap/scripts/pipeline_git.py +61 -22
- package/skills/gsd-path-roadmap/scripts/pipeline_state.py +176 -54
- 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 +140 -31
- package/skills/gsd-path-roadmap/scripts/state_promote.py +2 -0
- package/skills/gsd-path-roadmap/scripts/worktree_paths.py +18 -8
- package/skills/gsd-path-ship/SKILL.md +27 -10
- package/skills/gsd-path-ship/references/operating-rules.md +214 -0
- package/skills/gsd-path-ship/references/reviewer-wave.md +3 -1
- package/skills/gsd-path-ship/scripts/__pycache__/_common.cpython-312.pyc +0 -0
- package/skills/gsd-path-ship/scripts/_common.py +472 -19
- package/skills/gsd-path-ship/scripts/archive_milestone.py +146 -73
- package/skills/gsd-path-ship/scripts/build_recovery.py +4 -1
- package/skills/gsd-path-ship/scripts/build_state.py +66 -23
- package/skills/gsd-path-ship/scripts/check_handoffs.py +116 -20
- package/skills/gsd-path-ship/scripts/check_task_briefs.py +60 -8
- package/skills/gsd-path-ship/scripts/detect_project.py +345 -15
- package/skills/gsd-path-ship/scripts/discussion_records.py +19 -13
- package/skills/gsd-path-ship/scripts/discussion_validate.py +66 -21
- package/skills/gsd-path-ship/scripts/integration.py +434 -24
- package/skills/gsd-path-ship/scripts/isolation.py +1077 -28
- package/skills/gsd-path-ship/scripts/lean_verification.py +99 -19
- package/skills/gsd-path-ship/scripts/loop_run.py +20 -30
- package/skills/gsd-path-ship/scripts/members.py +704 -0
- package/skills/gsd-path-ship/scripts/path_config.py +2 -2
- package/skills/gsd-path-ship/scripts/pipeline_git.py +61 -22
- package/skills/gsd-path-ship/scripts/pipeline_state.py +176 -54
- 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 +140 -31
- package/skills/gsd-path-ship/scripts/state_promote.py +2 -0
- package/skills/gsd-path-ship/scripts/workflow_run.py +103 -29
- package/skills/gsd-path-ship/scripts/worktree_paths.py +18 -8
- package/skills/gsd-path-ship/templates/final-review.md +5 -2
- package/skills/gsd-path-ship/templates/gap-review.md +2 -1
- package/skills/gsd-path-ship/templates/wave-review.md +3 -1
- 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 +472 -19
- package/skills/gsd-path-undo/scripts/discussion_records.py +19 -13
- package/skills/gsd-path-undo/scripts/loop_run.py +20 -30
- package/skills/gsd-path-undo/scripts/path_config.py +2 -2
- package/skills/gsd-path-undo/scripts/pipeline_undo.py +117 -22
- package/skills/gsd-path-undo/scripts/review_panel.py +1 -1
- package/skills/gsd-path-undo/scripts/worktree_paths.py +18 -8
- package/skills/path/BUILD.md +7 -1
- package/skills/path/DECIDE.md +11 -4
- package/skills/path/DEFINE.md +8 -1
- package/skills/path/DOCS-AUDIT.md +6 -4
- package/skills/path/INSPECT.md +5 -4
- package/skills/path/PLAN.md +7 -1
- package/skills/path/RESEARCH.md +2 -1
- package/skills/path/ROADMAP.md +2 -1
- package/skills/path/SHIP.md +27 -10
- package/skills/path/SKILL.md +45 -1
- package/skills/path/references/build-native.md +47 -14
- package/skills/path/references/codebase-mapper.md +2 -1
- package/skills/path/references/docs-auditor.md +2 -1
- package/skills/path/references/operating-rules.md +214 -0
- package/skills/path/references/planner.md +8 -0
- package/skills/path/references/reviewer-wave.md +3 -1
- package/skills/path/scripts/_common.py +472 -19
- package/skills/path/scripts/archive_milestone.py +146 -73
- package/skills/path/scripts/bootstrap_repository.py +8 -5
- package/skills/path/scripts/build_recovery.py +4 -1
- package/skills/path/scripts/build_state.py +66 -23
- package/skills/path/scripts/check_docs_audit.py +14 -3
- package/skills/path/scripts/check_handoffs.py +116 -20
- package/skills/path/scripts/check_task_briefs.py +60 -8
- package/skills/path/scripts/check_update.py +2 -2
- package/skills/path/scripts/detect_project.py +345 -15
- package/skills/path/scripts/discussion_records.py +19 -13
- package/skills/path/scripts/discussion_validate.py +66 -21
- package/skills/path/scripts/dispatch_driver.py +111 -26
- package/skills/path/scripts/git_guard.py +116 -29
- package/skills/path/scripts/integration.py +434 -24
- package/skills/path/scripts/isolation.py +1077 -28
- package/skills/path/scripts/lean_verification.py +99 -19
- package/skills/path/scripts/loop_run.py +20 -30
- package/skills/path/scripts/members.py +704 -0
- package/skills/path/scripts/path_config.py +2 -2
- package/skills/path/scripts/pipeline_git.py +61 -22
- package/skills/path/scripts/pipeline_state.py +176 -54
- 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 +140 -31
- package/skills/path/scripts/state_promote.py +2 -0
- package/skills/path/scripts/status_runtime.py +36 -6
- package/skills/path/scripts/token_budget.py +12 -8
- package/skills/path/scripts/workflow_run.py +103 -29
- package/skills/path/scripts/worktree_paths.py +18 -8
- package/skills/path/templates/codebase.md +1 -1
- package/skills/path/templates/docs-audit.md +1 -1
- package/skills/path/templates/final-review.md +5 -2
- package/skills/path/templates/gap-review.md +2 -1
- package/skills/path/templates/task.md +1 -0
- package/skills/path/templates/wave-review.md +3 -1
|
@@ -12,10 +12,13 @@ sys.dont_write_bytecode = True
|
|
|
12
12
|
from pathlib import Path, PurePosixPath
|
|
13
13
|
from typing import Dict, List, Optional, Sequence, Set, Tuple
|
|
14
14
|
|
|
15
|
-
|
|
16
|
-
import _common
|
|
17
|
-
|
|
18
|
-
|
|
15
|
+
if __package__:
|
|
16
|
+
from . import _common
|
|
17
|
+
else:
|
|
18
|
+
try:
|
|
19
|
+
import _common
|
|
20
|
+
except ImportError: # pragma: no cover - package import used by tests
|
|
21
|
+
from scripts import _common
|
|
19
22
|
|
|
20
23
|
|
|
21
24
|
DEFAULT_TASKS_DIR = ".project/tasks"
|
|
@@ -183,10 +186,47 @@ def _verify_tokens(block: str) -> List[str]:
|
|
|
183
186
|
return tokens
|
|
184
187
|
|
|
185
188
|
|
|
189
|
+
def _member_bases(root: Path):
|
|
190
|
+
"""Resolve a member name to (checkout, origin/main SHA), reading MEMBERS.md once."""
|
|
191
|
+
cache: Dict[str, Optional[Tuple[Path, str]]] = {}
|
|
192
|
+
|
|
193
|
+
def locate(name: str) -> Optional[Tuple[Path, str]]:
|
|
194
|
+
if name not in cache:
|
|
195
|
+
if __package__:
|
|
196
|
+
from . import members
|
|
197
|
+
else:
|
|
198
|
+
try:
|
|
199
|
+
import members
|
|
200
|
+
except ImportError: # pragma: no cover - package import used by tests
|
|
201
|
+
from scripts import members
|
|
202
|
+
try:
|
|
203
|
+
listed = {member["name"]: Path(member["checkout"]) for member in members.read_members(root)}
|
|
204
|
+
checkout = listed.get(name)
|
|
205
|
+
if checkout is None:
|
|
206
|
+
cache[name] = None
|
|
207
|
+
else:
|
|
208
|
+
# A build lock selects the bound tip, including during Plan recovery;
|
|
209
|
+
# without the lock, use origin/main before the first build start.
|
|
210
|
+
lock = root / members.LOCK_PATH
|
|
211
|
+
locked = [entry for entry in (json.loads(lock.read_text(encoding="utf-8"))["members"]
|
|
212
|
+
if lock.is_file() else []) if entry["name"] == name]
|
|
213
|
+
if locked:
|
|
214
|
+
cache[name] = (checkout, _resolve_base(checkout, f"refs/heads/{locked[0]['branch']}"))
|
|
215
|
+
else:
|
|
216
|
+
members.require_origin_main(checkout)
|
|
217
|
+
cache[name] = (checkout, _resolve_base(checkout, "refs/remotes/origin/main"))
|
|
218
|
+
except (members.MembersError, BriefError) as error:
|
|
219
|
+
raise BriefError(f"member {name} is unavailable: {error}") from error
|
|
220
|
+
return cache[name]
|
|
221
|
+
|
|
222
|
+
return locate
|
|
223
|
+
|
|
224
|
+
|
|
186
225
|
def _lint_task(
|
|
187
226
|
repo: Path, base: str, path: Path,
|
|
188
227
|
dependency_files: Optional[Dict[str, Set[str]]] = None,
|
|
189
228
|
landed_bases: Optional[Dict[str, str]] = None,
|
|
229
|
+
member_base=None,
|
|
190
230
|
) -> Tuple[str, List[str], Optional[str], int]:
|
|
191
231
|
text = path.read_text(encoding="utf-8")
|
|
192
232
|
fields, error = _frontmatter(text)
|
|
@@ -198,6 +238,15 @@ def _lint_task(
|
|
|
198
238
|
checked = 0
|
|
199
239
|
if fields is None:
|
|
200
240
|
return task_id, [error or "invalid frontmatter"], None, checked
|
|
241
|
+
# A member task's paths resolve in the member at its origin/main, where the
|
|
242
|
+
# member bound branch starts; `repo:` absent means the coordinator.
|
|
243
|
+
git_root = repo
|
|
244
|
+
member = fields.get("repo")
|
|
245
|
+
if member is not None:
|
|
246
|
+
located = member_base(member) if isinstance(member, str) and member and member_base else None
|
|
247
|
+
if located is None:
|
|
248
|
+
return task_id, [f"repo: names no member in MEMBERS.md: {member}"], None, checked
|
|
249
|
+
git_root, base = located[0], (landed_bases or {}).get(task_id, located[1])
|
|
201
250
|
|
|
202
251
|
for field in REQUIRED_FIELDS:
|
|
203
252
|
if field not in fields:
|
|
@@ -237,7 +286,7 @@ def _lint_task(
|
|
|
237
286
|
for parent in candidate.parents:
|
|
238
287
|
if str(parent) == ".":
|
|
239
288
|
break
|
|
240
|
-
kind = _run_git(
|
|
289
|
+
kind = _run_git(git_root, "cat-file", "-t", f"{base}:{parent}")
|
|
241
290
|
if kind.returncode == 0 and kind.stdout.strip() != "tree":
|
|
242
291
|
problems.append(
|
|
243
292
|
f"files entry {normalized} has non-directory ancestor "
|
|
@@ -255,7 +304,7 @@ def _lint_task(
|
|
|
255
304
|
token not in declared
|
|
256
305
|
and token not in supplied
|
|
257
306
|
and not _supplied_contract(repo, path, token)
|
|
258
|
-
and not _base_exists(
|
|
307
|
+
and not _base_exists(git_root, base, token)
|
|
259
308
|
):
|
|
260
309
|
problems.append(f"## {name} names a path missing at the layer base: {token}")
|
|
261
310
|
|
|
@@ -267,7 +316,7 @@ def _lint_task(
|
|
|
267
316
|
else:
|
|
268
317
|
for token in _verify_tokens(block.group("block")):
|
|
269
318
|
checked += 1
|
|
270
|
-
if token not in declared and token not in supplied and not _base_exists(
|
|
319
|
+
if token not in declared and token not in supplied and not _base_exists(git_root, base, token):
|
|
271
320
|
problems.append(f"## Verify names a path missing at the layer base: {token}")
|
|
272
321
|
|
|
273
322
|
contract = None
|
|
@@ -284,6 +333,7 @@ def validate_task_briefs(
|
|
|
284
333
|
root: Path, base: str, tasks_dir: str = DEFAULT_TASKS_DIR,
|
|
285
334
|
*, dependency_files: Optional[Dict[str, Set[str]]] = None,
|
|
286
335
|
landed_bases: Optional[Dict[str, str]] = None,
|
|
336
|
+
member_base=None,
|
|
287
337
|
) -> Dict[str, object]:
|
|
288
338
|
"""Lint task briefs using the caller's validation bases.
|
|
289
339
|
|
|
@@ -305,9 +355,11 @@ def validate_task_briefs(
|
|
|
305
355
|
task_files = sorted(tasks_path.glob("*.md"))
|
|
306
356
|
if not task_files:
|
|
307
357
|
raise BriefError(f"no task briefs found in {tasks_dir}")
|
|
358
|
+
if member_base is None:
|
|
359
|
+
member_base = _member_bases(root)
|
|
308
360
|
for path in task_files:
|
|
309
361
|
task_id, task_problems, contract, task_checked = _lint_task(
|
|
310
|
-
root, resolved_base, path, dependency_files, landed_bases
|
|
362
|
+
root, resolved_base, path, dependency_files, landed_bases, member_base
|
|
311
363
|
)
|
|
312
364
|
problems.extend(f"{task_id}: {problem}" for problem in task_problems)
|
|
313
365
|
if contract is not None:
|
|
@@ -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
|
|
|
@@ -961,6 +968,26 @@ def is_staged_skill_bundle_artifact(
|
|
|
961
968
|
)
|
|
962
969
|
|
|
963
970
|
|
|
971
|
+
def ignored_junk(directory: Path, name: str, status: os.stat_result) -> bool:
|
|
972
|
+
"""Ignored, untracked OS junk. The caller's no-follow status decides the file type."""
|
|
973
|
+
if (
|
|
974
|
+
name not in _common.OS_JUNK_NAMES
|
|
975
|
+
or is_link_like_status(status)
|
|
976
|
+
or not stat.S_ISREG(status.st_mode)
|
|
977
|
+
):
|
|
978
|
+
return False
|
|
979
|
+
try:
|
|
980
|
+
result = subprocess.run(
|
|
981
|
+
["git", "-C", str(directory), "check-ignore", "-q", "--", name],
|
|
982
|
+
capture_output=True,
|
|
983
|
+
check=False,
|
|
984
|
+
env=git_environment(),
|
|
985
|
+
)
|
|
986
|
+
except OSError:
|
|
987
|
+
return False
|
|
988
|
+
return result.returncode == 0
|
|
989
|
+
|
|
990
|
+
|
|
964
991
|
def project_path_inventory(
|
|
965
992
|
project: Path, root: Path
|
|
966
993
|
) -> tuple[tuple[str, ...], tuple[str, ...]]:
|
|
@@ -1001,14 +1028,18 @@ def project_path_inventory(
|
|
|
1001
1028
|
posix_relative(current / name, root) for name in linked_directories
|
|
1002
1029
|
)
|
|
1003
1030
|
dirnames[:] = [name for name in dirnames if name not in linked_directories]
|
|
1031
|
+
kept = []
|
|
1004
1032
|
for name in filenames:
|
|
1005
1033
|
path = current / name
|
|
1006
|
-
relative = posix_relative(path, root)
|
|
1007
1034
|
status = lstat_evidence(path, missing_ok=False)
|
|
1035
|
+
if ignored_junk(current, name, status):
|
|
1036
|
+
continue
|
|
1037
|
+
kept.append(name)
|
|
1038
|
+
relative = posix_relative(path, root)
|
|
1008
1039
|
paths.append(relative)
|
|
1009
1040
|
if is_link_like(path, status) or not stat.S_ISREG(status.st_mode):
|
|
1010
1041
|
unsafe_paths.append(relative)
|
|
1011
|
-
if not
|
|
1042
|
+
if not kept and not dirnames and current != project:
|
|
1012
1043
|
paths.append(posix_relative(current, root))
|
|
1013
1044
|
if not paths:
|
|
1014
1045
|
try:
|
|
@@ -1017,7 +1048,11 @@ def project_path_inventory(
|
|
|
1017
1048
|
raise DetectError(
|
|
1018
1049
|
f"cannot list project evidence: {project}: {error}"
|
|
1019
1050
|
) from error
|
|
1020
|
-
paths = [
|
|
1051
|
+
paths = [
|
|
1052
|
+
posix_relative(project / name, root)
|
|
1053
|
+
for name in names
|
|
1054
|
+
if not ignored_junk(project, name, lstat_evidence(project / name, missing_ok=False))
|
|
1055
|
+
]
|
|
1021
1056
|
return tuple(sorted(paths)), tuple(sorted(unsafe_paths))
|
|
1022
1057
|
|
|
1023
1058
|
|
|
@@ -1093,7 +1128,7 @@ def classify(repo: Path) -> dict:
|
|
|
1093
1128
|
missing_ok=True,
|
|
1094
1129
|
)
|
|
1095
1130
|
if (
|
|
1096
|
-
|
|
1131
|
+
STATE_CREATE_SUPPORTED
|
|
1097
1132
|
and orphan_paths == (f".project/{STATE_TEMP_NAME}",)
|
|
1098
1133
|
and not unsafe_paths
|
|
1099
1134
|
and temporary_state_status is not None
|
|
@@ -1265,12 +1300,28 @@ def close_file_descriptors(
|
|
|
1265
1300
|
pass
|
|
1266
1301
|
|
|
1267
1302
|
|
|
1303
|
+
def project_entries(project_fd: int, project: Path) -> set:
|
|
1304
|
+
"""Names in .project without ignored OS junk (Finder may add .DS_Store at any time)."""
|
|
1305
|
+
entries = set(os.listdir(project_fd))
|
|
1306
|
+
for name in entries & _common.OS_JUNK_NAMES:
|
|
1307
|
+
try:
|
|
1308
|
+
status = os.stat(name, dir_fd=project_fd, follow_symlinks=False)
|
|
1309
|
+
except FileNotFoundError:
|
|
1310
|
+
continue # Keep the name: the caller's comparison then fails closed.
|
|
1311
|
+
if ignored_junk(project, name, status):
|
|
1312
|
+
entries.discard(name)
|
|
1313
|
+
return entries
|
|
1314
|
+
|
|
1315
|
+
|
|
1268
1316
|
def write_state_anchored(
|
|
1269
1317
|
root: Path,
|
|
1270
1318
|
content: str,
|
|
1271
1319
|
expected_root: os.stat_result,
|
|
1272
1320
|
expected_project: Optional[os.stat_result],
|
|
1273
1321
|
) -> None:
|
|
1322
|
+
if WINDOWS_STATE_CREATE_SUPPORTED:
|
|
1323
|
+
write_state_windows(root, content, expected_root, expected_project)
|
|
1324
|
+
return
|
|
1274
1325
|
if not ANCHORED_STATE_CREATE_SUPPORTED:
|
|
1275
1326
|
raise DetectError("anchored no-follow STATE.md creation is unavailable")
|
|
1276
1327
|
payload = content.encode("utf-8")
|
|
@@ -1333,7 +1384,7 @@ def write_state_anchored(
|
|
|
1333
1384
|
):
|
|
1334
1385
|
raise DetectError(".project changed after classification")
|
|
1335
1386
|
lock_exclusive(project_fd)
|
|
1336
|
-
entries =
|
|
1387
|
+
entries = project_entries(project_fd, root / ".project")
|
|
1337
1388
|
if entries not in (set(), {STATE_TEMP_NAME}):
|
|
1338
1389
|
raise DetectError(".project changed after classification")
|
|
1339
1390
|
create = (
|
|
@@ -1400,7 +1451,7 @@ def write_state_anchored(
|
|
|
1400
1451
|
or not os.path.samestat(created_status, current_state)
|
|
1401
1452
|
):
|
|
1402
1453
|
raise DetectError("temporary STATE.md changed while writing")
|
|
1403
|
-
if
|
|
1454
|
+
if project_entries(project_fd, root / ".project") != {STATE_TEMP_NAME}:
|
|
1404
1455
|
raise DetectError(".project contents changed while creating STATE.md")
|
|
1405
1456
|
try:
|
|
1406
1457
|
os.link(
|
|
@@ -1420,7 +1471,7 @@ def write_state_anchored(
|
|
|
1420
1471
|
published = os.stat("STATE.md", dir_fd=project_fd, follow_symlinks=False)
|
|
1421
1472
|
if published.st_nlink != 1 or not os.path.samestat(created_status, published):
|
|
1422
1473
|
raise DetectError("STATE.md changed while publishing")
|
|
1423
|
-
if
|
|
1474
|
+
if project_entries(project_fd, root / ".project") != {"STATE.md"}:
|
|
1424
1475
|
raise DetectError(".project contents changed while publishing STATE.md")
|
|
1425
1476
|
os.fsync(project_fd)
|
|
1426
1477
|
closing_state_fd = state_fd
|
|
@@ -1448,6 +1499,285 @@ def write_state_anchored(
|
|
|
1448
1499
|
close_file_descriptors(state_fd, project_fd, root_fd)
|
|
1449
1500
|
|
|
1450
1501
|
|
|
1502
|
+
# --- Windows anchored creation ------------------------------------------------
|
|
1503
|
+
# Windows has no dir_fd, O_NOFOLLOW, or O_DIRECTORY. Handles give the same
|
|
1504
|
+
# guarantee (docs/adr/0004-windows-anchored-state.md):
|
|
1505
|
+
# - each directory is opened without following a reparse point and held
|
|
1506
|
+
# without FILE_SHARE_DELETE, so it cannot be renamed or replaced while held;
|
|
1507
|
+
# - children are opened relative to the held parent handle (NtCreateFile with
|
|
1508
|
+
# a RootDirectory), so no path is resolved again after it was checked;
|
|
1509
|
+
# - STATE.md is published by renaming the temporary file's own handle without
|
|
1510
|
+
# replacing an existing name, and rollback deletes through that handle.
|
|
1511
|
+
|
|
1512
|
+
_FILE_LIST_DIRECTORY = 0x0001
|
|
1513
|
+
_FILE_TRAVERSE = 0x0020
|
|
1514
|
+
_FILE_READ_ATTRIBUTES = 0x0080
|
|
1515
|
+
_DELETE = 0x00010000
|
|
1516
|
+
_SYNCHRONIZE = 0x00100000
|
|
1517
|
+
_GENERIC_WRITE = 0x40000000
|
|
1518
|
+
_GENERIC_READ = 0x80000000
|
|
1519
|
+
_FILE_SHARE_READ = 0x1
|
|
1520
|
+
_FILE_SHARE_WRITE = 0x2
|
|
1521
|
+
_OPEN_EXISTING = 3
|
|
1522
|
+
_FILE_FLAG_OPEN_REPARSE_POINT = 0x00200000
|
|
1523
|
+
_FILE_FLAG_BACKUP_SEMANTICS = 0x02000000
|
|
1524
|
+
_FILE_OPEN = 1
|
|
1525
|
+
_FILE_CREATE = 2
|
|
1526
|
+
_FILE_OPEN_IF = 3
|
|
1527
|
+
_FILE_DIRECTORY_FILE = 0x00000001
|
|
1528
|
+
_FILE_SYNCHRONOUS_IO_NONALERT = 0x00000020
|
|
1529
|
+
_FILE_NON_DIRECTORY_FILE = 0x00000040
|
|
1530
|
+
_FILE_OPEN_REPARSE_POINT = 0x00200000
|
|
1531
|
+
_OBJ_CASE_INSENSITIVE = 0x00000040
|
|
1532
|
+
_FILE_RENAME_INFORMATION = 10
|
|
1533
|
+
_FILE_DISPOSITION_INFO = 4
|
|
1534
|
+
_DIRECTORY_ACCESS = _FILE_LIST_DIRECTORY | _FILE_TRAVERSE | _FILE_READ_ATTRIBUTES | _SYNCHRONIZE
|
|
1535
|
+
# Readers and writers may share a held directory; nobody may delete or rename it.
|
|
1536
|
+
_PIN_SHARE = _FILE_SHARE_READ | _FILE_SHARE_WRITE
|
|
1537
|
+
|
|
1538
|
+
WINDOWS_STATE_CREATE_SUPPORTED = os.name == "nt"
|
|
1539
|
+
STATE_CREATE_SUPPORTED = ANCHORED_STATE_CREATE_SUPPORTED or WINDOWS_STATE_CREATE_SUPPORTED
|
|
1540
|
+
|
|
1541
|
+
|
|
1542
|
+
class _WindowsHandles:
|
|
1543
|
+
"""kernel32/ntdll calls for the Windows anchored create, bound on first use."""
|
|
1544
|
+
|
|
1545
|
+
def __init__(self) -> None:
|
|
1546
|
+
import ctypes
|
|
1547
|
+
import msvcrt
|
|
1548
|
+
from ctypes import wintypes
|
|
1549
|
+
|
|
1550
|
+
class UnicodeString(ctypes.Structure):
|
|
1551
|
+
_fields_ = [("Length", wintypes.USHORT), ("MaximumLength", wintypes.USHORT),
|
|
1552
|
+
("Buffer", wintypes.LPWSTR)]
|
|
1553
|
+
|
|
1554
|
+
class ObjectAttributes(ctypes.Structure):
|
|
1555
|
+
_fields_ = [("Length", wintypes.ULONG), ("RootDirectory", wintypes.HANDLE),
|
|
1556
|
+
("ObjectName", ctypes.POINTER(UnicodeString)),
|
|
1557
|
+
("Attributes", wintypes.ULONG), ("SecurityDescriptor", ctypes.c_void_p),
|
|
1558
|
+
("SecurityQualityOfService", ctypes.c_void_p)]
|
|
1559
|
+
|
|
1560
|
+
class IoStatusBlock(ctypes.Structure):
|
|
1561
|
+
_fields_ = [("Status", ctypes.c_void_p), ("Information", ctypes.c_size_t)]
|
|
1562
|
+
|
|
1563
|
+
self.ctypes, self.wintypes, self.msvcrt = ctypes, wintypes, msvcrt
|
|
1564
|
+
self.UnicodeString, self.ObjectAttributes, self.IoStatusBlock = (
|
|
1565
|
+
UnicodeString, ObjectAttributes, IoStatusBlock)
|
|
1566
|
+
self.kernel32 = ctypes.WinDLL("kernel32", use_last_error=True)
|
|
1567
|
+
self.ntdll = ctypes.WinDLL("ntdll")
|
|
1568
|
+
self.kernel32.CreateFileW.restype = wintypes.HANDLE
|
|
1569
|
+
self.kernel32.CreateFileW.argtypes = (
|
|
1570
|
+
wintypes.LPCWSTR, wintypes.DWORD, wintypes.DWORD, ctypes.c_void_p,
|
|
1571
|
+
wintypes.DWORD, wintypes.DWORD, wintypes.HANDLE)
|
|
1572
|
+
self.kernel32.SetFileInformationByHandle.argtypes = (
|
|
1573
|
+
wintypes.HANDLE, ctypes.c_int, ctypes.c_void_p, wintypes.DWORD)
|
|
1574
|
+
self.ntdll.NtCreateFile.restype = ctypes.c_long
|
|
1575
|
+
self.ntdll.NtCreateFile.argtypes = (
|
|
1576
|
+
ctypes.POINTER(wintypes.HANDLE), wintypes.DWORD, ctypes.POINTER(ObjectAttributes),
|
|
1577
|
+
ctypes.POINTER(IoStatusBlock), ctypes.c_void_p, wintypes.ULONG, wintypes.ULONG,
|
|
1578
|
+
wintypes.ULONG, wintypes.ULONG, ctypes.c_void_p, wintypes.ULONG)
|
|
1579
|
+
self.ntdll.RtlNtStatusToDosError.restype = wintypes.ULONG
|
|
1580
|
+
self.ntdll.RtlNtStatusToDosError.argtypes = (ctypes.c_long,)
|
|
1581
|
+
self.ntdll.NtSetInformationFile.restype = ctypes.c_long
|
|
1582
|
+
self.ntdll.NtSetInformationFile.argtypes = (
|
|
1583
|
+
wintypes.HANDLE, ctypes.POINTER(IoStatusBlock), ctypes.c_void_p, wintypes.ULONG,
|
|
1584
|
+
ctypes.c_int)
|
|
1585
|
+
|
|
1586
|
+
def adopt(self, handle: int) -> int:
|
|
1587
|
+
"""A CRT descriptor that owns handle, so os.fstat, os.write, and os.close apply."""
|
|
1588
|
+
try:
|
|
1589
|
+
descriptor = self.msvcrt.open_osfhandle(handle, 0)
|
|
1590
|
+
except OSError:
|
|
1591
|
+
self.kernel32.CloseHandle(self.wintypes.HANDLE(handle))
|
|
1592
|
+
raise
|
|
1593
|
+
# The CRT adopts handles in text mode, where os.write turns "\n" into "\r\n".
|
|
1594
|
+
self.msvcrt.setmode(descriptor, os.O_BINARY)
|
|
1595
|
+
return descriptor
|
|
1596
|
+
|
|
1597
|
+
def open_root(self, path: Path) -> int:
|
|
1598
|
+
handle = self.kernel32.CreateFileW(
|
|
1599
|
+
str(path), _DIRECTORY_ACCESS, _PIN_SHARE, None, _OPEN_EXISTING,
|
|
1600
|
+
_FILE_FLAG_BACKUP_SEMANTICS | _FILE_FLAG_OPEN_REPARSE_POINT, None)
|
|
1601
|
+
if handle is None or handle == self.wintypes.HANDLE(-1).value:
|
|
1602
|
+
raise self.ctypes.WinError(self.ctypes.get_last_error())
|
|
1603
|
+
return self.adopt(handle)
|
|
1604
|
+
|
|
1605
|
+
def open_relative(
|
|
1606
|
+
self, parent_fd: int, name: str, access: int, share: int, disposition: int, options: int
|
|
1607
|
+
) -> int:
|
|
1608
|
+
"""Open name inside the directory held by parent_fd, never following a reparse point."""
|
|
1609
|
+
ctypes = self.ctypes
|
|
1610
|
+
buffer = ctypes.create_unicode_buffer(name)
|
|
1611
|
+
length = len(name) * ctypes.sizeof(ctypes.c_wchar)
|
|
1612
|
+
object_name = self.UnicodeString(length, length + ctypes.sizeof(ctypes.c_wchar),
|
|
1613
|
+
ctypes.cast(buffer, self.wintypes.LPWSTR))
|
|
1614
|
+
attributes = self.ObjectAttributes(
|
|
1615
|
+
ctypes.sizeof(self.ObjectAttributes), self.msvcrt.get_osfhandle(parent_fd),
|
|
1616
|
+
ctypes.pointer(object_name), _OBJ_CASE_INSENSITIVE, None, None)
|
|
1617
|
+
handle = self.wintypes.HANDLE()
|
|
1618
|
+
status = self.ntdll.NtCreateFile(
|
|
1619
|
+
ctypes.byref(handle), access | _SYNCHRONIZE, ctypes.byref(attributes),
|
|
1620
|
+
ctypes.byref(self.IoStatusBlock()), None, 0, share, disposition,
|
|
1621
|
+
options | _FILE_SYNCHRONOUS_IO_NONALERT | _FILE_OPEN_REPARSE_POINT, None, 0)
|
|
1622
|
+
if status < 0:
|
|
1623
|
+
raise ctypes.WinError(self.ntdll.RtlNtStatusToDosError(status))
|
|
1624
|
+
return self.adopt(handle.value)
|
|
1625
|
+
|
|
1626
|
+
def rename_no_replace(self, fd: int, directory_fd: int, name: str) -> None:
|
|
1627
|
+
"""Rename the file held by fd to name inside the directory held by directory_fd.
|
|
1628
|
+
|
|
1629
|
+
The renameat equivalent; fails when name already exists.
|
|
1630
|
+
"""
|
|
1631
|
+
ctypes, wintypes = self.ctypes, self.wintypes
|
|
1632
|
+
|
|
1633
|
+
class RenameInformation(ctypes.Structure):
|
|
1634
|
+
_fields_ = [("ReplaceIfExists", wintypes.BOOLEAN), ("RootDirectory", wintypes.HANDLE),
|
|
1635
|
+
("FileNameLength", wintypes.ULONG),
|
|
1636
|
+
("FileName", wintypes.WCHAR * (len(name) + 1))]
|
|
1637
|
+
|
|
1638
|
+
info = RenameInformation(False, self.msvcrt.get_osfhandle(directory_fd),
|
|
1639
|
+
len(name) * ctypes.sizeof(ctypes.c_wchar), name)
|
|
1640
|
+
status = self.ntdll.NtSetInformationFile(
|
|
1641
|
+
self.msvcrt.get_osfhandle(fd), ctypes.byref(self.IoStatusBlock()),
|
|
1642
|
+
ctypes.byref(info), ctypes.sizeof(info), _FILE_RENAME_INFORMATION)
|
|
1643
|
+
if status < 0:
|
|
1644
|
+
raise ctypes.WinError(self.ntdll.RtlNtStatusToDosError(status))
|
|
1645
|
+
|
|
1646
|
+
def delete_on_close(self, fd: int) -> None:
|
|
1647
|
+
"""Delete exactly the file held by fd when it closes, whatever its name is now."""
|
|
1648
|
+
ctypes = self.ctypes
|
|
1649
|
+
flag = ctypes.c_ubyte(1)
|
|
1650
|
+
if not self.kernel32.SetFileInformationByHandle(
|
|
1651
|
+
self.msvcrt.get_osfhandle(fd), _FILE_DISPOSITION_INFO,
|
|
1652
|
+
ctypes.byref(flag), ctypes.sizeof(flag)):
|
|
1653
|
+
raise ctypes.WinError(ctypes.get_last_error())
|
|
1654
|
+
|
|
1655
|
+
|
|
1656
|
+
def windows_project_entries(project: Path) -> set:
|
|
1657
|
+
"""project_entries for a .project held open without delete sharing."""
|
|
1658
|
+
entries = set(os.listdir(project))
|
|
1659
|
+
for name in entries & _common.OS_JUNK_NAMES:
|
|
1660
|
+
try:
|
|
1661
|
+
status = os.stat(project / name, follow_symlinks=False)
|
|
1662
|
+
except FileNotFoundError:
|
|
1663
|
+
continue
|
|
1664
|
+
if ignored_junk(project, name, status):
|
|
1665
|
+
entries.discard(name)
|
|
1666
|
+
return entries
|
|
1667
|
+
|
|
1668
|
+
|
|
1669
|
+
def write_state_windows(
|
|
1670
|
+
root: Path,
|
|
1671
|
+
content: str,
|
|
1672
|
+
expected_root: os.stat_result,
|
|
1673
|
+
expected_project: Optional[os.stat_result],
|
|
1674
|
+
) -> None:
|
|
1675
|
+
win = _WindowsHandles()
|
|
1676
|
+
payload = content.encode("utf-8")
|
|
1677
|
+
project = root / ".project"
|
|
1678
|
+
root_fd = project_fd = state_fd = None
|
|
1679
|
+
delete_on_failure = False
|
|
1680
|
+
# The same lock pipeline_state takes for this track.
|
|
1681
|
+
with _common.directory_mutex(project):
|
|
1682
|
+
try:
|
|
1683
|
+
try:
|
|
1684
|
+
root_fd = win.open_root(root)
|
|
1685
|
+
except OSError as error:
|
|
1686
|
+
raise DetectError(f"cannot open repo for STATE.md creation: {error}") from error
|
|
1687
|
+
opened_root = os.fstat(root_fd)
|
|
1688
|
+
if (
|
|
1689
|
+
is_link_like(root, opened_root)
|
|
1690
|
+
or not stat.S_ISDIR(opened_root.st_mode)
|
|
1691
|
+
or not os.path.samestat(expected_root, opened_root)
|
|
1692
|
+
):
|
|
1693
|
+
raise DetectError("repo changed after classification")
|
|
1694
|
+
try:
|
|
1695
|
+
project_fd = win.open_relative(
|
|
1696
|
+
root_fd, ".project", _DIRECTORY_ACCESS, _PIN_SHARE,
|
|
1697
|
+
_FILE_CREATE if expected_project is None else _FILE_OPEN,
|
|
1698
|
+
_FILE_DIRECTORY_FILE)
|
|
1699
|
+
except FileExistsError as error:
|
|
1700
|
+
raise DetectError(".project changed after classification") from error
|
|
1701
|
+
except OSError as error:
|
|
1702
|
+
raise DetectError(f"cannot open .project for STATE.md creation: {error}") from error
|
|
1703
|
+
opened_project = os.fstat(project_fd)
|
|
1704
|
+
if is_link_like(project, opened_project) or not stat.S_ISDIR(opened_project.st_mode):
|
|
1705
|
+
raise DetectError("cannot create state through a non-directory .project")
|
|
1706
|
+
if expected_project is not None and not os.path.samestat(expected_project, opened_project):
|
|
1707
|
+
raise DetectError(".project changed after classification")
|
|
1708
|
+
if windows_project_entries(project) not in (set(), {STATE_TEMP_NAME}):
|
|
1709
|
+
raise DetectError(".project changed after classification")
|
|
1710
|
+
try:
|
|
1711
|
+
# Share mode 0: nothing else may open the temporary file while it is held.
|
|
1712
|
+
state_fd = win.open_relative(
|
|
1713
|
+
project_fd, STATE_TEMP_NAME, _GENERIC_READ | _GENERIC_WRITE | _DELETE,
|
|
1714
|
+
0, _FILE_OPEN_IF, _FILE_NON_DIRECTORY_FILE)
|
|
1715
|
+
except OSError as error:
|
|
1716
|
+
raise DetectError(f"cannot reserve temporary STATE.md: {error}") from error
|
|
1717
|
+
created_status = os.fstat(state_fd)
|
|
1718
|
+
if is_link_like(project / STATE_TEMP_NAME, created_status) or not stat.S_ISREG(
|
|
1719
|
+
created_status.st_mode
|
|
1720
|
+
):
|
|
1721
|
+
raise DetectError("temporary STATE.md is not a regular file")
|
|
1722
|
+
if created_status.st_nlink != 1:
|
|
1723
|
+
raise DetectError("temporary STATE.md has an unexpected link count")
|
|
1724
|
+
delete_on_failure = True
|
|
1725
|
+
if "STATE.md" in os.listdir(project):
|
|
1726
|
+
raise DetectError("STATE.md already exists")
|
|
1727
|
+
os.ftruncate(state_fd, 0)
|
|
1728
|
+
write_all(state_fd, payload)
|
|
1729
|
+
os.fsync(state_fd)
|
|
1730
|
+
current_root = lstat_evidence(root, missing_ok=False)
|
|
1731
|
+
current_project = lstat_evidence(project, missing_ok=False)
|
|
1732
|
+
if (
|
|
1733
|
+
is_link_like(root, current_root)
|
|
1734
|
+
or not os.path.samestat(opened_root, current_root)
|
|
1735
|
+
or is_link_like(project, current_project)
|
|
1736
|
+
or not os.path.samestat(opened_project, current_project)
|
|
1737
|
+
):
|
|
1738
|
+
raise DetectError("project identity changed while creating STATE.md")
|
|
1739
|
+
current_state = os.fstat(state_fd)
|
|
1740
|
+
if current_state.st_nlink != 1 or not os.path.samestat(created_status, current_state):
|
|
1741
|
+
raise DetectError("temporary STATE.md changed while writing")
|
|
1742
|
+
if windows_project_entries(project) != {STATE_TEMP_NAME}:
|
|
1743
|
+
raise DetectError(".project contents changed while creating STATE.md")
|
|
1744
|
+
try:
|
|
1745
|
+
win.rename_no_replace(state_fd, project_fd, "STATE.md")
|
|
1746
|
+
except OSError as error:
|
|
1747
|
+
if "STATE.md" in os.listdir(project):
|
|
1748
|
+
raise DetectError("STATE.md already exists") from error
|
|
1749
|
+
raise DetectError(f"cannot publish STATE.md: {error}") from error
|
|
1750
|
+
published = lstat_evidence(project / "STATE.md", missing_ok=False)
|
|
1751
|
+
if (
|
|
1752
|
+
is_link_like(project / "STATE.md", published)
|
|
1753
|
+
or published.st_nlink != 1
|
|
1754
|
+
or not os.path.samestat(created_status, published)
|
|
1755
|
+
):
|
|
1756
|
+
raise DetectError("STATE.md changed while publishing")
|
|
1757
|
+
if windows_project_entries(project) != {"STATE.md"}:
|
|
1758
|
+
raise DetectError(".project contents changed while publishing STATE.md")
|
|
1759
|
+
delete_on_failure = False
|
|
1760
|
+
closing_state_fd, state_fd = state_fd, None
|
|
1761
|
+
try:
|
|
1762
|
+
os.close(closing_state_fd)
|
|
1763
|
+
except OSError as error:
|
|
1764
|
+
raise DetectError(f"cannot close STATE.md: {error}") from error
|
|
1765
|
+
except BaseException as error:
|
|
1766
|
+
if delete_on_failure and state_fd is not None:
|
|
1767
|
+
try:
|
|
1768
|
+
win.delete_on_close(state_fd)
|
|
1769
|
+
except OSError as rollback_error:
|
|
1770
|
+
raise DetectError(
|
|
1771
|
+
f"cannot roll back STATE.md: {rollback_error}") from error
|
|
1772
|
+
if isinstance(error, DetectError):
|
|
1773
|
+
raise
|
|
1774
|
+
if isinstance(error, OSError):
|
|
1775
|
+
raise DetectError(f"cannot create STATE.md: {error}") from error
|
|
1776
|
+
raise
|
|
1777
|
+
finally:
|
|
1778
|
+
close_file_descriptors(state_fd, project_fd, root_fd)
|
|
1779
|
+
|
|
1780
|
+
|
|
1451
1781
|
def initialize(
|
|
1452
1782
|
repo: Path, template: Path, phase: Optional[str] = None, *, require_git: bool = False
|
|
1453
1783
|
) -> dict:
|
|
@@ -1471,7 +1801,7 @@ def initialize(
|
|
|
1471
1801
|
if require_git:
|
|
1472
1802
|
git = subprocess.run(
|
|
1473
1803
|
["git", "-C", str(root), "rev-parse", "--is-inside-work-tree"],
|
|
1474
|
-
env=git_environment(), capture_output=True,
|
|
1804
|
+
env=git_environment(), capture_output=True, encoding="utf-8", errors="replace", check=False,
|
|
1475
1805
|
)
|
|
1476
1806
|
if git.returncode != 0 or git.stdout.strip() != "true":
|
|
1477
1807
|
return {**payload, "route": "setup-repository", "wrote_state": False}
|
|
@@ -1482,7 +1812,7 @@ def initialize(
|
|
|
1482
1812
|
raise DetectError(
|
|
1483
1813
|
f"initialize phase {phase} does not match verdict {payload['verdict']}"
|
|
1484
1814
|
)
|
|
1485
|
-
if not
|
|
1815
|
+
if not STATE_CREATE_SUPPORTED:
|
|
1486
1816
|
payload["wrote_state"] = False
|
|
1487
1817
|
payload["error"] = "anchored no-follow STATE.md creation is unavailable"
|
|
1488
1818
|
return payload
|
|
@@ -16,14 +16,20 @@ from pathlib import Path
|
|
|
16
16
|
from typing import Iterator, Optional, Sequence
|
|
17
17
|
|
|
18
18
|
def _load_pipeline_modules():
|
|
19
|
-
|
|
20
|
-
import archive_milestone
|
|
21
|
-
import pipeline_state
|
|
22
|
-
import _common
|
|
19
|
+
if __package__:
|
|
20
|
+
from . import archive_milestone
|
|
21
|
+
from . import pipeline_state
|
|
22
|
+
from . import _common
|
|
23
23
|
return _common, archive_milestone, pipeline_state
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
24
|
+
else:
|
|
25
|
+
try:
|
|
26
|
+
import archive_milestone
|
|
27
|
+
import pipeline_state
|
|
28
|
+
import _common
|
|
29
|
+
return _common, archive_milestone, pipeline_state
|
|
30
|
+
except ModuleNotFoundError as error:
|
|
31
|
+
if error.name not in {"archive_milestone", "pipeline_state", "_common"}:
|
|
32
|
+
raise
|
|
27
33
|
try:
|
|
28
34
|
from scripts import archive_milestone, pipeline_state, _common
|
|
29
35
|
return _common, archive_milestone, pipeline_state
|
|
@@ -111,7 +117,7 @@ def record_count(directory: Path) -> int:
|
|
|
111
117
|
def validate_or_empty(directory: Path) -> None:
|
|
112
118
|
if directory.is_symlink() or not directory.is_dir():
|
|
113
119
|
raise DiscussionError("discussion records must be a real directory")
|
|
114
|
-
if sorted(path.name for path in directory.iterdir()) != sorted(FILES):
|
|
120
|
+
if sorted(path.name for path in directory.iterdir() if not _common.is_ignored_junk(path)) != sorted(FILES):
|
|
115
121
|
raise DiscussionError("discussion directory must contain exactly DIALOGUE.md and ANSWERS.md")
|
|
116
122
|
if record_count(directory) == 0:
|
|
117
123
|
if "### D" in (directory / "DIALOGUE.md").read_text(encoding="utf-8"):
|
|
@@ -170,7 +176,7 @@ def validate_contents(project: Path, files: dict) -> None:
|
|
|
170
176
|
staging.mkdir()
|
|
171
177
|
try:
|
|
172
178
|
for name in FILES:
|
|
173
|
-
(staging / name).
|
|
179
|
+
(staging / name).write_bytes(files[name].encode("utf-8"))
|
|
174
180
|
validate_or_empty(staging)
|
|
175
181
|
finally:
|
|
176
182
|
shutil.rmtree(staging)
|
|
@@ -187,11 +193,11 @@ def initialize_records(discussion: Path, dialogue_template: Path, answers_templa
|
|
|
187
193
|
shutil.rmtree(staging)
|
|
188
194
|
staging.mkdir()
|
|
189
195
|
try:
|
|
190
|
-
(staging / "DIALOGUE.md").
|
|
191
|
-
template_header(dialogue_template, "### D001")
|
|
196
|
+
(staging / "DIALOGUE.md").write_bytes(
|
|
197
|
+
template_header(dialogue_template, "### D001").encode("utf-8")
|
|
192
198
|
)
|
|
193
|
-
(staging / "ANSWERS.md").
|
|
194
|
-
template_header(answers_template, "## Answer A001")
|
|
199
|
+
(staging / "ANSWERS.md").write_bytes(
|
|
200
|
+
template_header(answers_template, "## Answer A001").encode("utf-8")
|
|
195
201
|
)
|
|
196
202
|
os.replace(staging, discussion)
|
|
197
203
|
finally:
|