@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,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:
|
|
@@ -14,12 +14,16 @@ from datetime import date
|
|
|
14
14
|
from pathlib import Path, PurePosixPath
|
|
15
15
|
from typing import Optional, Sequence
|
|
16
16
|
|
|
17
|
-
|
|
18
|
-
from pipeline_state import PipelineState
|
|
19
|
-
import _common
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
17
|
+
if __package__:
|
|
18
|
+
from .pipeline_state import PipelineState
|
|
19
|
+
from . import _common
|
|
20
|
+
else:
|
|
21
|
+
try:
|
|
22
|
+
from pipeline_state import PipelineState
|
|
23
|
+
import _common
|
|
24
|
+
except ImportError: # pragma: no cover - package import used by tests
|
|
25
|
+
from scripts.pipeline_state import PipelineState
|
|
26
|
+
from scripts import _common
|
|
23
27
|
|
|
24
28
|
|
|
25
29
|
atomic_replace = _common.atomic_replace
|
|
@@ -105,7 +109,7 @@ def validate_discussion_directory(
|
|
|
105
109
|
) -> None:
|
|
106
110
|
if directory.is_symlink() or not directory.is_dir():
|
|
107
111
|
raise ArchiveError("discussion archive must be a real directory")
|
|
108
|
-
entries = sorted(path.name for path in directory.iterdir())
|
|
112
|
+
entries = sorted(path.name for path in directory.iterdir() if not _common.is_ignored_junk(path))
|
|
109
113
|
if entries != sorted(DISCUSSION_FILES):
|
|
110
114
|
raise ArchiveError("discussion archive must contain exactly DIALOGUE.md and ANSWERS.md")
|
|
111
115
|
dialogue, answers = (directory / name for name in DISCUSSION_FILES)
|
|
@@ -490,10 +494,13 @@ def archived_intent_criteria(archive: Path) -> dict[int, str]:
|
|
|
490
494
|
if not archive_milestone.is_real_file(intent):
|
|
491
495
|
raise ArchiveError("archived intent must be a real INTENT.md file")
|
|
492
496
|
text = HTML_COMMENT_PATTERN.sub("", intent.read_text(encoding="utf-8"))
|
|
493
|
-
|
|
494
|
-
import check_handoffs
|
|
495
|
-
|
|
496
|
-
|
|
497
|
+
if __package__:
|
|
498
|
+
from . import check_handoffs
|
|
499
|
+
else:
|
|
500
|
+
try:
|
|
501
|
+
import check_handoffs
|
|
502
|
+
except ImportError:
|
|
503
|
+
from scripts import check_handoffs
|
|
497
504
|
try:
|
|
498
505
|
criteria = check_handoffs._success_criteria(text)
|
|
499
506
|
except check_handoffs.HandoffError as error:
|
|
@@ -502,10 +509,13 @@ def archived_intent_criteria(archive: Path) -> dict[int, str]:
|
|
|
502
509
|
|
|
503
510
|
|
|
504
511
|
def parse_final_review(archive: Path) -> tuple:
|
|
505
|
-
|
|
506
|
-
import check_handoffs
|
|
507
|
-
|
|
508
|
-
|
|
512
|
+
if __package__:
|
|
513
|
+
from . import check_handoffs
|
|
514
|
+
else:
|
|
515
|
+
try:
|
|
516
|
+
import check_handoffs
|
|
517
|
+
except ImportError: # pragma: no cover - package import used by tests
|
|
518
|
+
from scripts import check_handoffs
|
|
509
519
|
|
|
510
520
|
final = archive / "review" / "FINAL.md"
|
|
511
521
|
if not archive_milestone.is_real_file(final):
|
|
@@ -597,8 +607,29 @@ def parse_final_review(archive: Path) -> tuple:
|
|
|
597
607
|
return reviewed_head.lower(), criteria
|
|
598
608
|
|
|
599
609
|
|
|
610
|
+
def _member_heads(text: str, source: str) -> dict:
|
|
611
|
+
if __package__:
|
|
612
|
+
from . import check_handoffs
|
|
613
|
+
else:
|
|
614
|
+
try:
|
|
615
|
+
import check_handoffs
|
|
616
|
+
except ImportError: # pragma: no cover - package import used by tests
|
|
617
|
+
from scripts import check_handoffs
|
|
618
|
+
try:
|
|
619
|
+
return check_handoffs.member_reviewed_heads(text, source)
|
|
620
|
+
except check_handoffs.HandoffError as error:
|
|
621
|
+
raise ArchiveError(str(error)) from error
|
|
622
|
+
|
|
623
|
+
|
|
600
624
|
def validate_gap_reviews(archive: Path, reviewed_head: str) -> None:
|
|
601
625
|
review = archive / "review"
|
|
626
|
+
member_heads = _member_heads((review / "FINAL.md").read_text(encoding="utf-8"), "FINAL.md")
|
|
627
|
+
lock = archive / "build" / "members.json"
|
|
628
|
+
locked = [item["name"] for item in json.loads(lock.read_text(encoding="utf-8"))["members"]] if lock.is_file() else []
|
|
629
|
+
if sorted(member_heads) != sorted(locked):
|
|
630
|
+
raise ArchiveError(
|
|
631
|
+
"FINAL.md Member reviewed HEAD lines must name each locked member: " + (", ".join(locked) or "none")
|
|
632
|
+
)
|
|
602
633
|
candidates = sorted(path for path in review.iterdir() if path.name.startswith("final-gap-"))
|
|
603
634
|
if not candidates:
|
|
604
635
|
raise ArchiveError("final review requires at least one final-gap-N.md artifact")
|
|
@@ -626,6 +657,8 @@ def validate_gap_reviews(archive: Path, reviewed_head: str) -> None:
|
|
|
626
657
|
gap_head = completed_field(lines, "Reviewed HEAD:", path.name).lower()
|
|
627
658
|
if gap_head != reviewed_head:
|
|
628
659
|
raise ArchiveError(f"{path.name} Reviewed HEAD does not match FINAL.md")
|
|
660
|
+
if _member_heads("\n".join(lines), path.name) != member_heads:
|
|
661
|
+
raise ArchiveError(f"{path.name} Member reviewed HEAD lines do not match FINAL.md")
|
|
629
662
|
if completed_field(lines, "Gap verdict:", path.name) != "pass":
|
|
630
663
|
raise ArchiveError(f"{path.name} gap review did not pass")
|
|
631
664
|
risk = completed_field(lines, "Risk:", path.name)
|
|
@@ -655,10 +688,13 @@ def validate_gap_reviews(archive: Path, reviewed_head: str) -> None:
|
|
|
655
688
|
|
|
656
689
|
|
|
657
690
|
def meaningful_review_evidence(lines: Sequence[str], marker: str, task_text: str = "") -> bool:
|
|
658
|
-
|
|
659
|
-
from check_handoffs import task_review_observation
|
|
660
|
-
|
|
661
|
-
|
|
691
|
+
if __package__:
|
|
692
|
+
from .check_handoffs import task_review_observation
|
|
693
|
+
else:
|
|
694
|
+
try:
|
|
695
|
+
from check_handoffs import task_review_observation
|
|
696
|
+
except ImportError: # pragma: no cover - package import used by tests
|
|
697
|
+
from scripts.check_handoffs import task_review_observation
|
|
662
698
|
|
|
663
699
|
prefix = f"- {marker} "
|
|
664
700
|
evidence = []
|
|
@@ -1071,8 +1107,17 @@ def archive_file_inventory(archive: Path) -> Sequence[str]:
|
|
|
1071
1107
|
for path in archive.rglob("*"):
|
|
1072
1108
|
if path.is_symlink():
|
|
1073
1109
|
raise ArchiveError(f"archive contents must not be symlinks: {path}")
|
|
1074
|
-
if path.is_file()
|
|
1075
|
-
|
|
1110
|
+
if not path.is_file() or path.name == "MANIFEST.md":
|
|
1111
|
+
continue
|
|
1112
|
+
relative = path.relative_to(archive)
|
|
1113
|
+
if _common.is_ignored_junk(path):
|
|
1114
|
+
if len(relative.parts) == 1:
|
|
1115
|
+
continue
|
|
1116
|
+
original = archive.parent.parent / relative
|
|
1117
|
+
project = archive.parent.parent.parent
|
|
1118
|
+
if run_git(project, "check-ignore", "-q", "--", original.relative_to(project).as_posix()).returncode == 0:
|
|
1119
|
+
continue
|
|
1120
|
+
contents.append(relative.as_posix())
|
|
1076
1121
|
return sorted(contents)
|
|
1077
1122
|
|
|
1078
1123
|
|