@opengsd/gsd-path 1.3.1 → 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 +281 -0
- package/DOCS.md +13 -7
- 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 +57 -8
- 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 +54 -38
- package/scripts/install.py +562 -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 +35 -9
- 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
|
@@ -0,0 +1,704 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# gsd-path project runtime
|
|
3
|
+
"""Record and validate the member repositories of a multi-repo coordinator.
|
|
4
|
+
|
|
5
|
+
The coordinator's `.project/MEMBERS.md` lists members in ship order
|
|
6
|
+
(docs/adr/0002-multi-repo-coordinator.md). `add` writes that file and an
|
|
7
|
+
untracked marker in the member's shared Git directory. The next approval
|
|
8
|
+
checkpoint commits MEMBERS.md with the other `.project` artifacts.
|
|
9
|
+
`member_role` verifies the marker from any worktree of the member.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import argparse
|
|
15
|
+
import json
|
|
16
|
+
import os
|
|
17
|
+
import re
|
|
18
|
+
import subprocess
|
|
19
|
+
import sys
|
|
20
|
+
|
|
21
|
+
# Runtime helpers must not modify their immutable installation.
|
|
22
|
+
sys.dont_write_bytecode = True
|
|
23
|
+
from pathlib import Path
|
|
24
|
+
from typing import Optional, Sequence
|
|
25
|
+
|
|
26
|
+
if __package__:
|
|
27
|
+
from . import _common, pipeline_state
|
|
28
|
+
else:
|
|
29
|
+
import _common
|
|
30
|
+
import pipeline_state
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
MEMBERS_FILE = "MEMBERS.md"
|
|
34
|
+
MARKER_SCHEMA = "gsd-path/member/v1"
|
|
35
|
+
MARKER_KEYS = {"schema", "coordinator", "project", "name"}
|
|
36
|
+
NAME_RE = re.compile(r"^[a-z0-9][a-z0-9-]*$")
|
|
37
|
+
FIELDS = ("Checkout", "Remote", "Integration")
|
|
38
|
+
INTEGRATIONS = ("default", "direct", "pull-request")
|
|
39
|
+
GITHUB_REMOTE_RE = re.compile(
|
|
40
|
+
r"(?:https://github\.com/|git@github\.com:|ssh://git@github\.com/)([^/\s]+)/([^/\s]+)"
|
|
41
|
+
)
|
|
42
|
+
# Member-side Path refs carry the coordinator name, so they cannot collide with
|
|
43
|
+
# a member's own Path history.
|
|
44
|
+
MEMBER_BRANCH_FORMATS = ("gsd-path/{}-", "gsd-path-task/{}-", "gsd-path-verify/{}-", "gsd-path-integrate/{}-")
|
|
45
|
+
MEMBER_TAG_FORMAT = "refs/tags/milestone/{}-"
|
|
46
|
+
# The coordinator records one authorization per (ref, object) before it
|
|
47
|
+
# publishes a member Path ref; member pre-push accepts only a matching one.
|
|
48
|
+
AUTHORIZATION_PREFIX = "refs/gsd-path/authorizations/"
|
|
49
|
+
LOCK_PATH = ".project/build/members.json"
|
|
50
|
+
LOCK_SCHEMA = "gsd-path/member-lock/v1"
|
|
51
|
+
HEADER = (
|
|
52
|
+
"# Members\n\n"
|
|
53
|
+
"<!-- Written by members.py add. One section per member repository, in\n"
|
|
54
|
+
" ship order. Fixed format; change it only through members.py. -->\n"
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class MembersError(RuntimeError):
|
|
59
|
+
pass
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _git(repo: Path, *arguments: str) -> str:
|
|
63
|
+
result = _common.run_git(repo, *arguments)
|
|
64
|
+
if result.returncode != 0:
|
|
65
|
+
detail = (result.stderr or result.stdout).strip() or "git command failed"
|
|
66
|
+
raise MembersError(f"{repo}: git {' '.join(arguments)}: {detail}")
|
|
67
|
+
return result.stdout.strip()
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _coordinator(repo: Path) -> tuple[Path, pipeline_state.PipelineState]:
|
|
71
|
+
try:
|
|
72
|
+
root = pipeline_state._repo_root(Path(repo))
|
|
73
|
+
state, _, _ = pipeline_state.load_state(root)
|
|
74
|
+
except pipeline_state.PipelineStateError as error:
|
|
75
|
+
raise MembersError(f"coordinator: {error}") from error
|
|
76
|
+
if Path(_git(root, "rev-parse", "--show-toplevel")).resolve() != root:
|
|
77
|
+
raise MembersError(f"coordinator is not a Git root: {root}")
|
|
78
|
+
return root, state
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _remote_identity(remote: str) -> Optional[tuple[str, str]]:
|
|
82
|
+
match = GITHUB_REMOTE_RE.fullmatch(remote)
|
|
83
|
+
if match is None:
|
|
84
|
+
return None
|
|
85
|
+
return match.group(1).lower(), match.group(2).removesuffix(".git").lower()
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _common_dir(checkout: Path) -> Path:
|
|
89
|
+
return Path(_git(checkout, "rev-parse", "--path-format=absolute", "--git-common-dir")).resolve()
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _marker_path(checkout: Path) -> Path:
|
|
93
|
+
directory = _common_dir(checkout) / "gsd-path"
|
|
94
|
+
if directory.is_symlink() or (directory.exists() and not directory.is_dir()):
|
|
95
|
+
raise MembersError(f"member marker directory must be real: {directory}")
|
|
96
|
+
path = directory / "member.json"
|
|
97
|
+
if path.is_symlink() or (path.exists() and not path.is_file()):
|
|
98
|
+
raise MembersError(
|
|
99
|
+
f"member marker is not a regular file: {path}; "
|
|
100
|
+
"remove it by hand, then run members.py repair --repo <coordinator>"
|
|
101
|
+
)
|
|
102
|
+
return path
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _read_marker(path: Path) -> Optional[dict[str, str]]:
|
|
106
|
+
if not path.exists() and not path.is_symlink():
|
|
107
|
+
return None
|
|
108
|
+
try:
|
|
109
|
+
if path.is_symlink() or not path.is_file():
|
|
110
|
+
raise MembersError(
|
|
111
|
+
f"member marker is not a regular file: {path}; "
|
|
112
|
+
"remove it by hand, then run members.py repair --repo <coordinator>"
|
|
113
|
+
)
|
|
114
|
+
data = json.loads(path.read_text(encoding="utf-8"))
|
|
115
|
+
if (not isinstance(data, dict) or set(data) != MARKER_KEYS
|
|
116
|
+
or any(not isinstance(data[key], str) or not data[key].strip() for key in MARKER_KEYS)
|
|
117
|
+
or data["schema"] != MARKER_SCHEMA
|
|
118
|
+
or not Path(data["coordinator"]).is_absolute()):
|
|
119
|
+
raise ValueError("unexpected content")
|
|
120
|
+
except (OSError, ValueError) as error:
|
|
121
|
+
raise MembersError(
|
|
122
|
+
f"member marker is unreadable: {path}: {error}; "
|
|
123
|
+
"run members.py repair --repo <coordinator>"
|
|
124
|
+
) from error
|
|
125
|
+
return data
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def _write_marker(checkout: Path, coordinator: Path, project: str, name: str) -> None:
|
|
129
|
+
path = _marker_path(checkout)
|
|
130
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
131
|
+
marker = {"schema": MARKER_SCHEMA, "coordinator": str(coordinator), "project": project, "name": name}
|
|
132
|
+
_common.atomic_write(path, json.dumps(marker, sort_keys=True) + "\n")
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def _refuse_foreign_marker(checkout: Path, coordinator: Path, project: str, name: str) -> None:
|
|
136
|
+
path = _marker_path(checkout)
|
|
137
|
+
try:
|
|
138
|
+
marker = _read_marker(path)
|
|
139
|
+
except MembersError:
|
|
140
|
+
return
|
|
141
|
+
if marker is not None and (marker["project"], marker["name"]) != (project, name):
|
|
142
|
+
raise MembersError(f"{checkout} is already a member of {marker['project']}")
|
|
143
|
+
if marker is not None:
|
|
144
|
+
try:
|
|
145
|
+
role = member_role(checkout)
|
|
146
|
+
except MembersError:
|
|
147
|
+
return
|
|
148
|
+
if role is not None and role["coordinator"] != coordinator:
|
|
149
|
+
raise MembersError(f"{checkout} is already a member of {role['coordinator']}")
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def marker_coordinator(checkout: Path) -> Optional[Path]:
|
|
153
|
+
"""The coordinator a member marker names, unverified; None without a marker."""
|
|
154
|
+
marker = _read_marker(_marker_path(Path(checkout)))
|
|
155
|
+
return None if marker is None else Path(marker["coordinator"])
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def member_role(checkout: Path) -> Optional[dict[str, object]]:
|
|
159
|
+
"""The verified coordinator of a member checkout, or None outside a member."""
|
|
160
|
+
checkout = Path(checkout)
|
|
161
|
+
marker = _read_marker(_marker_path(checkout))
|
|
162
|
+
if marker is None:
|
|
163
|
+
return None
|
|
164
|
+
stale = MembersError(
|
|
165
|
+
f"member marker for {marker['name']} is stale; "
|
|
166
|
+
"run members.py repair --repo <coordinator>"
|
|
167
|
+
)
|
|
168
|
+
try:
|
|
169
|
+
root, state = _coordinator(Path(marker["coordinator"]))
|
|
170
|
+
listed = read_members(root)
|
|
171
|
+
common = _common_dir(checkout)
|
|
172
|
+
for member in listed:
|
|
173
|
+
if member["name"] == marker["name"] and _common_dir(Path(member["checkout"])) == common:
|
|
174
|
+
if state.project != marker["project"]:
|
|
175
|
+
break
|
|
176
|
+
return {"coordinator": root, "project": state.project, "name": member["name"]}
|
|
177
|
+
except (MembersError, OSError) as error:
|
|
178
|
+
raise stale from error
|
|
179
|
+
raise stale
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def detect_member(checkout: Path) -> dict[str, object]:
|
|
183
|
+
"""Whether a repo is a member: its coordinator, name, and whether its marker is current."""
|
|
184
|
+
checkout = Path(checkout).resolve()
|
|
185
|
+
try:
|
|
186
|
+
named = marker_coordinator(checkout)
|
|
187
|
+
except MembersError as error:
|
|
188
|
+
return {"member": True, "current": False, "checkout": str(checkout), "reason": str(error)}
|
|
189
|
+
if named is None:
|
|
190
|
+
return {"member": False, "checkout": str(checkout)}
|
|
191
|
+
try:
|
|
192
|
+
role = member_role(checkout)
|
|
193
|
+
except MembersError as error:
|
|
194
|
+
return {"member": True, "current": False, "checkout": str(checkout),
|
|
195
|
+
"coordinator": str(named), "reason": str(error)}
|
|
196
|
+
return {"member": True, "current": True, "checkout": str(checkout), "coordinator": str(role["coordinator"]),
|
|
197
|
+
"project": role["project"], "name": role["name"]}
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def read_members(coordinator: Path) -> list[dict[str, str]]:
|
|
201
|
+
"""Parse MEMBERS.md; an absent file means a single-repo project."""
|
|
202
|
+
path = coordinator / ".project" / MEMBERS_FILE
|
|
203
|
+
if not path.exists() and not path.is_symlink():
|
|
204
|
+
return []
|
|
205
|
+
if path.is_symlink() or not path.is_file():
|
|
206
|
+
raise MembersError("MEMBERS.md must be a regular file")
|
|
207
|
+
members: list[dict[str, str]] = []
|
|
208
|
+
current: Optional[dict[str, str]] = None
|
|
209
|
+
preamble: list[str] = []
|
|
210
|
+
for number, line in enumerate(path.read_text(encoding="utf-8").splitlines(), 1):
|
|
211
|
+
if line.startswith("## "):
|
|
212
|
+
name = line[3:].strip()
|
|
213
|
+
if not NAME_RE.fullmatch(name):
|
|
214
|
+
raise MembersError(f"MEMBERS.md line {number}: invalid member name {name!r}")
|
|
215
|
+
current = {"name": name}
|
|
216
|
+
members.append(current)
|
|
217
|
+
continue
|
|
218
|
+
if current is None:
|
|
219
|
+
preamble.append(line)
|
|
220
|
+
continue
|
|
221
|
+
if not line.strip():
|
|
222
|
+
continue
|
|
223
|
+
key, separator, value = line.partition(":")
|
|
224
|
+
if not separator or key not in FIELDS or key.lower() in current:
|
|
225
|
+
raise MembersError(f"MEMBERS.md line {number}: unexpected line {line!r}")
|
|
226
|
+
current[key.lower()] = value.strip()
|
|
227
|
+
if not preamble or preamble[0] != "# Members":
|
|
228
|
+
raise MembersError("MEMBERS.md format error: expected # Members")
|
|
229
|
+
rest = preamble[1:]
|
|
230
|
+
while rest and not rest[0]:
|
|
231
|
+
rest.pop(0)
|
|
232
|
+
comment = HEADER.splitlines()[2:]
|
|
233
|
+
if rest[:len(comment)] == comment:
|
|
234
|
+
rest = rest[len(comment):]
|
|
235
|
+
if any(rest):
|
|
236
|
+
raise MembersError("MEMBERS.md format error: unexpected pre-section content")
|
|
237
|
+
names = [member["name"] for member in members]
|
|
238
|
+
if len(set(names)) != len(names):
|
|
239
|
+
raise MembersError("MEMBERS.md repeats a member name")
|
|
240
|
+
for member in members:
|
|
241
|
+
missing = [field for field in FIELDS if not member.get(field.lower())]
|
|
242
|
+
if missing:
|
|
243
|
+
raise MembersError(f"MEMBERS.md member {member['name']} lacks {', '.join(missing)}")
|
|
244
|
+
if member["integration"] not in INTEGRATIONS:
|
|
245
|
+
raise MembersError(
|
|
246
|
+
f"MEMBERS.md member {member['name']} has invalid Integration: {member['integration']}"
|
|
247
|
+
)
|
|
248
|
+
if not Path(member["checkout"]).is_absolute():
|
|
249
|
+
raise MembersError(f"MEMBERS.md member {member['name']} Checkout must be absolute")
|
|
250
|
+
return members
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def check_member(
|
|
254
|
+
coordinator: Path,
|
|
255
|
+
coordinator_name: str,
|
|
256
|
+
checkout: Path,
|
|
257
|
+
recorded_remote: Optional[str] = None,
|
|
258
|
+
) -> str:
|
|
259
|
+
"""Refuse a checkout that cannot join; return its origin URL."""
|
|
260
|
+
if checkout.is_symlink() or not checkout.is_dir():
|
|
261
|
+
raise MembersError(f"member checkout is not a real directory: {checkout}")
|
|
262
|
+
checkout = checkout.resolve()
|
|
263
|
+
if coordinator in checkout.parents or checkout in coordinator.parents:
|
|
264
|
+
raise MembersError(f"member checkout is nested with the coordinator: {checkout}")
|
|
265
|
+
if Path(_git(checkout, "rev-parse", "--show-toplevel")).resolve() != checkout:
|
|
266
|
+
raise MembersError(f"member checkout must be its Git root: {checkout}")
|
|
267
|
+
if _git(checkout, "rev-parse", "--show-superproject-working-tree"):
|
|
268
|
+
raise MembersError(f"member checkout is a submodule: {checkout}")
|
|
269
|
+
common = Path(_git(checkout, "rev-parse", "--path-format=absolute", "--git-common-dir"))
|
|
270
|
+
coordinator_common = Path(
|
|
271
|
+
_git(coordinator, "rev-parse", "--path-format=absolute", "--git-common-dir")
|
|
272
|
+
)
|
|
273
|
+
if common.resolve() == coordinator_common.resolve():
|
|
274
|
+
raise MembersError("the coordinator cannot be its own member")
|
|
275
|
+
if _git(checkout, "status", "--porcelain", "--untracked-files=all"):
|
|
276
|
+
raise MembersError(f"member has uncommitted changes: {checkout}")
|
|
277
|
+
# The configured URL, not `get-url`, which expands local insteadOf rewrites.
|
|
278
|
+
remote = _git(checkout, "config", "--get", "remote.origin.url")
|
|
279
|
+
if not GITHUB_REMOTE_RE.fullmatch(remote):
|
|
280
|
+
raise MembersError(f"member requires a GitHub.com origin: {remote}")
|
|
281
|
+
if recorded_remote is not None and remote != recorded_remote:
|
|
282
|
+
raise MembersError(f"member origin changed: {recorded_remote} -> {remote}")
|
|
283
|
+
default = _common.run_git(checkout, "symbolic-ref", "--short", "refs/remotes/origin/HEAD")
|
|
284
|
+
if default.returncode != 0 or default.stdout.strip() != "origin/main":
|
|
285
|
+
raise MembersError(f"member remote default must be main: {checkout}")
|
|
286
|
+
require_origin_main(checkout)
|
|
287
|
+
member_state = checkout / ".project" / "STATE.md"
|
|
288
|
+
if member_state.exists() or member_state.is_symlink():
|
|
289
|
+
if member_state.is_symlink() or not member_state.is_file():
|
|
290
|
+
raise MembersError("member STATE.md is unreadable")
|
|
291
|
+
try:
|
|
292
|
+
state, _, _ = pipeline_state.load_state(checkout)
|
|
293
|
+
except pipeline_state.PipelineStateError as error:
|
|
294
|
+
raise MembersError(f"member STATE.md is unreadable: {error}") from error
|
|
295
|
+
if state.milestone is not None and (state.phase, state.status) != ("shipped", "done"):
|
|
296
|
+
raise MembersError(f"member has an active milestone: {state.milestone}")
|
|
297
|
+
prefixes = member_ref_prefixes(coordinator_name) + tuple(
|
|
298
|
+
"refs/remotes/origin/" + branch.format(coordinator_name) for branch in MEMBER_BRANCH_FORMATS
|
|
299
|
+
)
|
|
300
|
+
refs = _git(checkout, "for-each-ref", "--format=%(refname)").splitlines()
|
|
301
|
+
colliding = [ref for ref in refs if ref.startswith(prefixes)]
|
|
302
|
+
if colliding:
|
|
303
|
+
raise MembersError("member refs collide with coordinator names: " + ", ".join(colliding))
|
|
304
|
+
return remote
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
MEMBER_CLOSE_SCHEMA = "gsd-path/member-close/v1"
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
def member_close_path(common: Path, archive_name: str) -> Path:
|
|
311
|
+
"""Ship's member-close journal; outside the archive so the archive inventory is unchanged."""
|
|
312
|
+
return common / "gsd-path" / "member-close" / f"{archive_name}.json"
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
def read_member_close(common: Path, archive_name: str) -> list[dict]:
|
|
316
|
+
path = member_close_path(common, archive_name)
|
|
317
|
+
if not path.is_file():
|
|
318
|
+
return []
|
|
319
|
+
data = json.loads(path.read_text(encoding="utf-8"))
|
|
320
|
+
if not isinstance(data, dict) or data.get("schema") != MEMBER_CLOSE_SCHEMA or not isinstance(data.get("members"), list):
|
|
321
|
+
raise MembersError(f"member-close journal is invalid: {path}")
|
|
322
|
+
return data["members"]
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
def milestone_member_close(common: Path, milestone: int) -> list[dict]:
|
|
326
|
+
"""Member-close rows of milestone M<milestone>, whatever archive name it closed under."""
|
|
327
|
+
rows: list[dict] = []
|
|
328
|
+
for path in sorted((common / "gsd-path" / "member-close").glob(f"{milestone:03d}-*.json")):
|
|
329
|
+
rows.extend(read_member_close(common, path.stem))
|
|
330
|
+
return rows
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
def write_member_close(common: Path, archive_name: str, rows: list[dict]) -> None:
|
|
334
|
+
path = member_close_path(common, archive_name)
|
|
335
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
336
|
+
_common.atomic_write(path, json.dumps({"schema": MEMBER_CLOSE_SCHEMA, "members": rows}, indent=2) + "\n")
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
def require_origin_main(checkout: Path) -> None:
|
|
340
|
+
baseline = _common.run_git(
|
|
341
|
+
checkout, "rev-parse", "--verify", "--quiet", "refs/remotes/origin/main^{commit}"
|
|
342
|
+
)
|
|
343
|
+
if baseline.returncode != 0:
|
|
344
|
+
raise MembersError("member requires refs/remotes/origin/main; run git fetch origin")
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
def member_ref_prefixes(project: str) -> tuple[str, ...]:
|
|
348
|
+
"""Ref prefixes a member repo reserves for the coordinator named `project`."""
|
|
349
|
+
return tuple(
|
|
350
|
+
"refs/heads/" + branch.format(project) for branch in MEMBER_BRANCH_FORMATS
|
|
351
|
+
) + (MEMBER_TAG_FORMAT.format(project),)
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
def _authorization_ref(project: str, kind: str, ref: str) -> str:
|
|
355
|
+
if not NAME_RE.fullmatch(project) or not ref.startswith(("refs/heads/", "refs/tags/")):
|
|
356
|
+
raise MembersError(f"cannot authorize {ref} for {project}")
|
|
357
|
+
return f"{AUTHORIZATION_PREFIX}{project}/{kind}/{ref}"
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
def _authorize(checkout: Path, project: str, kind: str, ref: str, sha: str) -> None:
|
|
361
|
+
authorization = _authorization_ref(project, kind, ref)
|
|
362
|
+
_git(checkout, "update-ref", authorization, sha)
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
def clear_authorization(checkout: Path, project: str, kind: str, ref: str) -> None:
|
|
366
|
+
"""Remove a push or delete authorization; S4 retirement clears a milestone's own."""
|
|
367
|
+
_git(checkout, "update-ref", "-d", _authorization_ref(project, kind, ref))
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
def authorize_push(checkout: Path, project: str, ref: str, sha: str) -> None:
|
|
371
|
+
"""Authorize `ref` at exactly `sha` until cleared (a commit or tag object)."""
|
|
372
|
+
_authorize(checkout, project, "push", ref, sha)
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
def authorize_delete(checkout: Path, project: str, ref: str, expected_remote_sha: str) -> None:
|
|
376
|
+
"""Allow deleting remote `ref` only while it still points at `expected_remote_sha`."""
|
|
377
|
+
_authorize(checkout, project, "delete", ref, expected_remote_sha)
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
def authorized(checkout: Path, project: str, kind: str, ref: str, sha: str) -> bool:
|
|
381
|
+
if not ref.startswith(("refs/heads/", "refs/tags/")):
|
|
382
|
+
return False
|
|
383
|
+
result = _common.run_git(
|
|
384
|
+
checkout, "rev-parse", "--verify", "--quiet", _authorization_ref(project, kind, ref)
|
|
385
|
+
)
|
|
386
|
+
return result.returncode == 0 and result.stdout.strip() == sha
|
|
387
|
+
|
|
388
|
+
|
|
389
|
+
def _task_members(coordinator: Path) -> set[str]:
|
|
390
|
+
try:
|
|
391
|
+
from check_task_briefs import _frontmatter
|
|
392
|
+
except ImportError: # pragma: no cover - package import used by tests
|
|
393
|
+
from scripts.check_task_briefs import _frontmatter
|
|
394
|
+
named = set()
|
|
395
|
+
for task in sorted((coordinator / ".project" / "tasks").glob("*.md")):
|
|
396
|
+
fields, error = _frontmatter(task.read_text(encoding="utf-8"))
|
|
397
|
+
if fields is None:
|
|
398
|
+
raise MembersError(f"{task.name}: {error}")
|
|
399
|
+
if fields.get("repo"):
|
|
400
|
+
named.add(str(fields["repo"]))
|
|
401
|
+
return named
|
|
402
|
+
|
|
403
|
+
|
|
404
|
+
def lock_build_members(coordinator: Path) -> Optional[list[dict[str, str]]]:
|
|
405
|
+
"""At build start, lock the members tasks name (MEMBERS.md order) and create
|
|
406
|
+
each member's bound branch at its origin/main. None when no task names a member."""
|
|
407
|
+
root, state = _coordinator(coordinator)
|
|
408
|
+
named = _task_members(root)
|
|
409
|
+
lock_path = root / LOCK_PATH
|
|
410
|
+
build_dir = lock_path.parent
|
|
411
|
+
if build_dir.is_symlink() or (build_dir.exists() and not build_dir.is_dir()):
|
|
412
|
+
raise MembersError(f"member lock directory must be real: {build_dir}")
|
|
413
|
+
if lock_path.is_symlink() or (lock_path.exists() and not lock_path.is_file()):
|
|
414
|
+
raise MembersError(f"member lock must be a regular file: {lock_path}")
|
|
415
|
+
if not named:
|
|
416
|
+
lock_path.unlink(missing_ok=True)
|
|
417
|
+
return None
|
|
418
|
+
listed = read_members(root)
|
|
419
|
+
unknown = sorted(named - {member["name"] for member in listed})
|
|
420
|
+
if unknown:
|
|
421
|
+
raise MembersError("tasks name members missing from MEMBERS.md: " + ", ".join(unknown))
|
|
422
|
+
ignore_error = _common.project_ignore_error(root)
|
|
423
|
+
if ignore_error:
|
|
424
|
+
raise MembersError(ignore_error)
|
|
425
|
+
bound = _common.BOUND_BRANCH_RE.fullmatch(state.branch or "")
|
|
426
|
+
if bound is None:
|
|
427
|
+
raise MembersError(f"coordinator branch is not a bound branch: {state.branch}")
|
|
428
|
+
branch = f"gsd-path/{state.project}-M{bound.group(1)}"
|
|
429
|
+
previous = {}
|
|
430
|
+
if lock_path.is_file():
|
|
431
|
+
previous = {entry["name"]: entry for entry in json.loads(lock_path.read_text(encoding="utf-8"))["members"]}
|
|
432
|
+
entries = []
|
|
433
|
+
missing = []
|
|
434
|
+
for member in listed:
|
|
435
|
+
if member["name"] not in named:
|
|
436
|
+
continue
|
|
437
|
+
checkout = Path(member["checkout"])
|
|
438
|
+
role = member_role(checkout)
|
|
439
|
+
if role is None or role["coordinator"] != root or role["name"] != member["name"]:
|
|
440
|
+
raise MembersError(f"member marker for {member['name']} is missing or stale; "
|
|
441
|
+
f"run members.py repair --repo {root}")
|
|
442
|
+
remote = _git(checkout, "config", "--get", "remote.origin.url")
|
|
443
|
+
if remote != member["remote"]:
|
|
444
|
+
raise MembersError(f"member origin changed: {member['remote']} -> {remote}")
|
|
445
|
+
require_origin_main(checkout)
|
|
446
|
+
base = _git(checkout, "rev-parse", "refs/remotes/origin/main^{commit}")
|
|
447
|
+
existing = _common.run_git(checkout, "rev-parse", "--verify", "--quiet", f"refs/heads/{branch}")
|
|
448
|
+
if existing.returncode == 0:
|
|
449
|
+
tip = existing.stdout.strip()
|
|
450
|
+
locked = previous.get(member["name"])
|
|
451
|
+
# Recovery re-entry keeps the locked base; otherwise the branch must be unused.
|
|
452
|
+
if locked and locked["branch"] == branch and _common.run_git(
|
|
453
|
+
checkout, "merge-base", "--is-ancestor", locked["base"], tip
|
|
454
|
+
).returncode == 0:
|
|
455
|
+
base = locked["base"]
|
|
456
|
+
elif _common.run_git(checkout, "merge-base", "--is-ancestor", tip, base).returncode == 0:
|
|
457
|
+
base = tip
|
|
458
|
+
else:
|
|
459
|
+
raise MembersError(f"member {member['name']} branch {branch} has commits not on origin/main")
|
|
460
|
+
else:
|
|
461
|
+
missing.append((checkout, base))
|
|
462
|
+
entries.append({"name": member["name"], "branch": branch, "base": base})
|
|
463
|
+
for checkout, base in missing:
|
|
464
|
+
_git(checkout, "update-ref", f"refs/heads/{branch}", base, "")
|
|
465
|
+
lock_path.parent.mkdir(parents=True, exist_ok=True)
|
|
466
|
+
_common.atomic_write(lock_path, json.dumps({"schema": LOCK_SCHEMA, "members": entries}, indent=2) + "\n")
|
|
467
|
+
return entries
|
|
468
|
+
|
|
469
|
+
|
|
470
|
+
def render(members: Sequence[dict[str, str]]) -> str:
|
|
471
|
+
sections = [
|
|
472
|
+
f"## {member['name']}\n"
|
|
473
|
+
f"Checkout: {member['checkout']}\n"
|
|
474
|
+
f"Remote: {member['remote']}\n"
|
|
475
|
+
f"Integration: {member['integration']}\n"
|
|
476
|
+
for member in members
|
|
477
|
+
]
|
|
478
|
+
return "\n".join([HEADER, *sections])
|
|
479
|
+
|
|
480
|
+
|
|
481
|
+
def add_member(repo: Path, name: str, checkout: Path, integration: str) -> list[dict[str, str]]:
|
|
482
|
+
root, state = _coordinator(repo)
|
|
483
|
+
if state.phase in {"build", "ship"}:
|
|
484
|
+
raise MembersError("members change only at a milestone boundary, not during build or ship")
|
|
485
|
+
if not NAME_RE.fullmatch(name):
|
|
486
|
+
raise MembersError(f"invalid member name: {name!r}")
|
|
487
|
+
members = read_members(root)
|
|
488
|
+
resolved = checkout.resolve()
|
|
489
|
+
remote = check_member(root, state.project, checkout)
|
|
490
|
+
common = Path(_git(resolved, "rev-parse", "--path-format=absolute", "--git-common-dir")).resolve()
|
|
491
|
+
identity = _remote_identity(remote)
|
|
492
|
+
for member in members:
|
|
493
|
+
previous = Path(member["checkout"])
|
|
494
|
+
if (member["name"] == name or previous == resolved
|
|
495
|
+
or _remote_identity(member["remote"]) == identity
|
|
496
|
+
or Path(_git(previous, "rev-parse", "--path-format=absolute", "--git-common-dir")).resolve() == common):
|
|
497
|
+
raise MembersError(f"member already recorded: {member['name']}")
|
|
498
|
+
_refuse_foreign_marker(resolved, root, state.project, name)
|
|
499
|
+
members.append(
|
|
500
|
+
{"name": name, "checkout": str(resolved), "remote": remote, "integration": integration}
|
|
501
|
+
)
|
|
502
|
+
_write_marker(resolved, root, state.project, name)
|
|
503
|
+
_common.atomic_write(root / ".project" / MEMBERS_FILE, render(members))
|
|
504
|
+
return members
|
|
505
|
+
|
|
506
|
+
|
|
507
|
+
MEMBER_CREATE_SCHEMA = "gsd-path/member-create/v1"
|
|
508
|
+
VISIBILITIES = ("public", "private", "internal")
|
|
509
|
+
|
|
510
|
+
|
|
511
|
+
def _gh(*arguments: str) -> subprocess.CompletedProcess:
|
|
512
|
+
return subprocess.run(["gh", *arguments], capture_output=True, text=True, check=False,
|
|
513
|
+
env={**os.environ, "GH_HOST": "github.com"})
|
|
514
|
+
|
|
515
|
+
|
|
516
|
+
def create_member(repo: Path, name: str, checkout: Path, integration: str, github: str,
|
|
517
|
+
visibility: str) -> list[dict[str, str]]:
|
|
518
|
+
"""Greenfield member: create the approved GitHub repo, clone it, then join it.
|
|
519
|
+
|
|
520
|
+
A journal in the coordinator Git directory keeps the approved target before any
|
|
521
|
+
external action; a rerun resumes each step and never creates a second repo."""
|
|
522
|
+
root, state = _coordinator(repo)
|
|
523
|
+
if state.phase in {"build", "ship"}:
|
|
524
|
+
raise MembersError("members change only at a milestone boundary, not during build or ship")
|
|
525
|
+
if not NAME_RE.fullmatch(name) or not re.fullmatch(r"[A-Za-z0-9-]+/[A-Za-z0-9._-]+", github):
|
|
526
|
+
raise MembersError(f"invalid member name or GitHub repository: {name!r} {github!r}")
|
|
527
|
+
if visibility not in VISIBILITIES:
|
|
528
|
+
raise MembersError(f"visibility must be one of {', '.join(VISIBILITIES)}")
|
|
529
|
+
if checkout.is_symlink():
|
|
530
|
+
raise MembersError(f"member checkout path is a symlink: {checkout}")
|
|
531
|
+
resolved = checkout.resolve()
|
|
532
|
+
if resolved == root or root in resolved.parents or resolved in root.parents:
|
|
533
|
+
raise MembersError(f"member checkout is nested with the coordinator: {resolved}")
|
|
534
|
+
if not resolved.parent.is_dir():
|
|
535
|
+
raise MembersError(f"member checkout parent does not exist: {resolved.parent}")
|
|
536
|
+
target = {"schema": MEMBER_CREATE_SCHEMA, "name": name, "github": github, "visibility": visibility,
|
|
537
|
+
"checkout": str(resolved), "integration": integration}
|
|
538
|
+
common = Path(_git(root, "rev-parse", "--path-format=absolute", "--git-common-dir"))
|
|
539
|
+
journal = common / "gsd-path" / "member-create" / f"{name}.json"
|
|
540
|
+
url = f"https://github.com/{github}.git"
|
|
541
|
+
if journal.is_file():
|
|
542
|
+
recorded = json.loads(journal.read_text(encoding="utf-8"))
|
|
543
|
+
if {key: recorded.get(key) for key in target} != target or recorded.get("step") not in {"create", "clone", "join"}:
|
|
544
|
+
raise MembersError(f"member-create journal for {name} holds a different approved target: {journal}")
|
|
545
|
+
else:
|
|
546
|
+
if any(member["name"] == name for member in read_members(root)):
|
|
547
|
+
raise MembersError(f"member already recorded: {name}")
|
|
548
|
+
if resolved.is_symlink() or (resolved.exists() and not resolved.is_dir()):
|
|
549
|
+
raise MembersError(f"member checkout path is occupied: {resolved}")
|
|
550
|
+
occupied = resolved.exists() and any(resolved.iterdir())
|
|
551
|
+
if occupied and Path(_common.run_git(resolved, "rev-parse", "--show-toplevel").stdout.strip()).resolve() != resolved:
|
|
552
|
+
raise MembersError(f"member checkout path is occupied: {resolved}")
|
|
553
|
+
if occupied and _git(resolved, "config", "--get", "remote.origin.url") != url:
|
|
554
|
+
raise MembersError(f"member checkout {resolved} is not a clone of {url}")
|
|
555
|
+
if not journal.is_file():
|
|
556
|
+
journal.parent.mkdir(parents=True, exist_ok=True)
|
|
557
|
+
recorded = {**target, "step": "create"}
|
|
558
|
+
_common.atomic_write(journal, json.dumps(recorded, indent=2, sort_keys=True) + "\n")
|
|
559
|
+
listed = [member for member in read_members(root) if member["name"] == name]
|
|
560
|
+
if listed:
|
|
561
|
+
if (len(listed) != 1 or listed[0] != {"name": name, "checkout": str(resolved), "remote": url,
|
|
562
|
+
"integration": integration}
|
|
563
|
+
or member_role(resolved) != {"coordinator": root, "project": state.project, "name": name}):
|
|
564
|
+
raise MembersError(f"member already recorded with a different checkout or marker: {name}")
|
|
565
|
+
journal.unlink()
|
|
566
|
+
return read_members(root)
|
|
567
|
+
viewed = _gh("repo", "view", github, "--json", "visibility")
|
|
568
|
+
if viewed.returncode != 0:
|
|
569
|
+
if "Could not resolve to a Repository" not in f"{viewed.stdout}\n{viewed.stderr}":
|
|
570
|
+
raise MembersError(f"could not inspect {github}: {(viewed.stderr or viewed.stdout).strip()}")
|
|
571
|
+
if recorded["step"] != "create":
|
|
572
|
+
raise MembersError(f"GitHub repository {github} disappeared after creation; refusing to create it again")
|
|
573
|
+
if occupied:
|
|
574
|
+
raise MembersError(f"member checkout {resolved} is a clone of a missing repository: {github}")
|
|
575
|
+
created = _gh("repo", "create", github, f"--{visibility}", "--add-readme")
|
|
576
|
+
if created.returncode != 0:
|
|
577
|
+
raise MembersError(f"could not create {github}: {(created.stderr or created.stdout).strip()}")
|
|
578
|
+
recorded["step"] = "clone"
|
|
579
|
+
_common.atomic_write(journal, json.dumps(recorded, indent=2, sort_keys=True) + "\n")
|
|
580
|
+
viewed = _gh("repo", "view", github, "--json", "visibility")
|
|
581
|
+
if viewed.returncode != 0:
|
|
582
|
+
raise MembersError(f"could not inspect {github}: {(viewed.stderr or viewed.stdout).strip()}")
|
|
583
|
+
try:
|
|
584
|
+
actual_visibility = json.loads(viewed.stdout)["visibility"].lower()
|
|
585
|
+
except (ValueError, KeyError, AttributeError) as error:
|
|
586
|
+
raise MembersError(f"could not read {github} visibility") from error
|
|
587
|
+
if actual_visibility != visibility:
|
|
588
|
+
raise MembersError(f"GitHub repository visibility is {actual_visibility}, not {visibility}")
|
|
589
|
+
recorded["step"] = "clone"
|
|
590
|
+
_common.atomic_write(journal, json.dumps(recorded, indent=2, sort_keys=True) + "\n")
|
|
591
|
+
if occupied:
|
|
592
|
+
fetched = _common.run_git(resolved, "fetch", "-q", "--prune", "origin")
|
|
593
|
+
if fetched.returncode != 0:
|
|
594
|
+
raise MembersError(f"could not fetch member checkout {resolved}: {(fetched.stderr or fetched.stdout).strip()}")
|
|
595
|
+
default = _common.run_git(resolved, "remote", "set-head", "origin", "--auto")
|
|
596
|
+
if default.returncode != 0 or _git(resolved, "symbolic-ref", "--short", "refs/remotes/origin/HEAD") != "origin/main":
|
|
597
|
+
raise MembersError(f"member remote default must be main: {resolved}")
|
|
598
|
+
baseline = _common.run_git(resolved, "rev-parse", "--verify", "--quiet", "refs/remotes/origin/main^{commit}")
|
|
599
|
+
if baseline.returncode != 0:
|
|
600
|
+
raise MembersError(f"member checkout {resolved} has no current origin/main")
|
|
601
|
+
head = _common.run_git(resolved, "rev-parse", "--verify", "--quiet", "HEAD^{commit}")
|
|
602
|
+
if head.returncode == 0:
|
|
603
|
+
if _common.run_git(resolved, "merge-base", "--is-ancestor", "HEAD", "origin/main").returncode != 0:
|
|
604
|
+
raise MembersError(f"member checkout {resolved} has history unrelated to current origin/main")
|
|
605
|
+
else:
|
|
606
|
+
_git(resolved, "checkout", "-B", "main", "origin/main")
|
|
607
|
+
else:
|
|
608
|
+
cloned = _common.run_git(resolved.parent, "clone", "-q", url, str(resolved))
|
|
609
|
+
if cloned.returncode != 0:
|
|
610
|
+
raise MembersError(f"could not clone {url}: {(cloned.stderr or cloned.stdout).strip()}")
|
|
611
|
+
recorded["step"] = "join"
|
|
612
|
+
_common.atomic_write(journal, json.dumps(recorded, indent=2, sort_keys=True) + "\n")
|
|
613
|
+
joined = add_member(root, name, resolved, integration)
|
|
614
|
+
journal.unlink()
|
|
615
|
+
return joined
|
|
616
|
+
|
|
617
|
+
|
|
618
|
+
def validate_members(repo: Path) -> list[dict[str, str]]:
|
|
619
|
+
root, state = _coordinator(repo)
|
|
620
|
+
members = read_members(root)
|
|
621
|
+
for member in members:
|
|
622
|
+
checkout = Path(member["checkout"])
|
|
623
|
+
check_member(root, state.project, checkout, member["remote"])
|
|
624
|
+
try:
|
|
625
|
+
role = member_role(checkout)
|
|
626
|
+
except MembersError as error:
|
|
627
|
+
raise MembersError(f"{error}; run members.py repair --repo {root}") from error
|
|
628
|
+
if role != {"coordinator": root, "project": state.project, "name": member["name"]}:
|
|
629
|
+
raise MembersError(
|
|
630
|
+
f"member marker for {member['name']} is missing or stale; "
|
|
631
|
+
f"run members.py repair --repo {root}"
|
|
632
|
+
)
|
|
633
|
+
return members
|
|
634
|
+
|
|
635
|
+
|
|
636
|
+
def repair_members(repo: Path) -> list[dict[str, str]]:
|
|
637
|
+
"""Rewrite missing or stale markers; never take over another coordinator's member."""
|
|
638
|
+
root, state = _coordinator(repo)
|
|
639
|
+
members = read_members(root)
|
|
640
|
+
for member in members:
|
|
641
|
+
checkout = Path(member["checkout"])
|
|
642
|
+
check_member(root, state.project, checkout, member["remote"])
|
|
643
|
+
_refuse_foreign_marker(checkout.resolve(), root, state.project, member["name"])
|
|
644
|
+
for member in members:
|
|
645
|
+
checkout = Path(member["checkout"]).resolve()
|
|
646
|
+
try:
|
|
647
|
+
role = member_role(checkout)
|
|
648
|
+
except MembersError:
|
|
649
|
+
role = None
|
|
650
|
+
if role != {"coordinator": root, "project": state.project, "name": member["name"]}:
|
|
651
|
+
_write_marker(checkout, root, state.project, member["name"])
|
|
652
|
+
return members
|
|
653
|
+
|
|
654
|
+
|
|
655
|
+
def parser() -> argparse.ArgumentParser:
|
|
656
|
+
result = argparse.ArgumentParser(description=__doc__)
|
|
657
|
+
commands = result.add_subparsers(dest="command", required=True)
|
|
658
|
+
add = commands.add_parser("add", help="record one member repository")
|
|
659
|
+
add.add_argument("--repo", required=True, type=Path, help="coordinator Git root")
|
|
660
|
+
add.add_argument("--name", required=True)
|
|
661
|
+
add.add_argument("--checkout", required=True, type=Path)
|
|
662
|
+
add.add_argument("--integration", choices=INTEGRATIONS, default="default")
|
|
663
|
+
add.add_argument("--create", metavar="OWNER/NAME",
|
|
664
|
+
help="greenfield: create this GitHub repo and clone it to --checkout first")
|
|
665
|
+
add.add_argument("--visibility", choices=VISIBILITIES, help="visibility for --create")
|
|
666
|
+
validate = commands.add_parser("validate", help="check MEMBERS.md and every member")
|
|
667
|
+
validate.add_argument("--repo", required=True, type=Path, help="coordinator Git root")
|
|
668
|
+
detect = commands.add_parser("detect", help="report whether a repo is a member, and of which coordinator")
|
|
669
|
+
detect.add_argument("--checkout", required=True, type=Path, help="repository to inspect")
|
|
670
|
+
repair = commands.add_parser("repair", help="rewrite missing or stale member markers")
|
|
671
|
+
repair.add_argument("--repo", required=True, type=Path, help="coordinator Git root")
|
|
672
|
+
return result
|
|
673
|
+
|
|
674
|
+
|
|
675
|
+
def main(argv: Optional[Sequence[str]] = None) -> int:
|
|
676
|
+
arguments = parser().parse_args(argv)
|
|
677
|
+
try:
|
|
678
|
+
if arguments.command == "detect":
|
|
679
|
+
print(json.dumps(detect_member(arguments.checkout), sort_keys=True))
|
|
680
|
+
return 0
|
|
681
|
+
if arguments.command == "add" and arguments.create:
|
|
682
|
+
if arguments.visibility is None:
|
|
683
|
+
raise MembersError("--create requires --visibility")
|
|
684
|
+
members = create_member(arguments.repo, arguments.name, arguments.checkout,
|
|
685
|
+
arguments.integration, arguments.create, arguments.visibility)
|
|
686
|
+
elif arguments.command == "add":
|
|
687
|
+
if arguments.visibility is not None:
|
|
688
|
+
raise MembersError("--visibility requires --create")
|
|
689
|
+
members = add_member(
|
|
690
|
+
arguments.repo, arguments.name, arguments.checkout, arguments.integration
|
|
691
|
+
)
|
|
692
|
+
elif arguments.command == "repair":
|
|
693
|
+
members = repair_members(arguments.repo)
|
|
694
|
+
else:
|
|
695
|
+
members = validate_members(arguments.repo)
|
|
696
|
+
except (MembersError, OSError) as error:
|
|
697
|
+
print(f"error: {error}", file=sys.stderr)
|
|
698
|
+
return 1
|
|
699
|
+
print(json.dumps({"members": members}, sort_keys=True))
|
|
700
|
+
return 0
|
|
701
|
+
|
|
702
|
+
|
|
703
|
+
if __name__ == "__main__":
|
|
704
|
+
raise SystemExit(main())
|