@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
|
@@ -19,8 +19,6 @@ import hashlib
|
|
|
19
19
|
import json
|
|
20
20
|
import os
|
|
21
21
|
import re
|
|
22
|
-
import shlex
|
|
23
|
-
import signal
|
|
24
22
|
import subprocess
|
|
25
23
|
import sys
|
|
26
24
|
import tempfile
|
|
@@ -170,12 +168,7 @@ def child_running(state: Dict[str, object]) -> bool:
|
|
|
170
168
|
return state.get("finished_at") is None and bool(state.get("pid")) and process_alive(int(state["pid"]))
|
|
171
169
|
|
|
172
170
|
|
|
173
|
-
|
|
174
|
-
try:
|
|
175
|
-
os.kill(pid, 0)
|
|
176
|
-
except ProcessLookupError:
|
|
177
|
-
return False
|
|
178
|
-
return True
|
|
171
|
+
process_alive = _common.process_alive
|
|
179
172
|
|
|
180
173
|
|
|
181
174
|
# --- child process ---------------------------------------------------------
|
|
@@ -208,13 +201,13 @@ def child_main(state_path: Path) -> int:
|
|
|
208
201
|
attempt_dir = state_path.parent
|
|
209
202
|
with open(attempt_dir / "brief.md", "rb") as brief, \
|
|
210
203
|
open(attempt_dir / "stdout", "wb") as out, open(attempt_dir / "stderr", "wb") as err:
|
|
211
|
-
process =
|
|
212
|
-
|
|
204
|
+
process = _common.popen_group(_common.resolve_argv(state["command"]), cwd=state["worktree"],
|
|
205
|
+
stdin=brief, stdout=out, stderr=err)
|
|
213
206
|
try:
|
|
214
207
|
exit_code: Optional[int] = process.wait(timeout=state.get("child_timeout"))
|
|
215
208
|
timed_out = False
|
|
216
209
|
except subprocess.TimeoutExpired:
|
|
217
|
-
|
|
210
|
+
_common.kill_tree(process)
|
|
218
211
|
process.wait()
|
|
219
212
|
exit_code, timed_out = None, True
|
|
220
213
|
save_state(attempt_dir / "exit.json",
|
|
@@ -224,6 +217,7 @@ def child_main(state_path: Path) -> int:
|
|
|
224
217
|
|
|
225
218
|
def brief_text(state: Dict[str, object], role_brief: Path, task_template: Path) -> str:
|
|
226
219
|
worktree = Path(str(state["worktree"]))
|
|
220
|
+
project_root = Path(str(state.get("project_root") or worktree))
|
|
227
221
|
task_id = state["task_id"]
|
|
228
222
|
agents = worktree / "AGENTS.md"
|
|
229
223
|
workflow = worktree / "WORKFLOW.md"
|
|
@@ -234,12 +228,14 @@ def brief_text(state: Dict[str, object], role_brief: Path, task_template: Path)
|
|
|
234
228
|
f"Work only in this worktree root: {worktree}",
|
|
235
229
|
f"Task file: {worktree / str(state['task_file'])}",
|
|
236
230
|
f"Task template: {task_template}",
|
|
237
|
-
f"INTENT.md: {
|
|
231
|
+
f"INTENT.md: {project_root / '.project/intent/INTENT.md'}",
|
|
238
232
|
f"AGENTS.md: {agents if agents.exists() else 'absent'}",
|
|
239
233
|
f"WORKFLOW.md: {workflow if workflow.exists() else 'absent'}",
|
|
240
234
|
f"Recorded base: {state['base']}",
|
|
241
235
|
f"Dispatch mode: {state['mode']} ("
|
|
242
236
|
+ ("the primary worktree on the bound branch" if state["mode"] == "serial"
|
|
237
|
+
else f"an isolated worktree of member {state['member']}; never touch the member team's "
|
|
238
|
+
"checkout or the coordinator" if state["mode"] == "member"
|
|
243
239
|
else "an isolated task worktree; never touch the primary") + ").",
|
|
244
240
|
"Next consumer: the build orchestrator reruns Verify in an isolated checkout and lands "
|
|
245
241
|
"the task. Gate: Verify passes and only the task's declared files plus its Log changed.",
|
|
@@ -248,7 +244,7 @@ def brief_text(state: Dict[str, object], role_brief: Path, task_template: Path)
|
|
|
248
244
|
if state.get("answered"):
|
|
249
245
|
lines.append(f"The task Log now records an `{ANSWER_MARK}` to your earlier question; "
|
|
250
246
|
"continue from it.")
|
|
251
|
-
lines.append(task_context.render(
|
|
247
|
+
lines.append(task_context.render(project_root, worktree / str(state['task_file'])))
|
|
252
248
|
return "\n".join(lines) + "\n"
|
|
253
249
|
|
|
254
250
|
|
|
@@ -282,7 +278,7 @@ def selected_command(state: Dict[str, object], options: argparse.Namespace, comm
|
|
|
282
278
|
if caps_path is None:
|
|
283
279
|
if overrides or previous or model_policy.policy_path(primary, project_dir).exists():
|
|
284
280
|
raise model_policy.PolicyError('model policy requires --model-capabilities for this dispatch')
|
|
285
|
-
return
|
|
281
|
+
return _common.split_command(command), None
|
|
286
282
|
caps = model_policy.read_json(caps_path)
|
|
287
283
|
action = getattr(options, 'action', 'round')
|
|
288
284
|
role = {'round': 'coder', 'review': 'reviewer', 'panel': 'review_panel',
|
|
@@ -297,7 +293,7 @@ def selected_command(state: Dict[str, object], options: argparse.Namespace, comm
|
|
|
297
293
|
dict(overrides, model=state['slug']), previous)
|
|
298
294
|
model_policy.validate_panel(selection, state['family'], state.get('excluded_families', []))
|
|
299
295
|
state['slug'] = selection['selected']['model']
|
|
300
|
-
return model_policy.command_args(
|
|
296
|
+
return model_policy.command_args(_common.split_command(command), selection), selection
|
|
301
297
|
|
|
302
298
|
|
|
303
299
|
def retain_selection(root: Path, state: Dict[str, object]) -> Dict[str, object]:
|
|
@@ -340,12 +336,12 @@ def spawn(root: Path, state: Dict[str, object], options: argparse.Namespace,
|
|
|
340
336
|
"child_timeout": options.child_timeout, "outcome": None,
|
|
341
337
|
"dispatched_at": now(), "origin": "question" if state.get("answered") else "dispatch"})
|
|
342
338
|
state_path = attempt_dir / "state.json"
|
|
343
|
-
(attempt_dir / "brief.md").
|
|
339
|
+
(attempt_dir / "brief.md").write_bytes(brief(state).encode("utf-8"))
|
|
344
340
|
save_state(state_path, state)
|
|
345
|
-
wrapper =
|
|
341
|
+
wrapper = _common.popen_detached(
|
|
346
342
|
[sys.executable, "-B", str(Path(__file__).resolve()), "_child", "--state", str(state_path)],
|
|
347
343
|
cwd=state["worktree"], stdin=subprocess.DEVNULL, stdout=subprocess.DEVNULL,
|
|
348
|
-
stderr=subprocess.DEVNULL,
|
|
344
|
+
stderr=subprocess.DEVNULL,
|
|
349
345
|
)
|
|
350
346
|
state["_path"] = str(state_path)
|
|
351
347
|
update_state(state, pid=wrapper.pid)
|
|
@@ -356,8 +352,11 @@ def spawn(root: Path, state: Dict[str, object], options: argparse.Namespace,
|
|
|
356
352
|
|
|
357
353
|
|
|
358
354
|
def log_delta(primary: Path, state: Dict[str, object]) -> str:
|
|
359
|
-
|
|
355
|
+
contract_file = state.get("contract_file") or state["task_file"]
|
|
356
|
+
base_text = isolation.git_text(primary, "show", f"{state['base']}:{contract_file}")
|
|
360
357
|
text = (Path(str(state["worktree"])) / str(state["task_file"])).read_text(encoding="utf-8")
|
|
358
|
+
if state["mode"] == "member":
|
|
359
|
+
return isolation.member_log_delta(base_text, text)
|
|
361
360
|
return isolation.task_log_delta(base_text, text)
|
|
362
361
|
|
|
363
362
|
|
|
@@ -374,14 +373,14 @@ def snapshot_tree(primary: Path) -> str:
|
|
|
374
373
|
env = dict(os.environ, GIT_INDEX_FILE=str(Path(temporary) / "index"))
|
|
375
374
|
for arguments in (("read-tree", "HEAD"), ("add", "-A"), ("write-tree",)):
|
|
376
375
|
completed = subprocess.run(("git", "-C", str(primary), *arguments), env=env,
|
|
377
|
-
|
|
376
|
+
encoding="utf-8", errors="replace", capture_output=True)
|
|
378
377
|
if completed.returncode:
|
|
379
378
|
raise DriverStop(f"git {' '.join(arguments)} failed: {completed.stderr.strip()}")
|
|
380
379
|
return completed.stdout.strip()
|
|
381
380
|
|
|
382
381
|
|
|
383
382
|
def run_verify(command: str, cwd: Path) -> Dict[str, object]:
|
|
384
|
-
completed = _common.run_command(
|
|
383
|
+
completed = _common.run_command(*_common.bash_argv(command), cwd=cwd)
|
|
385
384
|
return {"exit_code": completed.returncode, "stdout": completed.stdout, "stderr": completed.stderr}
|
|
386
385
|
|
|
387
386
|
|
|
@@ -394,6 +393,30 @@ def state_from_task(primary: Path, task_id: str) -> Dict[str, object]:
|
|
|
394
393
|
break
|
|
395
394
|
else:
|
|
396
395
|
raise DriverStop(f"task {task_id} has no task file")
|
|
396
|
+
member = fields.get("repo")
|
|
397
|
+
if member:
|
|
398
|
+
# A member activation lives in the live copy inside its member sidecar.
|
|
399
|
+
try:
|
|
400
|
+
checkout, project, _ = isolation._member_context(primary, str(member))
|
|
401
|
+
except isolation.IsolationError as error:
|
|
402
|
+
raise DriverStop(str(error), task=task_id) from error
|
|
403
|
+
task_file = task_path.relative_to(primary).as_posix()
|
|
404
|
+
sidecar = isolation.sidecar_root(checkout, "task", f"{project}-{task_id}").resolve()
|
|
405
|
+
copy = isolation.member_task_copy(sidecar, task_file)
|
|
406
|
+
live, _ = isolation.task_frontmatter(copy.read_text(encoding="utf-8")) if copy.is_file() else ({}, None)
|
|
407
|
+
if not live or live.get("status") != "in-progress" or not live.get("base") or not live.get("member_base"):
|
|
408
|
+
raise DriverStop(f"member task {task_id} has no activated live copy in {sidecar}", task=task_id)
|
|
409
|
+
return {"task_id": task_id, "title": str(live.get("title")), "task_file": copy.relative_to(sidecar).as_posix(),
|
|
410
|
+
"contract_file": task_file, "files": list(live.get("files") or []), "base": str(live["base"]),
|
|
411
|
+
"worktree": str(sidecar), "task_branch": f"{isolation.TASK_BRANCH_PREFIX}{project}-{task_id}",
|
|
412
|
+
"mode": "member", "member": str(member), "member_base": str(live["member_base"]),
|
|
413
|
+
"project_root": str(primary), "sidecar": None}
|
|
414
|
+
if fields.get("status") != "in-progress" or not fields.get("base"):
|
|
415
|
+
# A parallel activation lives only in its isolate; the primary copy stays pending.
|
|
416
|
+
isolate = isolation.sidecar_root(primary, "task", task_id).resolve()
|
|
417
|
+
if isolation.authorized_task_worktree(isolate, isolation.require_bound(primary)):
|
|
418
|
+
fields, _ = isolation.task_frontmatter(
|
|
419
|
+
(isolate / task_path.relative_to(primary)).read_text(encoding="utf-8"))
|
|
397
420
|
if fields.get("status") != "in-progress" or not fields.get("base"):
|
|
398
421
|
raise DriverStop(f"task {task_id} is not an in-progress task with a recorded base")
|
|
399
422
|
def value(key: str) -> Optional[str]:
|
|
@@ -453,7 +476,21 @@ def finish_task(primary: Path, state: Dict[str, object]) -> Dict[str, object]:
|
|
|
453
476
|
if not passed:
|
|
454
477
|
if state["mode"] == "parallel":
|
|
455
478
|
isolation.deactivate_task(worktree, task_id, str(state["task_branch"]))
|
|
479
|
+
elif state["mode"] == "member":
|
|
480
|
+
isolation.deactivate_member_task(primary, str(state["member"]), task_id)
|
|
456
481
|
raise DriverStop("Verify failed in the isolate", task=task_id, execution=execution)
|
|
482
|
+
if state["mode"] == "member":
|
|
483
|
+
member = str(state["member"])
|
|
484
|
+
landed = isolation.land_member(primary, member, task_id, str(state["title"]),
|
|
485
|
+
str(state["contract_file"]), base, str(state["member_base"]))
|
|
486
|
+
landing = str(landed["landing"])
|
|
487
|
+
# Only a landing whose parent is the member base proves the verified member tree.
|
|
488
|
+
ledger = isolation.git_output(worktree, "rev-parse", f"{landing}^") == str(state["member_base"])
|
|
489
|
+
if ledger:
|
|
490
|
+
build_state.verify_record(str(primary), command, landing, "pass", execution=execution, member=member)
|
|
491
|
+
isolation.retire_member_task(primary, member, task_id)
|
|
492
|
+
return {"task": task_id, "commit": str(landed["commit"]), "mode": "member",
|
|
493
|
+
"landing": landing, "verify": execution, "ledger": ledger}
|
|
457
494
|
landed = isolation.land(primary, worktree, base, task_id, str(state["title"]), task_file,
|
|
458
495
|
list(state["files"]))
|
|
459
496
|
commit = str(landed["commit"])
|
|
@@ -485,7 +522,8 @@ def recover_report(primary: Path, project_dir: str, receipt: Dict[str, object])
|
|
|
485
522
|
|
|
486
523
|
def checkpoint_project(primary: Path, receipt: Dict[str, object], subject: str, body: str) -> Optional[str]:
|
|
487
524
|
"""Commit .project bookkeeping through the canonical checkpoint; None when nothing is dirty."""
|
|
488
|
-
if not any(path.startswith(".project/") for path in isolation.uncommitted_paths(primary))
|
|
525
|
+
if not any(path.startswith(".project/") for path in isolation.uncommitted_paths(primary)) \
|
|
526
|
+
and not isolation.ignored_legacy_ledger(primary):
|
|
489
527
|
return None
|
|
490
528
|
result = isolation.checkpoint(primary, isolation.current_sha(primary), subject, body, [".project"])
|
|
491
529
|
receipt["steps"].append({"script": "isolation.py checkpoint", "result": result})
|
|
@@ -582,7 +620,7 @@ class Round:
|
|
|
582
620
|
"""Run the bundled token_budget.py against this milestone's ledger; blocked is a stop."""
|
|
583
621
|
command = [sys.executable, "-B", str(Path(__file__).resolve().parent / "token_budget.py"),
|
|
584
622
|
action, "--ledger", str(budget_ledger(self.primary)), *arguments]
|
|
585
|
-
completed = subprocess.run(command, cwd=self.primary, capture_output=True,
|
|
623
|
+
completed = subprocess.run(command, cwd=self.primary, capture_output=True, encoding="utf-8", errors="replace")
|
|
586
624
|
result = json.loads(completed.stdout) if completed.stdout.strip() else {}
|
|
587
625
|
self.receipt["steps"].append({"script": f"token_budget.py {action}", "command": command,
|
|
588
626
|
"exit_code": completed.returncode, "result": result,
|
|
@@ -605,12 +643,24 @@ class Round:
|
|
|
605
643
|
# recovery -------------------------------------------------------------
|
|
606
644
|
|
|
607
645
|
def recover(self) -> None:
|
|
646
|
+
# A journaled member landing is safe to finish; it never lands the same work twice.
|
|
647
|
+
journals = isolation.common_git_dir(self.primary).joinpath(*isolation.MEMBER_LANDING_DIR)
|
|
648
|
+
retired_members = set()
|
|
649
|
+
for journal in sorted(journals.glob("*.json")) if journals.is_dir() else []:
|
|
650
|
+
result = isolation.recover_member_landing(self.primary, journal.stem)
|
|
651
|
+
self.receipt["steps"].append({"script": "isolation.recover_member_landing", "task": journal.stem,
|
|
652
|
+
"result": result})
|
|
653
|
+
if result["state"] == "landed":
|
|
654
|
+
isolation.retire_member_task(self.primary, str(result["member"]), journal.stem)
|
|
655
|
+
retired_members.add(journal.stem)
|
|
608
656
|
report = recover_report(self.primary, self.project_dir, self.receipt)
|
|
609
657
|
for task in report["tasks"]:
|
|
610
658
|
worktree = task.get("worktree")
|
|
611
659
|
verdict = task["verdict"]
|
|
612
660
|
if verdict in ("recovered", "attested"):
|
|
613
661
|
self.proven[str(task.get("task_id"))] = str(task.get("commit"))
|
|
662
|
+
if verdict == "recovered" and task.get("member") and task.get("task_id") not in retired_members:
|
|
663
|
+
isolation.retire_member_task(self.primary, str(task["member"]), str(task["task_id"]))
|
|
614
664
|
if verdict == "recovered" and worktree and worktree["clean"] and task.get("task_branch"):
|
|
615
665
|
isolation.retire(self.primary, Path(str(worktree["path"])),
|
|
616
666
|
str(task["task_branch"]), False)
|
|
@@ -730,6 +780,8 @@ class Round:
|
|
|
730
780
|
if state.get("wave") != self.receipt["wave"]:
|
|
731
781
|
raise DriverStop("task is outside the requested wave", task=state["task_id"],
|
|
732
782
|
wave=state.get("wave"), requested_wave=self.receipt["wave"])
|
|
783
|
+
if state.get("mode") == "member":
|
|
784
|
+
isolation.ensure_member_hooks(self.primary, str(state["member"]), str(state["task_id"]))
|
|
733
785
|
fresh = spawn(self.root, state, self.options,
|
|
734
786
|
lambda fresh: brief_text(fresh, self.options.role_brief, self.options.task_template))
|
|
735
787
|
self.receipt["dispatched"].append(self.summary(fresh))
|
|
@@ -805,6 +857,28 @@ class Round:
|
|
|
805
857
|
self.runner("lint-round", head)
|
|
806
858
|
round_size = len(selected) + len(in_flight)
|
|
807
859
|
for task in selected:
|
|
860
|
+
if task.get("repo"):
|
|
861
|
+
member = str(task["repo"])
|
|
862
|
+
retained = isolation.retained_member_task(self.primary, member, task["id"],
|
|
863
|
+
task["task_file"], head)
|
|
864
|
+
if retained and retained["state"] == "unused":
|
|
865
|
+
isolation.retire_member_task(self.primary, member, task["id"])
|
|
866
|
+
retained = None
|
|
867
|
+
if retained:
|
|
868
|
+
isolate, active = retained["isolate"], retained["active"]
|
|
869
|
+
else:
|
|
870
|
+
isolate = isolation.isolate_member_task(self.primary, member, task["id"])
|
|
871
|
+
active = isolation.activate_member_task(self.primary, member, task["id"],
|
|
872
|
+
f"build_{task['id'].lower()}", task["task_file"], head)
|
|
873
|
+
sidecar_path = Path(str(active["worktree"]))
|
|
874
|
+
self.launch({"task_id": task["id"], "title": task["title"],
|
|
875
|
+
"task_file": Path(str(active["copy"])).relative_to(sidecar_path).as_posix(),
|
|
876
|
+
"contract_file": task["task_file"], "files": task["files"], "wave": task["wave"],
|
|
877
|
+
"verify_heavy": task["verify_heavy"], "base": head, "worktree": str(sidecar_path),
|
|
878
|
+
"task_branch": isolate["task_branch"], "mode": "member", "member": member,
|
|
879
|
+
"member_base": isolate["member_base"], "project_root": str(self.primary),
|
|
880
|
+
"sidecar": None, "answered": False, "model_selection": task['model_selection']})
|
|
881
|
+
continue
|
|
808
882
|
prepared = self.runner("prepare-task", head, task["id"], round_size)
|
|
809
883
|
isolate = prepared[0]["result"]
|
|
810
884
|
sidecar = prepared[1]["result"] if len(prepared) > 1 else None
|
|
@@ -1178,7 +1252,7 @@ class Review:
|
|
|
1178
1252
|
def helper_json(receipt: Dict[str, object], script: str, *arguments: str, cwd: Path) -> Dict[str, object]:
|
|
1179
1253
|
"""Run a bundled helper and return its JSON stdout; a non-zero exit is a typed stop."""
|
|
1180
1254
|
command = [sys.executable, "-B", str(Path(__file__).resolve().parent / script), *arguments]
|
|
1181
|
-
completed = subprocess.run(command, cwd=cwd, capture_output=True,
|
|
1255
|
+
completed = subprocess.run(command, cwd=cwd, capture_output=True, encoding="utf-8", errors="replace")
|
|
1182
1256
|
step = {"script": script, "command": command, "exit_code": completed.returncode,
|
|
1183
1257
|
"stdout": completed.stdout, "stderr": completed.stderr}
|
|
1184
1258
|
receipt["steps"].append(step)
|
|
@@ -1746,7 +1820,7 @@ Heavy: {'yes' if heavy else 'no'}
|
|
|
1746
1820
|
receipt["plan_wave"] = fix_wave
|
|
1747
1821
|
lint = subprocess.run([sys.executable, "-B", str(Path(__file__).resolve().parent / "check_task_briefs.py"),
|
|
1748
1822
|
"--repo", str(primary), "--base", isolation.current_sha(primary),
|
|
1749
|
-
"--tasks-dir", f"{options.project_dir}/tasks"], capture_output=True,
|
|
1823
|
+
"--tasks-dir", f"{options.project_dir}/tasks"], capture_output=True, encoding="utf-8", errors="replace")
|
|
1750
1824
|
receipt["steps"].append({"script": "check_task_briefs.py", "exit_code": lint.returncode,
|
|
1751
1825
|
"stdout": lint.stdout, "stderr": lint.stderr})
|
|
1752
1826
|
if lint.returncode:
|
|
@@ -1981,6 +2055,17 @@ def main(argv=None) -> int:
|
|
|
1981
2055
|
current.recover()
|
|
1982
2056
|
records = [state for state in latest_states(current.root)
|
|
1983
2057
|
if state["task_id"] == arguments.task_id]
|
|
2058
|
+
native = None
|
|
2059
|
+
if records and records[0].get("outcome") is not None:
|
|
2060
|
+
# A settled driver record at an older base is superseded by a native retry.
|
|
2061
|
+
try:
|
|
2062
|
+
native = state_from_task(primary, arguments.task_id)
|
|
2063
|
+
except DriverStop:
|
|
2064
|
+
pass
|
|
2065
|
+
if native and native["base"] != records[0].get("base"):
|
|
2066
|
+
records = []
|
|
2067
|
+
else:
|
|
2068
|
+
native = None
|
|
1984
2069
|
if arguments.task_id in current.proven:
|
|
1985
2070
|
commit = current.proven[arguments.task_id]
|
|
1986
2071
|
if records and records[0].get("outcome") is None:
|
|
@@ -1997,7 +2082,7 @@ def main(argv=None) -> int:
|
|
|
1997
2082
|
current.classify(record)
|
|
1998
2083
|
else: # a coder dispatched by hand: derive the isolate from the task frontmatter
|
|
1999
2084
|
current.receipt["landed"].append(
|
|
2000
|
-
finish_task(primary, state_from_task(primary, arguments.task_id)))
|
|
2085
|
+
finish_task(primary, native or state_from_task(primary, arguments.task_id)))
|
|
2001
2086
|
result = {**current.receipt, "status": "landed" if current.receipt["landed"] else "blocked"}
|
|
2002
2087
|
elif arguments.action == "answer":
|
|
2003
2088
|
result = answer(primary, arguments.task_id, arguments.answer)
|
package/scripts/git_guard.py
CHANGED
|
@@ -6,6 +6,7 @@ hook wiring. Inspection failures and violations both exit 1: unreadable Git
|
|
|
6
6
|
state cannot prove archive immutability or safe publication.
|
|
7
7
|
"""
|
|
8
8
|
|
|
9
|
+
import json
|
|
9
10
|
import re
|
|
10
11
|
import subprocess
|
|
11
12
|
from functools import lru_cache
|
|
@@ -19,8 +20,9 @@ sys.path.insert(0, str(_HERE / "runtime" if (_HERE / "runtime" / "isolation.py")
|
|
|
19
20
|
sys.dont_write_bytecode = True # a hook must not leave __pycache__ in the worktree
|
|
20
21
|
try:
|
|
21
22
|
from isolation import BOOKKEEPING_PREFIXES, NULL_SHA, _landing_state, task_frontmatter
|
|
22
|
-
from pipeline_git import is_ship_subject, task_commit_body
|
|
23
|
+
from pipeline_git import is_ship_subject, ship_commit_body, task_commit_body
|
|
23
24
|
from pipeline_state import _completion_status
|
|
25
|
+
import members
|
|
24
26
|
except ImportError as error: # pragma: no cover - broken install
|
|
25
27
|
print(
|
|
26
28
|
f"gsd-path guard: pipeline runtime is missing ({error}); commit blocked; "
|
|
@@ -57,6 +59,10 @@ PRODUCT_HINT = (
|
|
|
57
59
|
f"while this worktree carries unfinished milestone state, {LANDING_HINT}; "
|
|
58
60
|
"findings during ship reopen through the patch plan"
|
|
59
61
|
)
|
|
62
|
+
MEMBER_PUSH_HINT = (
|
|
63
|
+
"a member publishes its coordinator's Path refs, or work from them, only with "
|
|
64
|
+
"a coordinator authorization for that exact ref and object"
|
|
65
|
+
)
|
|
60
66
|
PUBLICATION_HINT = (
|
|
61
67
|
"unshipped milestone work reaches a remote only as its ship commit "
|
|
62
68
|
"(STATE shipped/done) through archive_milestone.py integrate; findings during "
|
|
@@ -84,7 +90,7 @@ def staged_entries():
|
|
|
84
90
|
output = subprocess.run(
|
|
85
91
|
["git", "diff", "--cached", "--name-status", "-z", "--find-renames"],
|
|
86
92
|
capture_output=True,
|
|
87
|
-
|
|
93
|
+
encoding="utf-8", errors="replace",
|
|
88
94
|
check=True,
|
|
89
95
|
).stdout
|
|
90
96
|
tokens = output.split("\0")
|
|
@@ -139,7 +145,7 @@ def committed_archive_roots():
|
|
|
139
145
|
head = subprocess.run(
|
|
140
146
|
["git", "rev-parse", "--verify", "--quiet", "HEAD^{commit}"],
|
|
141
147
|
capture_output=True,
|
|
142
|
-
|
|
148
|
+
encoding="utf-8", errors="replace",
|
|
143
149
|
check=False,
|
|
144
150
|
)
|
|
145
151
|
if head.returncode == 1:
|
|
@@ -148,7 +154,7 @@ def committed_archive_roots():
|
|
|
148
154
|
output = subprocess.run(
|
|
149
155
|
["git", "ls-tree", "-r", "--name-only", "HEAD", "--", ARCHIVE_PREFIX],
|
|
150
156
|
capture_output=True,
|
|
151
|
-
|
|
157
|
+
encoding="utf-8", errors="replace",
|
|
152
158
|
check=True,
|
|
153
159
|
).stdout
|
|
154
160
|
return {
|
|
@@ -162,7 +168,7 @@ def staged_file(path):
|
|
|
162
168
|
return subprocess.run(
|
|
163
169
|
["git", "show", f":{path}"],
|
|
164
170
|
capture_output=True,
|
|
165
|
-
|
|
171
|
+
encoding="utf-8", errors="replace",
|
|
166
172
|
check=True,
|
|
167
173
|
).stdout
|
|
168
174
|
|
|
@@ -176,7 +182,7 @@ def shown_file(revspec):
|
|
|
176
182
|
shown = subprocess.run(
|
|
177
183
|
["git", "show", revspec],
|
|
178
184
|
capture_output=True,
|
|
179
|
-
|
|
185
|
+
encoding="utf-8", errors="replace",
|
|
180
186
|
check=False,
|
|
181
187
|
)
|
|
182
188
|
return shown.stdout if shown.returncode == 0 else None
|
|
@@ -187,7 +193,7 @@ def state_at(revision):
|
|
|
187
193
|
subprocess.run(
|
|
188
194
|
["git", "cat-file", "-e", revision],
|
|
189
195
|
capture_output=True,
|
|
190
|
-
|
|
196
|
+
encoding="utf-8", errors="replace",
|
|
191
197
|
check=True,
|
|
192
198
|
)
|
|
193
199
|
content = shown_file(f"{revision}:.project/STATE.md")
|
|
@@ -288,12 +294,25 @@ def ship_contract_violations(entries, subject, body, new_archives, state):
|
|
|
288
294
|
reviewed_head = subprocess.run(
|
|
289
295
|
["git", "rev-parse", "--verify", "HEAD"],
|
|
290
296
|
capture_output=True,
|
|
291
|
-
|
|
297
|
+
encoding="utf-8", errors="replace",
|
|
292
298
|
check=True,
|
|
293
299
|
).stdout.strip()
|
|
294
|
-
|
|
300
|
+
lock = f"{archive}/build/members.json"
|
|
301
|
+
staged_lock = subprocess.run(
|
|
302
|
+
["git", "ls-files", "--cached", "--", lock],
|
|
303
|
+
capture_output=True,
|
|
304
|
+
encoding="utf-8", errors="replace",
|
|
305
|
+
check=True,
|
|
306
|
+
).stdout
|
|
307
|
+
locked = [entry["name"] for entry in json.loads(staged_file(lock))["members"]] if staged_lock else []
|
|
308
|
+
rows = members.read_member_close(
|
|
309
|
+
Path(git_output("rev-parse", "--path-format=absolute", "--git-common-dir")), Path(archive).name
|
|
310
|
+
) if locked else []
|
|
311
|
+
if [row["name"] for row in rows] != locked or any(row["status"] != "integrated" for row in rows):
|
|
312
|
+
found.append("ship commit requires every locked member closed; run archive_milestone.py close-members")
|
|
313
|
+
expected_body = ship_commit_body(archive, reviewed_head, rows).rstrip("\n")
|
|
295
314
|
if body != expected_body:
|
|
296
|
-
found.append("ship commit body does not match Archive
|
|
315
|
+
found.append("ship commit body does not match Archive, Reviewed-HEAD, and closed members")
|
|
297
316
|
changed_paths = {
|
|
298
317
|
new if code in "CR" else old
|
|
299
318
|
for code, old, new in entries
|
|
@@ -383,11 +402,15 @@ def staged_abandon_target(new_archives, state):
|
|
|
383
402
|
|
|
384
403
|
|
|
385
404
|
@lru_cache(maxsize=None)
|
|
405
|
+
def git_output(*arguments):
|
|
406
|
+
return subprocess.run(["git", *arguments], capture_output=True, encoding="utf-8", errors="replace", check=True).stdout.strip()
|
|
407
|
+
|
|
408
|
+
|
|
386
409
|
def current_branch():
|
|
387
410
|
return subprocess.run(
|
|
388
411
|
["git", "branch", "--show-current"],
|
|
389
412
|
capture_output=True,
|
|
390
|
-
|
|
413
|
+
encoding="utf-8", errors="replace",
|
|
391
414
|
check=True,
|
|
392
415
|
).stdout.strip()
|
|
393
416
|
|
|
@@ -398,7 +421,7 @@ def closed_milestone_reason():
|
|
|
398
421
|
return None
|
|
399
422
|
commits = subprocess.run(
|
|
400
423
|
["git", "log", "--format=%H", "--grep=^ship:", "HEAD"],
|
|
401
|
-
capture_output=True,
|
|
424
|
+
capture_output=True, encoding="utf-8", errors="replace", check=True,
|
|
402
425
|
).stdout.splitlines()
|
|
403
426
|
if any(ship_commit_at(sha, branch) for sha in commits):
|
|
404
427
|
return (f"closed milestone {branch} accepts no new work; finish integration "
|
|
@@ -411,7 +434,7 @@ def head_descends_from(commit):
|
|
|
411
434
|
subprocess.run(
|
|
412
435
|
["git", "merge-base", "--is-ancestor", commit, "HEAD"],
|
|
413
436
|
capture_output=True,
|
|
414
|
-
|
|
437
|
+
encoding="utf-8", errors="replace",
|
|
415
438
|
check=False,
|
|
416
439
|
).returncode
|
|
417
440
|
== 0
|
|
@@ -429,7 +452,7 @@ def repo_root():
|
|
|
429
452
|
subprocess.run(
|
|
430
453
|
["git", "rev-parse", "--show-toplevel"],
|
|
431
454
|
capture_output=True,
|
|
432
|
-
|
|
455
|
+
encoding="utf-8", errors="replace",
|
|
433
456
|
check=True,
|
|
434
457
|
).stdout.strip()
|
|
435
458
|
)
|
|
@@ -505,7 +528,7 @@ def ship_commit_at(sha, bound):
|
|
|
505
528
|
shown = subprocess.run(
|
|
506
529
|
["git", "show", "--no-patch", "--format=%s", sha],
|
|
507
530
|
capture_output=True,
|
|
508
|
-
|
|
531
|
+
encoding="utf-8", errors="replace",
|
|
509
532
|
check=True,
|
|
510
533
|
)
|
|
511
534
|
archive = state.get("archive") or ""
|
|
@@ -514,13 +537,7 @@ def ship_commit_at(sha, bound):
|
|
|
514
537
|
)
|
|
515
538
|
|
|
516
539
|
|
|
517
|
-
def
|
|
518
|
-
"""Refuse ref updates that publish unshipped milestone work.
|
|
519
|
-
|
|
520
|
-
A bound-named ref moves only to, or away from, its strict ship commit. Any
|
|
521
|
-
other ref may not carry a commit whose STATE.md still owes a ship commit.
|
|
522
|
-
"""
|
|
523
|
-
found = []
|
|
540
|
+
def _push_updates(lines):
|
|
524
541
|
for line in lines:
|
|
525
542
|
parts = line.split()
|
|
526
543
|
if len(parts) != 4:
|
|
@@ -528,6 +545,60 @@ def pre_push_violations(lines):
|
|
|
528
545
|
local_ref, local_sha, remote_ref, remote_sha = parts
|
|
529
546
|
if any(re.fullmatch(r"[0-9a-fA-F]{40}", sha) is None for sha in (local_sha, remote_sha)):
|
|
530
547
|
raise ValueError(f"unexpected pre-push SHA: {line!r}")
|
|
548
|
+
yield local_ref, local_sha, remote_ref, remote_sha
|
|
549
|
+
|
|
550
|
+
|
|
551
|
+
def _commits(*arguments):
|
|
552
|
+
listed = subprocess.run(
|
|
553
|
+
["git", "rev-list", *arguments], capture_output=True, encoding="utf-8", errors="replace", check=True
|
|
554
|
+
)
|
|
555
|
+
return set(listed.stdout.split())
|
|
556
|
+
|
|
557
|
+
|
|
558
|
+
def member_pre_push_violations(lines, role):
|
|
559
|
+
"""Member mode: the member's own STATE is ignored; coordinator refs need authorization."""
|
|
560
|
+
checkout, project = repo_root(), role["project"]
|
|
561
|
+
reserved = members.member_ref_prefixes(project)
|
|
562
|
+
members.require_origin_main(checkout)
|
|
563
|
+
exclude = ["--not", "refs/remotes/origin/main"]
|
|
564
|
+
member_work = None
|
|
565
|
+
found = []
|
|
566
|
+
for _, local_sha, remote_ref, remote_sha in _push_updates(lines):
|
|
567
|
+
if local_sha == NULL_SHA:
|
|
568
|
+
if remote_ref.startswith(reserved) and not members.authorized(
|
|
569
|
+
checkout, project, "delete", remote_ref, remote_sha
|
|
570
|
+
):
|
|
571
|
+
found.append(f"{remote_ref} deletion is not authorized by {project}; {MEMBER_PUSH_HINT}")
|
|
572
|
+
continue
|
|
573
|
+
if members.authorized(checkout, project, "push", remote_ref, local_sha):
|
|
574
|
+
continue
|
|
575
|
+
if remote_ref.startswith(reserved):
|
|
576
|
+
found.append(
|
|
577
|
+
f"{remote_ref} <- {local_sha[:12]} is not authorized by {project}; {MEMBER_PUSH_HINT}"
|
|
578
|
+
)
|
|
579
|
+
continue
|
|
580
|
+
if member_work is None:
|
|
581
|
+
bound = [ref for ref in subprocess.run(
|
|
582
|
+
["git", "for-each-ref", "--format=%(refname)", "refs/heads/gsd-path/"],
|
|
583
|
+
capture_output=True, encoding="utf-8", errors="replace", check=True,
|
|
584
|
+
).stdout.split() if ref.startswith(f"refs/heads/gsd-path/{project}-")]
|
|
585
|
+
member_work = _commits(*bound, *exclude) if bound else set()
|
|
586
|
+
if member_work & _commits(local_sha, *exclude):
|
|
587
|
+
found.append(
|
|
588
|
+
f"{remote_ref} <- {local_sha[:12]} carries unpublished {project} member work; "
|
|
589
|
+
f"{MEMBER_PUSH_HINT}"
|
|
590
|
+
)
|
|
591
|
+
return found
|
|
592
|
+
|
|
593
|
+
|
|
594
|
+
def pre_push_violations(lines):
|
|
595
|
+
"""Refuse ref updates that publish unshipped milestone work.
|
|
596
|
+
|
|
597
|
+
A bound-named ref moves only to, or away from, its strict ship commit. Any
|
|
598
|
+
other ref may not carry a commit whose STATE.md still owes a ship commit.
|
|
599
|
+
"""
|
|
600
|
+
found = []
|
|
601
|
+
for local_ref, local_sha, remote_ref, remote_sha in _push_updates(lines):
|
|
531
602
|
names = [ref.removeprefix("refs/heads/") for ref in (local_ref, remote_ref)]
|
|
532
603
|
bounds = {name for name in names if BOUND_BRANCH.fullmatch(name)}
|
|
533
604
|
if bounds:
|
|
@@ -574,7 +645,7 @@ def default_branch():
|
|
|
574
645
|
origin_head = subprocess.run(
|
|
575
646
|
["git", "symbolic-ref", "--quiet", "--short", "refs/remotes/origin/HEAD"],
|
|
576
647
|
capture_output=True,
|
|
577
|
-
|
|
648
|
+
encoding="utf-8", errors="replace",
|
|
578
649
|
check=False,
|
|
579
650
|
)
|
|
580
651
|
if origin_head.returncode == 0 and origin_head.stdout.strip().startswith("origin/"):
|
|
@@ -582,7 +653,7 @@ def default_branch():
|
|
|
582
653
|
configured = subprocess.run(
|
|
583
654
|
["git", "config", "--get", "init.defaultBranch"],
|
|
584
655
|
capture_output=True,
|
|
585
|
-
|
|
656
|
+
encoding="utf-8", errors="replace",
|
|
586
657
|
check=False,
|
|
587
658
|
)
|
|
588
659
|
if configured.returncode == 0 and configured.stdout.strip():
|
|
@@ -604,7 +675,7 @@ def is_integration_merge(subject):
|
|
|
604
675
|
merge_head = subprocess.run(
|
|
605
676
|
["git", "rev-parse", "--verify", "--quiet", "MERGE_HEAD^{commit}"],
|
|
606
677
|
capture_output=True,
|
|
607
|
-
|
|
678
|
+
encoding="utf-8", errors="replace",
|
|
608
679
|
check=False,
|
|
609
680
|
)
|
|
610
681
|
bound_head = subprocess.run(
|
|
@@ -616,7 +687,7 @@ def is_integration_merge(subject):
|
|
|
616
687
|
f"refs/heads/{match.group('branch')}^{{commit}}",
|
|
617
688
|
],
|
|
618
689
|
capture_output=True,
|
|
619
|
-
|
|
690
|
+
encoding="utf-8", errors="replace",
|
|
620
691
|
check=False,
|
|
621
692
|
)
|
|
622
693
|
return (
|
|
@@ -708,18 +779,34 @@ def report(found, action):
|
|
|
708
779
|
|
|
709
780
|
|
|
710
781
|
def main(argv):
|
|
782
|
+
# Git reads hook output as UTF-8; a Windows pipe would default to cp1252.
|
|
783
|
+
# In-process callers may substitute text streams.
|
|
784
|
+
for stream in (sys.stdout, sys.stderr):
|
|
785
|
+
if hasattr(stream, "reconfigure"):
|
|
786
|
+
stream.reconfigure(encoding="utf-8", errors="replace")
|
|
711
787
|
if len(argv) > 1 and argv[1] == "closed-milestone":
|
|
712
788
|
reason = closed_milestone_reason()
|
|
713
789
|
return report([reason] if reason else [], "mutation")
|
|
714
790
|
if len(argv) > 1 and argv[1] == "pre-push":
|
|
715
791
|
try:
|
|
716
|
-
|
|
792
|
+
stdin = getattr(sys.stdin, "buffer", None)
|
|
793
|
+
text = (stdin.read().decode("utf-8", errors="surrogateescape")
|
|
794
|
+
if stdin is not None else sys.stdin.read())
|
|
795
|
+
lines = text.splitlines()
|
|
796
|
+
role = members.member_role(repo_root())
|
|
797
|
+
found = member_pre_push_violations(lines, role) if role else pre_push_violations(lines)
|
|
717
798
|
except Exception as error:
|
|
718
799
|
print(f"gsd-path guard: inspection failed; push blocked ({error})", file=sys.stderr)
|
|
719
800
|
return 1
|
|
720
801
|
return report(found, "push")
|
|
721
802
|
try:
|
|
722
|
-
|
|
803
|
+
# A member follows its coordinator, so its own STATE does not gate commits.
|
|
804
|
+
role = members.member_role(repo_root())
|
|
805
|
+
member = role is not None
|
|
806
|
+
if member and current_branch().startswith(f"gsd-path/{role['project']}-"):
|
|
807
|
+
return report([f"direct commits on the member bound branch {current_branch()} are refused; "
|
|
808
|
+
"member tasks land there through Path"], "commit")
|
|
809
|
+
reason = None if member else closed_milestone_reason()
|
|
723
810
|
if reason:
|
|
724
811
|
return report([reason], "commit")
|
|
725
812
|
entries = staged_entries()
|
|
@@ -740,7 +827,7 @@ def main(argv):
|
|
|
740
827
|
)
|
|
741
828
|
if abandon is not None:
|
|
742
829
|
current_archive = abandon[0]
|
|
743
|
-
landing = product_commit_violations(entries, subject, body)
|
|
830
|
+
landing = [] if member else product_commit_violations(entries, subject, body)
|
|
744
831
|
except Exception as error:
|
|
745
832
|
print(
|
|
746
833
|
f"gsd-path guard: inspection failed; commit blocked ({error}); "
|