@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
|
@@ -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
|
|
|
@@ -12,23 +12,8 @@ import re
|
|
|
12
12
|
from pathlib import Path, PurePosixPath
|
|
13
13
|
from typing import Optional
|
|
14
14
|
|
|
15
|
-
|
|
16
|
-
from pipeline_git import (
|
|
17
|
-
bound_branch_name,
|
|
18
|
-
default_branch_name,
|
|
19
|
-
integrate_commit_body,
|
|
20
|
-
integrate_subject,
|
|
21
|
-
is_bound_branch,
|
|
22
|
-
milestone_id,
|
|
23
|
-
milestone_number,
|
|
24
|
-
ship_subject,
|
|
25
|
-
)
|
|
26
|
-
from pipeline_state import PipelineState
|
|
27
|
-
from isolation import publication_base
|
|
28
|
-
import _common
|
|
29
|
-
import worktree_paths
|
|
30
|
-
except ImportError: # pragma: no cover - package import used by tests
|
|
31
|
-
from scripts.pipeline_git import (
|
|
15
|
+
if __package__:
|
|
16
|
+
from .pipeline_git import (
|
|
32
17
|
bound_branch_name,
|
|
33
18
|
default_branch_name,
|
|
34
19
|
integrate_commit_body,
|
|
@@ -38,10 +23,41 @@ except ImportError: # pragma: no cover - package import used by tests
|
|
|
38
23
|
milestone_number,
|
|
39
24
|
ship_subject,
|
|
40
25
|
)
|
|
41
|
-
from
|
|
42
|
-
from
|
|
43
|
-
from
|
|
44
|
-
from
|
|
26
|
+
from .pipeline_state import PipelineState
|
|
27
|
+
from .isolation import publication_base
|
|
28
|
+
from . import _common
|
|
29
|
+
from . import worktree_paths
|
|
30
|
+
else:
|
|
31
|
+
try:
|
|
32
|
+
from pipeline_git import (
|
|
33
|
+
bound_branch_name,
|
|
34
|
+
default_branch_name,
|
|
35
|
+
integrate_commit_body,
|
|
36
|
+
integrate_subject,
|
|
37
|
+
is_bound_branch,
|
|
38
|
+
milestone_id,
|
|
39
|
+
milestone_number,
|
|
40
|
+
ship_subject,
|
|
41
|
+
)
|
|
42
|
+
from pipeline_state import PipelineState
|
|
43
|
+
from isolation import publication_base
|
|
44
|
+
import _common
|
|
45
|
+
import worktree_paths
|
|
46
|
+
except ImportError: # pragma: no cover - package import used by tests
|
|
47
|
+
from scripts.pipeline_git import (
|
|
48
|
+
bound_branch_name,
|
|
49
|
+
default_branch_name,
|
|
50
|
+
integrate_commit_body,
|
|
51
|
+
integrate_subject,
|
|
52
|
+
is_bound_branch,
|
|
53
|
+
milestone_id,
|
|
54
|
+
milestone_number,
|
|
55
|
+
ship_subject,
|
|
56
|
+
)
|
|
57
|
+
from scripts.pipeline_state import PipelineState
|
|
58
|
+
from scripts.isolation import publication_base
|
|
59
|
+
from scripts import _common
|
|
60
|
+
from scripts import worktree_paths
|
|
45
61
|
|
|
46
62
|
|
|
47
63
|
if __package__:
|
|
@@ -541,8 +557,8 @@ def ensure_integration_tag(
|
|
|
541
557
|
|
|
542
558
|
def github_repository(project: Path) -> str:
|
|
543
559
|
remote = archive_milestone.require_git_success(
|
|
544
|
-
run_git(project, "
|
|
545
|
-
"
|
|
560
|
+
run_git(project, "config", "--get", "remote.origin.url"),
|
|
561
|
+
"read configured origin URL",
|
|
546
562
|
)
|
|
547
563
|
patterns = (
|
|
548
564
|
r"https://github\.com/([^/]+/[^/]+?)(?:\.git)?$",
|
|
@@ -701,8 +717,9 @@ def update_pull_request_body(
|
|
|
701
717
|
archive_path: str,
|
|
702
718
|
ship_commit: str,
|
|
703
719
|
branch: str,
|
|
720
|
+
expected_body: Optional[str] = None,
|
|
704
721
|
) -> dict:
|
|
705
|
-
expected = pull_request_body(archive_path, ship_commit, branch)
|
|
722
|
+
expected = expected_body if expected_body is not None else pull_request_body(archive_path, ship_commit, branch)
|
|
706
723
|
current = pull.get("body")
|
|
707
724
|
if isinstance(current, str) and current.rstrip().endswith(PR_CREDIT_LINE):
|
|
708
725
|
return pull
|
|
@@ -1394,6 +1411,12 @@ def validate_integrated(
|
|
|
1394
1411
|
raise ArchiveError(f"published {ref} differs from local integration evidence")
|
|
1395
1412
|
if pull_request is not None:
|
|
1396
1413
|
result["pull_request"] = pull_request
|
|
1414
|
+
for row in shipped.get("members", []):
|
|
1415
|
+
proven = validate_member_integrated(project, row["name"], configured, row["reviewed_head"], refresh=refresh)
|
|
1416
|
+
if (proven["merge"], proven["tag"]) != (row["merge"], row["tag"]):
|
|
1417
|
+
raise ArchiveError(f"member {row['name']} integration differs from the ship commit")
|
|
1418
|
+
if "members" in shipped:
|
|
1419
|
+
result["members"] = shipped["members"]
|
|
1397
1420
|
return result
|
|
1398
1421
|
|
|
1399
1422
|
|
|
@@ -1457,3 +1480,390 @@ def require_published_integration(
|
|
|
1457
1480
|
raise ArchiveError(
|
|
1458
1481
|
f"published milestone tag {tag_name} does not point at the integration merge"
|
|
1459
1482
|
)
|
|
1483
|
+
|
|
1484
|
+
|
|
1485
|
+
def _member_names(coordinator: Path, member: str, archive_path: str) -> tuple[Path, str, str, str, str]:
|
|
1486
|
+
"""A member's checkout, coordinator project, bound branch, merge subject, and tag."""
|
|
1487
|
+
try:
|
|
1488
|
+
import isolation
|
|
1489
|
+
except ImportError: # pragma: no cover - package import used by tests
|
|
1490
|
+
from scripts import isolation
|
|
1491
|
+
try:
|
|
1492
|
+
checkout, project = isolation.member_checkout(coordinator.resolve(), member)
|
|
1493
|
+
except isolation.IsolationError as error:
|
|
1494
|
+
raise ArchiveError(str(error)) from error
|
|
1495
|
+
archive_name = PurePosixPath(archive_path).name
|
|
1496
|
+
identifier = milestone_id(milestone_number(archive_name))
|
|
1497
|
+
bound = f"gsd-path/{project}-{identifier}"
|
|
1498
|
+
subject = f"integrate: {project} {identifier} — merge {bound} into main"
|
|
1499
|
+
return checkout, project, bound, subject, f"milestone/{project}-{archive_name}"
|
|
1500
|
+
|
|
1501
|
+
|
|
1502
|
+
def _member_merge_body(archive_path: str, member: str, reviewed_head: str) -> str:
|
|
1503
|
+
return f"Archive: {archive_path}\nMember: {member}\nReviewed-HEAD: {reviewed_head}"
|
|
1504
|
+
|
|
1505
|
+
|
|
1506
|
+
def member_pull_request_tag_message(tag_name: str, pull_request: str, reviewed_head: str, landing: str) -> str:
|
|
1507
|
+
return (f"milestone {tag_name.removeprefix('milestone/')}\n\nMode: pull-request\n"
|
|
1508
|
+
f"Pull-Request: {pull_request}\nReviewed-HEAD: {reviewed_head}\nLanding: {landing}")
|
|
1509
|
+
|
|
1510
|
+
|
|
1511
|
+
def member_pull_request_tag_metadata(contents: str) -> dict[str, str]:
|
|
1512
|
+
fields: dict[str, str] = {}
|
|
1513
|
+
for line in contents.splitlines():
|
|
1514
|
+
match = re.fullmatch(r"(Mode|Pull-Request|Reviewed-HEAD|Landing): (.+)", line)
|
|
1515
|
+
if match:
|
|
1516
|
+
if match.group(1) in fields:
|
|
1517
|
+
raise ArchiveError("pull-request member tag repeats metadata")
|
|
1518
|
+
fields[match.group(1)] = match.group(2)
|
|
1519
|
+
if set(fields) != {"Mode", "Pull-Request", "Reviewed-HEAD", "Landing"}:
|
|
1520
|
+
raise ArchiveError("pull-request member tag is missing metadata")
|
|
1521
|
+
if fields["Mode"] != "pull-request":
|
|
1522
|
+
raise ArchiveError("pull-request member tag has the wrong mode")
|
|
1523
|
+
return fields
|
|
1524
|
+
|
|
1525
|
+
|
|
1526
|
+
def _find_member_merge(checkout: Path, subject: str, body: str, reviewed_head: str) -> Optional[str]:
|
|
1527
|
+
"""The member merge in origin/main's first-parent history, if published."""
|
|
1528
|
+
log = archive_milestone.require_git_success(
|
|
1529
|
+
run_git(checkout, "log", "--first-parent", "--format=%H%x00%s", "origin/main"),
|
|
1530
|
+
"inspect member origin/main first-parent history",
|
|
1531
|
+
)
|
|
1532
|
+
matches = [commit for commit, _, text in (line.partition("\x00") for line in log.splitlines())
|
|
1533
|
+
if text == subject]
|
|
1534
|
+
if len(matches) > 1:
|
|
1535
|
+
raise ArchiveError(f"multiple member merges named {subject!r} on origin/main")
|
|
1536
|
+
if not matches:
|
|
1537
|
+
return None
|
|
1538
|
+
_require_member_merge(checkout, matches[0], subject, body, reviewed_head)
|
|
1539
|
+
return matches[0]
|
|
1540
|
+
|
|
1541
|
+
|
|
1542
|
+
def _require_member_merge(checkout: Path, commit: str, subject: str, body: str, reviewed_head: str) -> str:
|
|
1543
|
+
"""Check a canonical member merge; return its first parent."""
|
|
1544
|
+
archive_milestone.require_canonical_commit_body(checkout, commit, subject, body, "member integration")
|
|
1545
|
+
parents = archive_milestone.require_git_success(
|
|
1546
|
+
run_git(checkout, "rev-list", "--parents", "-n", "1", commit), "inspect member merge parents"
|
|
1547
|
+
).split()
|
|
1548
|
+
if len(parents) != 3 or parents[2] != reviewed_head:
|
|
1549
|
+
raise ArchiveError("member merge second parent is not the member reviewed HEAD")
|
|
1550
|
+
return parents[1]
|
|
1551
|
+
|
|
1552
|
+
|
|
1553
|
+
def _create_member_merge(checkout: Path, project: str, archive_path: str, subject: str, body: str,
|
|
1554
|
+
reviewed_head: str, origin_main: str) -> str:
|
|
1555
|
+
identifier = milestone_id(milestone_number(PurePosixPath(archive_path).name))
|
|
1556
|
+
branch = f"gsd-path-integrate/{project}-{identifier}"
|
|
1557
|
+
try:
|
|
1558
|
+
worktree = worktree_paths.worktree_path(checkout, "integrate", f"{project}-{identifier}", pin=True)
|
|
1559
|
+
except (ValueError, OSError) as error:
|
|
1560
|
+
raise ArchiveError(str(error)) from error
|
|
1561
|
+
interrupted = registered_worktree(checkout, branch)
|
|
1562
|
+
if interrupted is not None and optional_ref(interrupted, "MERGE_HEAD") is not None:
|
|
1563
|
+
archive_milestone.require_git_success(run_git(interrupted, "merge", "--abort"),
|
|
1564
|
+
"abort interrupted member merge")
|
|
1565
|
+
remove_registered_worktree(checkout, branch, worktree)
|
|
1566
|
+
tip = optional_ref(checkout, f"refs/heads/{branch}")
|
|
1567
|
+
if tip is not None:
|
|
1568
|
+
base_tip = run_git(checkout, "merge-base", "--is-ancestor", tip, origin_main)
|
|
1569
|
+
if base_tip.returncode != 0:
|
|
1570
|
+
if base_tip.returncode != 1:
|
|
1571
|
+
archive_milestone.require_git_success(base_tip, "inspect interrupted member integration base")
|
|
1572
|
+
if _require_member_merge(checkout, tip, subject, body, reviewed_head) == origin_main:
|
|
1573
|
+
return tip
|
|
1574
|
+
delete_integration_branch(checkout, branch, tip)
|
|
1575
|
+
archive_milestone.require_git_success(
|
|
1576
|
+
run_git(checkout, "worktree", "add", "--quiet", "-b", branch, str(worktree), origin_main),
|
|
1577
|
+
"create member integration worktree",
|
|
1578
|
+
)
|
|
1579
|
+
merged = run_git(worktree, "merge", "--no-ff", "-m", subject, "-m", body, reviewed_head)
|
|
1580
|
+
if merged.returncode != 0:
|
|
1581
|
+
conflicts = run_git(worktree, "diff", "--name-only", "--diff-filter=U").stdout.split()
|
|
1582
|
+
if optional_ref(worktree, "MERGE_HEAD") is not None:
|
|
1583
|
+
run_git(worktree, "merge", "--abort")
|
|
1584
|
+
remove_registered_worktree(checkout, branch, worktree)
|
|
1585
|
+
delete_integration_branch(checkout, branch, origin_main)
|
|
1586
|
+
raise ArchiveError("member integration merge failed; Git aborted it without resolving files: "
|
|
1587
|
+
+ (", ".join(conflicts) or (merged.stderr or merged.stdout).strip()))
|
|
1588
|
+
merge = archive_milestone.require_git_success(run_git(worktree, "rev-parse", "HEAD"), "resolve member merge")
|
|
1589
|
+
if _require_member_merge(worktree, merge, subject, body, reviewed_head) != origin_main:
|
|
1590
|
+
raise ArchiveError("member merge first parent is not origin/main")
|
|
1591
|
+
remove_registered_worktree(checkout, branch, worktree)
|
|
1592
|
+
return merge
|
|
1593
|
+
|
|
1594
|
+
|
|
1595
|
+
def _push_member_ref(checkout: Path, project: str, source: str, ref: str, sha: str, lease: Optional[str]) -> None:
|
|
1596
|
+
"""Authorize `ref` at `sha` for the member pre-push hook, then publish it."""
|
|
1597
|
+
try:
|
|
1598
|
+
import members
|
|
1599
|
+
except ImportError: # pragma: no cover - package import used by tests
|
|
1600
|
+
from scripts import members
|
|
1601
|
+
members.authorize_push(checkout, project, ref, sha)
|
|
1602
|
+
arguments = ["push"] + ([f"--force-with-lease={ref}:{lease}"] if lease is not None else [])
|
|
1603
|
+
archive_milestone.require_git_success(
|
|
1604
|
+
run_git(checkout, *arguments, "origin", f"{source}:{ref}"), f"publish member {ref}"
|
|
1605
|
+
)
|
|
1606
|
+
|
|
1607
|
+
|
|
1608
|
+
def _publish_member_bound(checkout: Path, project: str, member: str, bound: str, reviewed_head: str) -> None:
|
|
1609
|
+
bound_ref = f"refs/heads/{bound}"
|
|
1610
|
+
remote_bound = live_remote_ref(checkout, bound_ref)
|
|
1611
|
+
if remote_bound is None:
|
|
1612
|
+
_push_member_ref(checkout, project, reviewed_head, bound_ref, reviewed_head, "")
|
|
1613
|
+
elif remote_bound != reviewed_head:
|
|
1614
|
+
raise ArchiveError(f"member {member} origin/{bound} moved: {remote_bound}")
|
|
1615
|
+
|
|
1616
|
+
|
|
1617
|
+
def _require_member_close(checkout: Path, member: str, bound: str, reviewed_head: str) -> None:
|
|
1618
|
+
if optional_ref(checkout, f"refs/heads/{bound}") != reviewed_head:
|
|
1619
|
+
raise ArchiveError(f"member {member} {bound} is not at its reviewed HEAD {reviewed_head}")
|
|
1620
|
+
if default_branch_name(refresh_origin(checkout)["remote_default"]) != "main":
|
|
1621
|
+
raise ArchiveError(f"member {member} remote default must be main")
|
|
1622
|
+
|
|
1623
|
+
|
|
1624
|
+
def integrate_member(coordinator: Path, member: str, archive_path: str, reviewed_head: str) -> dict:
|
|
1625
|
+
"""Direct-mode member close: publish the bound branch, merge it into main, then tag."""
|
|
1626
|
+
checkout, project, bound, subject, tag_name = _member_names(coordinator, member, archive_path)
|
|
1627
|
+
body = _member_merge_body(archive_path, member, reviewed_head)
|
|
1628
|
+
_require_member_close(checkout, member, bound, reviewed_head)
|
|
1629
|
+
merge = _find_member_merge(checkout, subject, body, reviewed_head)
|
|
1630
|
+
if merge is None:
|
|
1631
|
+
origin_main = archive_milestone.require_git_success(
|
|
1632
|
+
run_git(checkout, "rev-parse", "origin/main"), "resolve member origin/main")
|
|
1633
|
+
ancestry = run_git(checkout, "merge-base", "--is-ancestor", reviewed_head, origin_main)
|
|
1634
|
+
if ancestry.returncode == 0:
|
|
1635
|
+
raise ArchiveError("member reviewed HEAD reached origin/main outside Path; user-approved recovery required")
|
|
1636
|
+
if ancestry.returncode != 1:
|
|
1637
|
+
archive_milestone.require_git_success(ancestry, "inspect member reviewed HEAD ancestry")
|
|
1638
|
+
merge = _create_member_merge(checkout, project, archive_path, subject, body, reviewed_head, origin_main)
|
|
1639
|
+
# Publish in order: bound branch, main, tag; a rerun publishes only what is missing.
|
|
1640
|
+
_publish_member_bound(checkout, project, member, bound, reviewed_head)
|
|
1641
|
+
if run_git(checkout, "merge-base", "--is-ancestor", merge, "origin/main").returncode != 0:
|
|
1642
|
+
_push_member_ref(checkout, project, merge, "refs/heads/main", merge, None)
|
|
1643
|
+
archive_milestone.require_git_success(
|
|
1644
|
+
run_git(checkout, "update-ref", "refs/remotes/origin/main", merge), "refresh member origin/main")
|
|
1645
|
+
tag_object, tag_published = ensure_integration_tag(checkout, tag_name, merge)
|
|
1646
|
+
if not tag_published:
|
|
1647
|
+
_push_member_ref(checkout, project, f"refs/tags/{tag_name}", f"refs/tags/{tag_name}", tag_object, None)
|
|
1648
|
+
archive_milestone.require_git_success(
|
|
1649
|
+
run_git(checkout, "update-ref", f"refs/remotes/origin/tags/{tag_name}", tag_object),
|
|
1650
|
+
"refresh member milestone-tag ref")
|
|
1651
|
+
identifier = milestone_id(milestone_number(PurePosixPath(archive_path).name))
|
|
1652
|
+
delete_integration_branch(checkout, f"gsd-path-integrate/{project}-{identifier}", merge)
|
|
1653
|
+
return validate_member_integrated(coordinator, member, archive_path, reviewed_head)
|
|
1654
|
+
|
|
1655
|
+
|
|
1656
|
+
def integrate_member_pull_request(coordinator: Path, member: str, archive_path: str, reviewed_head: str) -> dict:
|
|
1657
|
+
"""Pull-request member close: open one PR for the bound branch; after a human merge, tag it."""
|
|
1658
|
+
checkout, project, bound, subject, tag_name = _member_names(coordinator, member, archive_path)
|
|
1659
|
+
body = _member_merge_body(archive_path, member, reviewed_head)
|
|
1660
|
+
_require_member_close(checkout, member, bound, reviewed_head)
|
|
1661
|
+
require_github_authentication()
|
|
1662
|
+
repository = github_repository(checkout)
|
|
1663
|
+
pull = find_pull_request(repository, bound, reviewed_head)
|
|
1664
|
+
if pull is None or pull["merged_at"] is None:
|
|
1665
|
+
if pull is not None and pull["state"] != "open":
|
|
1666
|
+
raise ArchiveError(f"member {member} pull request was closed without merging")
|
|
1667
|
+
_publish_member_bound(checkout, project, member, bound, reviewed_head)
|
|
1668
|
+
if pull is None:
|
|
1669
|
+
pull = require_pull_request_shape(github_api_json(
|
|
1670
|
+
f"repos/{repository}/pulls", "--method", "POST", "-f", f"title={subject}",
|
|
1671
|
+
"-f", f"head={bound}", "-f", "base=main", "-f", f"body={body}\n\n---\n{PR_CREDIT_LINE}"))
|
|
1672
|
+
require_pull_request_identity(repository, pull, bound, reviewed_head)
|
|
1673
|
+
else:
|
|
1674
|
+
pull = update_pull_request_body(
|
|
1675
|
+
repository, pull, archive_path, reviewed_head, bound,
|
|
1676
|
+
f"{body}\n\n---\n{PR_CREDIT_LINE}",
|
|
1677
|
+
)
|
|
1678
|
+
require_pull_request_merge_provenance(repository, pull["number"])
|
|
1679
|
+
return {"status": "awaiting-merge", "mode": "pull-request", "member": member,
|
|
1680
|
+
"pull_request": pull["html_url"]}
|
|
1681
|
+
merge = pull["merge_commit_sha"]
|
|
1682
|
+
if pull["state"] != "closed" or not isinstance(merge, str):
|
|
1683
|
+
raise ArchiveError(f"member {member} pull request merge metadata is invalid")
|
|
1684
|
+
require_pull_request_merge_provenance(repository, pull["number"], merge)
|
|
1685
|
+
refresh_origin(checkout)
|
|
1686
|
+
require_pull_request_merge(checkout, merge, reviewed_head, "origin/main")
|
|
1687
|
+
pull = update_pull_request_body(
|
|
1688
|
+
repository, pull, archive_path, reviewed_head, bound,
|
|
1689
|
+
f"{body}\n\n---\n{PR_CREDIT_LINE}",
|
|
1690
|
+
)
|
|
1691
|
+
message = member_pull_request_tag_message(tag_name, pull["html_url"], reviewed_head, merge)
|
|
1692
|
+
tag_object, tag_published = ensure_integration_tag(checkout, tag_name, merge, message)
|
|
1693
|
+
if not tag_published:
|
|
1694
|
+
_push_member_ref(checkout, project, f"refs/tags/{tag_name}", f"refs/tags/{tag_name}", tag_object, None)
|
|
1695
|
+
archive_milestone.require_git_success(
|
|
1696
|
+
run_git(checkout, "update-ref", f"refs/remotes/origin/tags/{tag_name}", tag_object),
|
|
1697
|
+
"refresh member milestone-tag ref")
|
|
1698
|
+
return validate_member_integrated(coordinator, member, archive_path, reviewed_head)
|
|
1699
|
+
|
|
1700
|
+
|
|
1701
|
+
def validate_member_integrated(
|
|
1702
|
+
coordinator: Path, member: str, archive_path: str, reviewed_head: str, *, refresh: bool = True
|
|
1703
|
+
) -> dict:
|
|
1704
|
+
"""Prove the member merge is on origin/main and its published tag points at it."""
|
|
1705
|
+
checkout, _, bound, subject, tag_name = _member_names(coordinator, member, archive_path)
|
|
1706
|
+
tracking = f"refs/remotes/origin/tags/{tag_name}"
|
|
1707
|
+
if refresh:
|
|
1708
|
+
refresh_origin(checkout)
|
|
1709
|
+
published = live_remote_ref(checkout, f"refs/tags/{tag_name}")
|
|
1710
|
+
else:
|
|
1711
|
+
published = optional_ref(checkout, tracking)
|
|
1712
|
+
contents = ""
|
|
1713
|
+
if refresh and published is not None:
|
|
1714
|
+
archive_milestone.require_git_success(
|
|
1715
|
+
run_git(checkout, "update-ref", tracking, published), "refresh member milestone-tag ref")
|
|
1716
|
+
if published is not None:
|
|
1717
|
+
contents = run_git(checkout, "for-each-ref", "--format=%(contents)", tracking).stdout
|
|
1718
|
+
if "Mode: pull-request" in contents.splitlines():
|
|
1719
|
+
metadata = member_pull_request_tag_metadata(contents)
|
|
1720
|
+
if metadata["Reviewed-HEAD"] != reviewed_head:
|
|
1721
|
+
raise ArchiveError(f"member {member} tag {tag_name} names a different reviewed HEAD")
|
|
1722
|
+
merge = archive_milestone.require_git_success(
|
|
1723
|
+
run_git(checkout, "rev-parse", f"{tracking}^{{commit}}"), "resolve member tag target")
|
|
1724
|
+
if metadata["Landing"] != merge:
|
|
1725
|
+
raise ArchiveError(f"member {member} tag {tag_name} names a different landing")
|
|
1726
|
+
require_github_authentication()
|
|
1727
|
+
repository = github_repository(checkout)
|
|
1728
|
+
pull = find_pull_request(repository, bound, reviewed_head)
|
|
1729
|
+
if pull is None:
|
|
1730
|
+
raise ArchiveError(f"member {member} GitHub pull request is missing")
|
|
1731
|
+
if pull["html_url"] != metadata["Pull-Request"]:
|
|
1732
|
+
raise ArchiveError(f"member {member} tag {tag_name} names the wrong pull request")
|
|
1733
|
+
if (pull["state"] != "closed" or pull["merged_at"] is None
|
|
1734
|
+
or pull["merge_commit_sha"] != merge):
|
|
1735
|
+
raise ArchiveError(f"member {member} GitHub pull request is not merged at the tagged landing")
|
|
1736
|
+
require_pull_request_merge_provenance(repository, pull["number"], merge)
|
|
1737
|
+
require_pull_request_merge(checkout, merge, reviewed_head, "origin/main")
|
|
1738
|
+
require_annotated_tag(
|
|
1739
|
+
checkout, tracking, merge, f"member tag {tag_name}",
|
|
1740
|
+
member_pull_request_tag_message(tag_name, pull["html_url"], reviewed_head, merge),
|
|
1741
|
+
)
|
|
1742
|
+
else:
|
|
1743
|
+
merge = _find_member_merge(checkout, subject, _member_merge_body(archive_path, member, reviewed_head),
|
|
1744
|
+
reviewed_head)
|
|
1745
|
+
if merge is None:
|
|
1746
|
+
raise ArchiveError(f"member {member} has no {subject!r} merge on origin/main")
|
|
1747
|
+
if published is None:
|
|
1748
|
+
raise ArchiveError(f"member {member} tag {tag_name} is not published")
|
|
1749
|
+
require_annotated_tag(checkout, tracking, merge, f"member tag {tag_name}")
|
|
1750
|
+
return {"member": member, "merge": merge, "tag": tag_name, "tag_object": published}
|
|
1751
|
+
|
|
1752
|
+
|
|
1753
|
+
def retire_member(coordinator: Path, member: str, archive_path: str, reviewed_head: str,
|
|
1754
|
+
merge: str, tag: str) -> dict:
|
|
1755
|
+
"""After integration, delete a member's bound branch everywhere and clear its authorizations."""
|
|
1756
|
+
try:
|
|
1757
|
+
import members
|
|
1758
|
+
except ImportError: # pragma: no cover - package import used by tests
|
|
1759
|
+
from scripts import members
|
|
1760
|
+
checkout, project, bound, _, tag_name = _member_names(coordinator, member, archive_path)
|
|
1761
|
+
proven = validate_member_integrated(coordinator, member, archive_path, reviewed_head)
|
|
1762
|
+
if (proven["merge"], proven["tag"]) != (merge, tag):
|
|
1763
|
+
raise ArchiveError(f"member {member} integration differs from the ship commit")
|
|
1764
|
+
bound_ref = f"refs/heads/{bound}"
|
|
1765
|
+
local = optional_ref(checkout, bound_ref)
|
|
1766
|
+
if local is not None and local != reviewed_head:
|
|
1767
|
+
raise ArchiveError(f"member {member} {bound} moved: {local}")
|
|
1768
|
+
holder = registered_worktree(checkout, bound)
|
|
1769
|
+
if holder is not None:
|
|
1770
|
+
try:
|
|
1771
|
+
expected = worktree_paths.worktree_path(
|
|
1772
|
+
checkout, "bound", bound.removeprefix("gsd-path/")
|
|
1773
|
+
).resolve()
|
|
1774
|
+
except (ValueError, OSError) as error:
|
|
1775
|
+
raise ArchiveError(str(error)) from error
|
|
1776
|
+
if holder != expected:
|
|
1777
|
+
raise ArchiveError(f"member {member} bound checkout is outside Path's workspace: {holder}")
|
|
1778
|
+
status = archive_milestone.require_git_success(
|
|
1779
|
+
run_git(holder, "status", "--porcelain", "--untracked-files=all"),
|
|
1780
|
+
"inspect member bound checkout",
|
|
1781
|
+
)
|
|
1782
|
+
if status:
|
|
1783
|
+
raise ArchiveError(f"member {member} bound checkout is not clean: {holder}")
|
|
1784
|
+
remote = live_remote_ref(checkout, bound_ref)
|
|
1785
|
+
if remote is not None:
|
|
1786
|
+
if remote != reviewed_head:
|
|
1787
|
+
raise ArchiveError(f"member {member} origin/{bound} moved: {remote}")
|
|
1788
|
+
members.authorize_delete(checkout, project, bound_ref, reviewed_head)
|
|
1789
|
+
archive_milestone.require_git_success(
|
|
1790
|
+
run_git(checkout, "push", f"--force-with-lease={bound_ref}:{reviewed_head}", "origin", "--delete", bound),
|
|
1791
|
+
f"delete member origin/{bound}")
|
|
1792
|
+
if holder is not None:
|
|
1793
|
+
archive_milestone.require_git_success(run_git(checkout, "worktree", "remove", str(holder)),
|
|
1794
|
+
"remove member bound checkout")
|
|
1795
|
+
if local is not None:
|
|
1796
|
+
archive_milestone.require_git_success(run_git(checkout, "update-ref", "-d", bound_ref, reviewed_head),
|
|
1797
|
+
"delete member bound branch")
|
|
1798
|
+
run_git(checkout, "update-ref", "-d", f"refs/remotes/origin/{bound}")
|
|
1799
|
+
for kind, ref in (("push", bound_ref), ("push", "refs/heads/main"), ("push", f"refs/tags/{tag_name}"),
|
|
1800
|
+
("delete", bound_ref)):
|
|
1801
|
+
members.clear_authorization(checkout, project, kind, ref)
|
|
1802
|
+
return {"member": member, "retired": bound}
|
|
1803
|
+
|
|
1804
|
+
|
|
1805
|
+
def close_members(repo: Path) -> dict:
|
|
1806
|
+
"""Close each locked member in lock order before the coordinator ship commit; resumable."""
|
|
1807
|
+
try:
|
|
1808
|
+
import check_handoffs
|
|
1809
|
+
import members
|
|
1810
|
+
from pipeline_git import ship_commit_body
|
|
1811
|
+
except ImportError: # pragma: no cover - package import used by tests
|
|
1812
|
+
from scripts import check_handoffs, members
|
|
1813
|
+
from scripts.pipeline_git import ship_commit_body
|
|
1814
|
+
project = repo.resolve()
|
|
1815
|
+
state, _, _ = archive_milestone.strict_state(project)
|
|
1816
|
+
if (state.phase, state.status) != ("ship", "active") or state.archive is None:
|
|
1817
|
+
raise ArchiveError("close-members requires ship/active with a prepared archive")
|
|
1818
|
+
archive = project / state.archive
|
|
1819
|
+
final_text = (archive / "review" / "FINAL.md").read_text(encoding="utf-8")
|
|
1820
|
+
reviewed = check_handoffs._reviewed_head(final_text, "FINAL.md")
|
|
1821
|
+
lock = archive / "build" / "members.json"
|
|
1822
|
+
locked = json.loads(lock.read_text(encoding="utf-8"))["members"] if lock.is_file() else []
|
|
1823
|
+
try:
|
|
1824
|
+
heads = check_handoffs.member_reviewed_heads(final_text, "FINAL.md")
|
|
1825
|
+
except check_handoffs.HandoffError as error:
|
|
1826
|
+
raise ArchiveError(str(error)) from error
|
|
1827
|
+
if sorted(heads) != sorted(entry["name"] for entry in locked):
|
|
1828
|
+
raise ArchiveError("FINAL.md Member reviewed HEAD lines must name each locked member")
|
|
1829
|
+
common = Path(archive_milestone.require_git_success(
|
|
1830
|
+
run_git(project, "rev-parse", "--path-format=absolute", "--git-common-dir"), "resolve Git common dir"))
|
|
1831
|
+
archive_name = PurePosixPath(state.archive).name
|
|
1832
|
+
try:
|
|
1833
|
+
modes = {item["name"]: item["integration"] for item in members.read_members(project)} if locked else {}
|
|
1834
|
+
rows = {row["name"]: row for row in members.read_member_close(common, archive_name)}
|
|
1835
|
+
except members.MembersError as error:
|
|
1836
|
+
raise ArchiveError(str(error)) from error
|
|
1837
|
+
def save() -> None:
|
|
1838
|
+
members.write_member_close(common, archive_name, [rows[item["name"]] for item in locked if item["name"] in rows])
|
|
1839
|
+
|
|
1840
|
+
closed = []
|
|
1841
|
+
for entry in locked:
|
|
1842
|
+
name = entry["name"]
|
|
1843
|
+
mode = modes.get(name, "default")
|
|
1844
|
+
mode = state.integration if mode == "default" else mode
|
|
1845
|
+
row = rows.get(name) or {"name": name, "mode": mode, "reviewed_head": heads[name],
|
|
1846
|
+
"status": "pending", "merge": None, "tag": None}
|
|
1847
|
+
if row["reviewed_head"] != heads[name] or row["mode"] != mode:
|
|
1848
|
+
raise ArchiveError(f"member-close journal for {name} differs from FINAL.md or MEMBERS.md")
|
|
1849
|
+
if row["status"] == "integrated":
|
|
1850
|
+
result = validate_member_integrated(project, name, state.archive, heads[name])
|
|
1851
|
+
if (result["merge"], result["tag"]) != (row["merge"], row["tag"]):
|
|
1852
|
+
raise ArchiveError(f"member {name} integration differs from its member-close journal")
|
|
1853
|
+
closed.append(row)
|
|
1854
|
+
continue
|
|
1855
|
+
# Journal pending before the member moves; a later member waits for this one.
|
|
1856
|
+
rows[name] = row
|
|
1857
|
+
save()
|
|
1858
|
+
if mode == "direct":
|
|
1859
|
+
result = integrate_member(project, name, state.archive, heads[name])
|
|
1860
|
+
else:
|
|
1861
|
+
result = integrate_member_pull_request(project, name, state.archive, heads[name])
|
|
1862
|
+
if result.get("status") == "awaiting-merge":
|
|
1863
|
+
row["pull_request"] = result["pull_request"]
|
|
1864
|
+
save()
|
|
1865
|
+
return {"status": "awaiting-merge", "member": name, "pull_request": result["pull_request"]}
|
|
1866
|
+
row.update(status="integrated", merge=result["merge"], tag=result["tag"])
|
|
1867
|
+
save()
|
|
1868
|
+
closed.append(row)
|
|
1869
|
+
return {"status": "integrated", "members": closed, "body": ship_commit_body(state.archive, reviewed, closed)}
|