@opengsd/gsd-path 1.0.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 +397 -0
- package/DOCS.md +341 -0
- package/FULL.md +475 -0
- package/GUIDE.md +23 -0
- package/HOOKS.md +273 -0
- package/LICENSE +21 -0
- package/MIGRATE.md +100 -0
- package/QUICK.md +146 -0
- package/README.md +280 -0
- package/RUNTIME.md +229 -0
- package/UPDATE.md +196 -0
- package/WORKFLOW.md +683 -0
- package/package.json +80 -0
- package/platforms/claude/dispatch.md +63 -0
- package/platforms/copilot/dispatch.md +61 -0
- package/platforms/cursor/agent.md +17 -0
- package/platforms/cursor/dispatch.md +63 -0
- package/platforms/grok/dispatch.md +61 -0
- package/platforms/kimi/dispatch.md +64 -0
- package/platforms/kiro/dispatch.md +58 -0
- package/platforms/opencode/dispatch.md +62 -0
- package/platforms/qwen/dispatch.md +60 -0
- package/platforms/shared-agents/dispatch.md +131 -0
- package/plugin.json +14 -0
- package/scripts/_common.py +180 -0
- package/scripts/archive_milestone.py +2265 -0
- package/scripts/bootstrap_repository.py +710 -0
- package/scripts/build_state.py +854 -0
- package/scripts/check_docs_audit.py +463 -0
- package/scripts/check_handoffs.py +1855 -0
- package/scripts/check_task_briefs.py +376 -0
- package/scripts/check_update.py +93 -0
- package/scripts/core_hook_gate.py +34 -0
- package/scripts/core_hook_settings.py +119 -0
- package/scripts/detect_project.py +1517 -0
- package/scripts/discussion_records.py +619 -0
- package/scripts/discussion_validate.py +1321 -0
- package/scripts/dispatch_driver.py +1812 -0
- package/scripts/git_guard.py +759 -0
- package/scripts/guard_hook.py +2124 -0
- package/scripts/install.mjs +3239 -0
- package/scripts/install.py +3008 -0
- package/scripts/integration.py +1433 -0
- package/scripts/isolation.py +3538 -0
- package/scripts/lean_verification.py +256 -0
- package/scripts/loop_run.py +837 -0
- package/scripts/migrate_core.py +169 -0
- package/scripts/pipeline_diagnose.py +715 -0
- package/scripts/pipeline_git.py +940 -0
- package/scripts/pipeline_state.py +2291 -0
- package/scripts/pipeline_undo.py +1039 -0
- package/scripts/promote_lookahead.py +415 -0
- package/scripts/review_findings.py +714 -0
- package/scripts/review_panel.py +553 -0
- package/scripts/skill-resources.json +340 -0
- package/scripts/state_checkpoint.py +1039 -0
- package/scripts/state_promote.py +743 -0
- package/scripts/status_runtime.py +122 -0
- package/scripts/sync_skill_resources.py +259 -0
- package/scripts/token_budget.py +146 -0
- package/scripts/wizard.mjs +179 -0
- package/scripts/workflow_run.py +117 -0
- package/skills/gsd-path/BUILD.md +700 -0
- package/skills/gsd-path/DECIDE.md +115 -0
- package/skills/gsd-path/DEFINE.md +297 -0
- package/skills/gsd-path/DOCS-AUDIT.md +190 -0
- package/skills/gsd-path/INSPECT.md +167 -0
- package/skills/gsd-path/PLAN.md +411 -0
- package/skills/gsd-path/RESEARCH.md +159 -0
- package/skills/gsd-path/ROADMAP.md +215 -0
- package/skills/gsd-path/SHIP.md +412 -0
- package/skills/gsd-path/SKILL.md +471 -0
- package/skills/gsd-path/agents/openai.yaml +6 -0
- package/skills/gsd-path/references/codebase-mapper.md +52 -0
- package/skills/gsd-path/references/coder.md +90 -0
- package/skills/gsd-path/references/decider.md +36 -0
- package/skills/gsd-path/references/dispatch.md +131 -0
- package/skills/gsd-path/references/docs-auditor.md +60 -0
- package/skills/gsd-path/references/planner.md +104 -0
- package/skills/gsd-path/references/researcher.md +42 -0
- package/skills/gsd-path/references/reviewer.md +188 -0
- package/skills/gsd-path/references/roadmapper.md +48 -0
- package/skills/gsd-path/scripts/_common.py +180 -0
- package/skills/gsd-path/scripts/archive_milestone.py +2265 -0
- package/skills/gsd-path/scripts/bootstrap_repository.py +710 -0
- package/skills/gsd-path/scripts/build_state.py +854 -0
- package/skills/gsd-path/scripts/check_docs_audit.py +463 -0
- package/skills/gsd-path/scripts/check_handoffs.py +1855 -0
- package/skills/gsd-path/scripts/check_task_briefs.py +376 -0
- package/skills/gsd-path/scripts/check_update.py +93 -0
- package/skills/gsd-path/scripts/detect_project.py +1517 -0
- package/skills/gsd-path/scripts/discussion_records.py +619 -0
- package/skills/gsd-path/scripts/discussion_validate.py +1321 -0
- package/skills/gsd-path/scripts/dispatch_driver.py +1812 -0
- package/skills/gsd-path/scripts/integration.py +1433 -0
- package/skills/gsd-path/scripts/isolation.py +3538 -0
- package/skills/gsd-path/scripts/lean_verification.py +256 -0
- package/skills/gsd-path/scripts/loop_run.py +837 -0
- package/skills/gsd-path/scripts/pipeline_git.py +940 -0
- package/skills/gsd-path/scripts/pipeline_state.py +2291 -0
- package/skills/gsd-path/scripts/promote_lookahead.py +415 -0
- package/skills/gsd-path/scripts/review_findings.py +714 -0
- package/skills/gsd-path/scripts/review_panel.py +553 -0
- package/skills/gsd-path/scripts/state_checkpoint.py +1039 -0
- package/skills/gsd-path/scripts/state_promote.py +743 -0
- package/skills/gsd-path/scripts/token_budget.py +146 -0
- package/skills/gsd-path/scripts/workflow_run.py +117 -0
- package/skills/gsd-path/templates/answers.md +29 -0
- package/skills/gsd-path/templates/archive-manifest.md +37 -0
- package/skills/gsd-path/templates/charter.md +50 -0
- package/skills/gsd-path/templates/codebase.md +46 -0
- package/skills/gsd-path/templates/dialogue.md +21 -0
- package/skills/gsd-path/templates/docs-audit.md +69 -0
- package/skills/gsd-path/templates/evidence.md +24 -0
- package/skills/gsd-path/templates/final-review.md +27 -0
- package/skills/gsd-path/templates/gap-review.md +20 -0
- package/skills/gsd-path/templates/intent.md +98 -0
- package/skills/gsd-path/templates/loop.md +59 -0
- package/skills/gsd-path/templates/patch-findings.md +16 -0
- package/skills/gsd-path/templates/plan-panel.md +25 -0
- package/skills/gsd-path/templates/plan.md +89 -0
- package/skills/gsd-path/templates/repository.md +16 -0
- package/skills/gsd-path/templates/research-handoff.md +25 -0
- package/skills/gsd-path/templates/roadmap.md +38 -0
- package/skills/gsd-path/templates/skeptic.md +36 -0
- package/skills/gsd-path/templates/state.md +27 -0
- package/skills/gsd-path/templates/synthesis.md +36 -0
- package/skills/gsd-path/templates/task.md +74 -0
- package/skills/gsd-path/templates/wave-panel.md +25 -0
- package/skills/gsd-path/templates/wave-review.md +64 -0
- package/skills/gsd-path-build/SKILL.md +700 -0
- package/skills/gsd-path-build/agents/openai.yaml +6 -0
- package/skills/gsd-path-build/references/coder.md +90 -0
- package/skills/gsd-path-build/references/dispatch.md +131 -0
- package/skills/gsd-path-build/references/reviewer.md +188 -0
- package/skills/gsd-path-build/scripts/_common.py +180 -0
- package/skills/gsd-path-build/scripts/archive_milestone.py +2265 -0
- package/skills/gsd-path-build/scripts/build_state.py +854 -0
- package/skills/gsd-path-build/scripts/check_handoffs.py +1855 -0
- package/skills/gsd-path-build/scripts/check_task_briefs.py +376 -0
- package/skills/gsd-path-build/scripts/discussion_records.py +619 -0
- package/skills/gsd-path-build/scripts/discussion_validate.py +1321 -0
- package/skills/gsd-path-build/scripts/dispatch_driver.py +1812 -0
- package/skills/gsd-path-build/scripts/integration.py +1433 -0
- package/skills/gsd-path-build/scripts/isolation.py +3538 -0
- package/skills/gsd-path-build/scripts/loop_run.py +837 -0
- package/skills/gsd-path-build/scripts/pipeline_git.py +940 -0
- package/skills/gsd-path-build/scripts/pipeline_state.py +2291 -0
- package/skills/gsd-path-build/scripts/review_findings.py +714 -0
- package/skills/gsd-path-build/scripts/review_panel.py +553 -0
- package/skills/gsd-path-build/scripts/state_checkpoint.py +1039 -0
- package/skills/gsd-path-build/scripts/state_promote.py +743 -0
- package/skills/gsd-path-build/scripts/token_budget.py +146 -0
- package/skills/gsd-path-build/scripts/workflow_run.py +117 -0
- package/skills/gsd-path-build/templates/skeptic.md +36 -0
- package/skills/gsd-path-build/templates/task.md +74 -0
- package/skills/gsd-path-build/templates/wave-panel.md +25 -0
- package/skills/gsd-path-build/templates/wave-review.md +64 -0
- package/skills/gsd-path-decide/SKILL.md +115 -0
- package/skills/gsd-path-decide/agents/openai.yaml +6 -0
- package/skills/gsd-path-decide/references/decider.md +36 -0
- package/skills/gsd-path-decide/references/dispatch.md +131 -0
- package/skills/gsd-path-decide/scripts/_common.py +180 -0
- package/skills/gsd-path-decide/scripts/check_handoffs.py +1855 -0
- package/skills/gsd-path-decide/scripts/check_task_briefs.py +376 -0
- package/skills/gsd-path-decide/scripts/discussion_records.py +619 -0
- package/skills/gsd-path-decide/scripts/isolation.py +3538 -0
- package/skills/gsd-path-decide/scripts/pipeline_git.py +940 -0
- package/skills/gsd-path-decide/scripts/pipeline_state.py +2291 -0
- package/skills/gsd-path-decide/scripts/state_checkpoint.py +1039 -0
- package/skills/gsd-path-decide/scripts/state_promote.py +743 -0
- package/skills/gsd-path-decide/templates/synthesis.md +36 -0
- package/skills/gsd-path-define/SKILL.md +297 -0
- package/skills/gsd-path-define/agents/openai.yaml +6 -0
- package/skills/gsd-path-define/scripts/_common.py +180 -0
- package/skills/gsd-path-define/scripts/check_handoffs.py +1855 -0
- package/skills/gsd-path-define/scripts/check_task_briefs.py +376 -0
- package/skills/gsd-path-define/scripts/detect_project.py +1517 -0
- package/skills/gsd-path-define/scripts/discussion_records.py +619 -0
- package/skills/gsd-path-define/scripts/isolation.py +3538 -0
- package/skills/gsd-path-define/scripts/pipeline_git.py +940 -0
- package/skills/gsd-path-define/scripts/pipeline_state.py +2291 -0
- package/skills/gsd-path-define/scripts/state_checkpoint.py +1039 -0
- package/skills/gsd-path-define/scripts/state_promote.py +743 -0
- package/skills/gsd-path-define/templates/charter.md +50 -0
- package/skills/gsd-path-define/templates/intent.md +98 -0
- package/skills/gsd-path-define/templates/state.md +27 -0
- package/skills/gsd-path-discuss/SKILL.md +162 -0
- package/skills/gsd-path-discuss/agents/openai.yaml +6 -0
- package/skills/gsd-path-discuss/scripts/_common.py +180 -0
- package/skills/gsd-path-discuss/scripts/discussion_records.py +619 -0
- package/skills/gsd-path-discuss/templates/answers.md +29 -0
- package/skills/gsd-path-discuss/templates/dialogue.md +21 -0
- package/skills/gsd-path-docs-audit/SKILL.md +190 -0
- package/skills/gsd-path-docs-audit/agents/openai.yaml +6 -0
- package/skills/gsd-path-docs-audit/references/dispatch.md +131 -0
- package/skills/gsd-path-docs-audit/references/docs-auditor.md +60 -0
- package/skills/gsd-path-docs-audit/scripts/_common.py +180 -0
- package/skills/gsd-path-docs-audit/scripts/check_docs_audit.py +463 -0
- package/skills/gsd-path-docs-audit/scripts/check_handoffs.py +1855 -0
- package/skills/gsd-path-docs-audit/scripts/check_task_briefs.py +376 -0
- package/skills/gsd-path-docs-audit/scripts/discussion_records.py +619 -0
- package/skills/gsd-path-docs-audit/scripts/isolation.py +3538 -0
- package/skills/gsd-path-docs-audit/scripts/pipeline_git.py +940 -0
- package/skills/gsd-path-docs-audit/scripts/pipeline_state.py +2291 -0
- package/skills/gsd-path-docs-audit/scripts/state_checkpoint.py +1039 -0
- package/skills/gsd-path-docs-audit/scripts/state_promote.py +743 -0
- package/skills/gsd-path-docs-audit/templates/docs-audit.md +69 -0
- package/skills/gsd-path-forensics/SKILL.md +39 -0
- package/skills/gsd-path-forensics/agents/openai.yaml +6 -0
- package/skills/gsd-path-forensics/scripts/_common.py +180 -0
- package/skills/gsd-path-forensics/scripts/discussion_records.py +619 -0
- package/skills/gsd-path-forensics/scripts/pipeline_diagnose.py +715 -0
- package/skills/gsd-path-forensics/scripts/pipeline_undo.py +1039 -0
- package/skills/gsd-path-inspect/SKILL.md +167 -0
- package/skills/gsd-path-inspect/agents/openai.yaml +6 -0
- package/skills/gsd-path-inspect/references/codebase-mapper.md +52 -0
- package/skills/gsd-path-inspect/references/dispatch.md +131 -0
- package/skills/gsd-path-inspect/references/docs-auditor.md +60 -0
- package/skills/gsd-path-inspect/scripts/_common.py +180 -0
- package/skills/gsd-path-inspect/scripts/check_docs_audit.py +463 -0
- package/skills/gsd-path-inspect/scripts/check_handoffs.py +1855 -0
- package/skills/gsd-path-inspect/scripts/check_task_briefs.py +376 -0
- package/skills/gsd-path-inspect/scripts/detect_project.py +1517 -0
- package/skills/gsd-path-inspect/scripts/discussion_records.py +619 -0
- package/skills/gsd-path-inspect/scripts/isolation.py +3538 -0
- package/skills/gsd-path-inspect/scripts/pipeline_git.py +940 -0
- package/skills/gsd-path-inspect/scripts/pipeline_state.py +2291 -0
- package/skills/gsd-path-inspect/scripts/state_checkpoint.py +1039 -0
- package/skills/gsd-path-inspect/scripts/state_promote.py +743 -0
- package/skills/gsd-path-inspect/templates/codebase.md +46 -0
- package/skills/gsd-path-inspect/templates/docs-audit.md +69 -0
- package/skills/gsd-path-inspect/templates/state.md +27 -0
- package/skills/gsd-path-loop/SKILL.md +98 -0
- package/skills/gsd-path-loop/agents/openai.yaml +6 -0
- package/skills/gsd-path-loop/examples/ci-repair.LOOP.md +61 -0
- package/skills/gsd-path-loop/scripts/_common.py +180 -0
- package/skills/gsd-path-loop/scripts/discussion_records.py +619 -0
- package/skills/gsd-path-loop/scripts/loop_run.py +837 -0
- package/skills/gsd-path-loop/templates/loop.md +59 -0
- package/skills/gsd-path-migrate/SKILL.md +134 -0
- package/skills/gsd-path-migrate/agents/openai.yaml +6 -0
- package/skills/gsd-path-migrate/scripts/core_hook_gate.py +34 -0
- package/skills/gsd-path-migrate/scripts/core_hook_settings.py +119 -0
- package/skills/gsd-path-migrate/scripts/discussion_records.py +619 -0
- package/skills/gsd-path-migrate/scripts/migrate_core.py +169 -0
- package/skills/gsd-path-plan/SKILL.md +411 -0
- package/skills/gsd-path-plan/agents/openai.yaml +6 -0
- package/skills/gsd-path-plan/references/dispatch.md +131 -0
- package/skills/gsd-path-plan/references/planner.md +104 -0
- package/skills/gsd-path-plan/scripts/_common.py +180 -0
- package/skills/gsd-path-plan/scripts/check_handoffs.py +1855 -0
- package/skills/gsd-path-plan/scripts/check_task_briefs.py +376 -0
- package/skills/gsd-path-plan/scripts/discussion_records.py +619 -0
- package/skills/gsd-path-plan/scripts/isolation.py +3538 -0
- package/skills/gsd-path-plan/scripts/pipeline_git.py +940 -0
- package/skills/gsd-path-plan/scripts/pipeline_state.py +2291 -0
- package/skills/gsd-path-plan/scripts/review_panel.py +553 -0
- package/skills/gsd-path-plan/scripts/state_checkpoint.py +1039 -0
- package/skills/gsd-path-plan/scripts/state_promote.py +743 -0
- package/skills/gsd-path-plan/scripts/workflow_run.py +117 -0
- package/skills/gsd-path-plan/templates/patch-findings.md +16 -0
- package/skills/gsd-path-plan/templates/plan-panel.md +25 -0
- package/skills/gsd-path-plan/templates/plan.md +89 -0
- package/skills/gsd-path-plan/templates/task.md +74 -0
- package/skills/gsd-path-research/SKILL.md +159 -0
- package/skills/gsd-path-research/agents/openai.yaml +6 -0
- package/skills/gsd-path-research/references/dispatch.md +131 -0
- package/skills/gsd-path-research/references/researcher.md +42 -0
- package/skills/gsd-path-research/scripts/_common.py +180 -0
- package/skills/gsd-path-research/scripts/check_handoffs.py +1855 -0
- package/skills/gsd-path-research/scripts/check_task_briefs.py +376 -0
- package/skills/gsd-path-research/scripts/discussion_records.py +619 -0
- package/skills/gsd-path-research/scripts/isolation.py +3538 -0
- package/skills/gsd-path-research/scripts/pipeline_git.py +940 -0
- package/skills/gsd-path-research/scripts/pipeline_state.py +2291 -0
- package/skills/gsd-path-research/scripts/state_checkpoint.py +1039 -0
- package/skills/gsd-path-research/scripts/state_promote.py +743 -0
- package/skills/gsd-path-research/templates/evidence.md +24 -0
- package/skills/gsd-path-research/templates/research-handoff.md +25 -0
- package/skills/gsd-path-roadmap/SKILL.md +215 -0
- package/skills/gsd-path-roadmap/agents/openai.yaml +6 -0
- package/skills/gsd-path-roadmap/references/dispatch.md +131 -0
- package/skills/gsd-path-roadmap/references/roadmapper.md +48 -0
- package/skills/gsd-path-roadmap/scripts/_common.py +180 -0
- package/skills/gsd-path-roadmap/scripts/archive_milestone.py +2265 -0
- package/skills/gsd-path-roadmap/scripts/check_handoffs.py +1855 -0
- package/skills/gsd-path-roadmap/scripts/check_task_briefs.py +376 -0
- package/skills/gsd-path-roadmap/scripts/detect_project.py +1517 -0
- package/skills/gsd-path-roadmap/scripts/discussion_records.py +619 -0
- package/skills/gsd-path-roadmap/scripts/discussion_validate.py +1321 -0
- package/skills/gsd-path-roadmap/scripts/integration.py +1433 -0
- package/skills/gsd-path-roadmap/scripts/isolation.py +3538 -0
- package/skills/gsd-path-roadmap/scripts/pipeline_git.py +940 -0
- package/skills/gsd-path-roadmap/scripts/pipeline_state.py +2291 -0
- package/skills/gsd-path-roadmap/scripts/promote_lookahead.py +415 -0
- package/skills/gsd-path-roadmap/scripts/review_panel.py +553 -0
- package/skills/gsd-path-roadmap/scripts/state_checkpoint.py +1039 -0
- package/skills/gsd-path-roadmap/scripts/state_promote.py +743 -0
- package/skills/gsd-path-roadmap/templates/roadmap.md +38 -0
- package/skills/gsd-path-ship/SKILL.md +412 -0
- package/skills/gsd-path-ship/agents/openai.yaml +6 -0
- package/skills/gsd-path-ship/references/dispatch.md +131 -0
- package/skills/gsd-path-ship/references/reviewer.md +188 -0
- package/skills/gsd-path-ship/scripts/_common.py +180 -0
- package/skills/gsd-path-ship/scripts/archive_milestone.py +2265 -0
- package/skills/gsd-path-ship/scripts/build_state.py +854 -0
- package/skills/gsd-path-ship/scripts/check_handoffs.py +1855 -0
- package/skills/gsd-path-ship/scripts/check_task_briefs.py +376 -0
- package/skills/gsd-path-ship/scripts/discussion_records.py +619 -0
- package/skills/gsd-path-ship/scripts/discussion_validate.py +1321 -0
- package/skills/gsd-path-ship/scripts/integration.py +1433 -0
- package/skills/gsd-path-ship/scripts/isolation.py +3538 -0
- package/skills/gsd-path-ship/scripts/lean_verification.py +256 -0
- package/skills/gsd-path-ship/scripts/pipeline_git.py +940 -0
- package/skills/gsd-path-ship/scripts/pipeline_state.py +2291 -0
- package/skills/gsd-path-ship/scripts/review_panel.py +553 -0
- package/skills/gsd-path-ship/scripts/state_checkpoint.py +1039 -0
- package/skills/gsd-path-ship/scripts/state_promote.py +743 -0
- package/skills/gsd-path-ship/scripts/workflow_run.py +117 -0
- package/skills/gsd-path-ship/templates/archive-manifest.md +37 -0
- package/skills/gsd-path-ship/templates/final-review.md +27 -0
- package/skills/gsd-path-ship/templates/gap-review.md +20 -0
- package/skills/gsd-path-ship/templates/patch-findings.md +16 -0
- package/skills/gsd-path-ship/templates/wave-review.md +64 -0
- package/skills/gsd-path-undo/SKILL.md +45 -0
- package/skills/gsd-path-undo/agents/openai.yaml +6 -0
- package/skills/gsd-path-undo/scripts/_common.py +180 -0
- package/skills/gsd-path-undo/scripts/discussion_records.py +619 -0
- package/skills/gsd-path-undo/scripts/pipeline_undo.py +1039 -0
- package/skills/path/BUILD.md +700 -0
- package/skills/path/DECIDE.md +115 -0
- package/skills/path/DEFINE.md +297 -0
- package/skills/path/DOCS-AUDIT.md +190 -0
- package/skills/path/INSPECT.md +167 -0
- package/skills/path/PLAN.md +411 -0
- package/skills/path/RESEARCH.md +159 -0
- package/skills/path/ROADMAP.md +215 -0
- package/skills/path/SHIP.md +412 -0
- package/skills/path/SKILL.md +471 -0
- package/skills/path/agents/openai.yaml +6 -0
- package/skills/path/references/codebase-mapper.md +52 -0
- package/skills/path/references/coder.md +90 -0
- package/skills/path/references/decider.md +36 -0
- package/skills/path/references/dispatch.md +131 -0
- package/skills/path/references/docs-auditor.md +60 -0
- package/skills/path/references/planner.md +104 -0
- package/skills/path/references/researcher.md +42 -0
- package/skills/path/references/reviewer.md +188 -0
- package/skills/path/references/roadmapper.md +48 -0
- package/skills/path/scripts/_common.py +180 -0
- package/skills/path/scripts/archive_milestone.py +2265 -0
- package/skills/path/scripts/bootstrap_repository.py +710 -0
- package/skills/path/scripts/build_state.py +854 -0
- package/skills/path/scripts/check_docs_audit.py +463 -0
- package/skills/path/scripts/check_handoffs.py +1855 -0
- package/skills/path/scripts/check_task_briefs.py +376 -0
- package/skills/path/scripts/check_update.py +93 -0
- package/skills/path/scripts/detect_project.py +1517 -0
- package/skills/path/scripts/discussion_records.py +619 -0
- package/skills/path/scripts/discussion_validate.py +1321 -0
- package/skills/path/scripts/dispatch_driver.py +1812 -0
- package/skills/path/scripts/integration.py +1433 -0
- package/skills/path/scripts/isolation.py +3538 -0
- package/skills/path/scripts/lean_verification.py +256 -0
- package/skills/path/scripts/loop_run.py +837 -0
- package/skills/path/scripts/pipeline_git.py +940 -0
- package/skills/path/scripts/pipeline_state.py +2291 -0
- package/skills/path/scripts/promote_lookahead.py +415 -0
- package/skills/path/scripts/review_findings.py +714 -0
- package/skills/path/scripts/review_panel.py +553 -0
- package/skills/path/scripts/state_checkpoint.py +1039 -0
- package/skills/path/scripts/state_promote.py +743 -0
- package/skills/path/scripts/token_budget.py +146 -0
- package/skills/path/scripts/workflow_run.py +117 -0
- package/skills/path/templates/answers.md +29 -0
- package/skills/path/templates/archive-manifest.md +37 -0
- package/skills/path/templates/charter.md +50 -0
- package/skills/path/templates/codebase.md +46 -0
- package/skills/path/templates/dialogue.md +21 -0
- package/skills/path/templates/docs-audit.md +69 -0
- package/skills/path/templates/evidence.md +24 -0
- package/skills/path/templates/final-review.md +27 -0
- package/skills/path/templates/gap-review.md +20 -0
- package/skills/path/templates/intent.md +98 -0
- package/skills/path/templates/loop.md +59 -0
- package/skills/path/templates/patch-findings.md +16 -0
- package/skills/path/templates/plan-panel.md +25 -0
- package/skills/path/templates/plan.md +89 -0
- package/skills/path/templates/repository.md +16 -0
- package/skills/path/templates/research-handoff.md +25 -0
- package/skills/path/templates/roadmap.md +38 -0
- package/skills/path/templates/skeptic.md +36 -0
- package/skills/path/templates/state.md +27 -0
- package/skills/path/templates/synthesis.md +36 -0
- package/skills/path/templates/task.md +74 -0
- package/skills/path/templates/wave-panel.md +25 -0
- package/skills/path/templates/wave-review.md +64 -0
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# gsd-path project runtime
|
|
3
|
+
"""Lint build task briefs against the layer base before dispatch."""
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import json
|
|
7
|
+
import re
|
|
8
|
+
import sys
|
|
9
|
+
from pathlib import Path, PurePosixPath
|
|
10
|
+
from typing import Dict, List, Optional, Sequence, Set, Tuple
|
|
11
|
+
|
|
12
|
+
try:
|
|
13
|
+
import _common
|
|
14
|
+
except ImportError: # pragma: no cover - package import used by tests
|
|
15
|
+
from scripts import _common
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
DEFAULT_TASKS_DIR = ".project/tasks"
|
|
19
|
+
REQUIRED_FIELDS = (
|
|
20
|
+
"id",
|
|
21
|
+
"title",
|
|
22
|
+
"wave",
|
|
23
|
+
"deps",
|
|
24
|
+
"status",
|
|
25
|
+
"agent",
|
|
26
|
+
"base",
|
|
27
|
+
"worktree",
|
|
28
|
+
"task_branch",
|
|
29
|
+
"files",
|
|
30
|
+
)
|
|
31
|
+
FORBIDDEN_FIELDS = ("commit",)
|
|
32
|
+
REQUIRED_SECTIONS = (
|
|
33
|
+
"Context",
|
|
34
|
+
"Approach",
|
|
35
|
+
"Interface contract",
|
|
36
|
+
"Intent coverage",
|
|
37
|
+
"Acceptance criteria",
|
|
38
|
+
"Verify",
|
|
39
|
+
"Log",
|
|
40
|
+
)
|
|
41
|
+
PROSE_SECTIONS = ("Context", "Approach", "Interface contract")
|
|
42
|
+
FIELD_PATTERN = _common.FIELD_PATTERN
|
|
43
|
+
INLINE_LIST_PATTERN = _common.INLINE_LIST_PATTERN
|
|
44
|
+
LIST_ITEM_PATTERN = _common.LIST_ITEM_PATTERN
|
|
45
|
+
HEADING_PATTERN = re.compile(r"(?m)^## (?P<name>.+?)\s*$")
|
|
46
|
+
COMMENT_PATTERN = re.compile(r"<!--.*?-->", re.DOTALL)
|
|
47
|
+
BACKTICK_PATTERN = re.compile(r"`([^`\n]+)`")
|
|
48
|
+
PATH_TOKEN_PATTERN = re.compile(r"[A-Za-z0-9._~/-]+")
|
|
49
|
+
VERIFY_BLOCK_PATTERN = re.compile(r"```bash[ \t]*\n(?P<block>.*?)```", re.DOTALL)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class BriefError(RuntimeError):
|
|
53
|
+
"""Raised when task briefs fail the layer-prep lint or inputs are unusable."""
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
_run_git = _common.run_git
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _resolve_base(repo: Path, base: str) -> str:
|
|
60
|
+
result = _run_git(repo, "rev-parse", "--verify", "--quiet", f"{base}^{{commit}}")
|
|
61
|
+
if result.returncode != 0:
|
|
62
|
+
raise BriefError(f"--base does not resolve to a commit: {base}")
|
|
63
|
+
return result.stdout.strip()
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _base_exists(repo: Path, base: str, path: str) -> bool:
|
|
67
|
+
return _run_git(repo, "cat-file", "-e", f"{base}:{path}").returncode == 0
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _supplied_contract(repo: Path, task: Path, token: str) -> bool:
|
|
71
|
+
"""Plan approval checkpoints these supplied inputs after the brief gate."""
|
|
72
|
+
track = task.parent.parent.relative_to(repo).as_posix()
|
|
73
|
+
if track not in {".project", ".project/next"}:
|
|
74
|
+
return False
|
|
75
|
+
if token not in {
|
|
76
|
+
f"{track}/intent/INTENT.md",
|
|
77
|
+
f"{track}/research/SYNTHESIS.md",
|
|
78
|
+
}:
|
|
79
|
+
return False
|
|
80
|
+
path = repo / token
|
|
81
|
+
return path.is_file() and path.resolve() == repo.resolve() / token
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
_strip_yaml_comment = _common.strip_yaml_comment
|
|
85
|
+
_unquote = _common.unquote
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _frontmatter(text: str) -> Tuple[Optional[Dict[str, object]], Optional[str]]:
|
|
89
|
+
lines = text.splitlines()
|
|
90
|
+
if not lines or lines[0] != "---":
|
|
91
|
+
return None, "missing YAML frontmatter"
|
|
92
|
+
fields: Dict[str, object] = {}
|
|
93
|
+
index = 1
|
|
94
|
+
while index < len(lines):
|
|
95
|
+
line = lines[index]
|
|
96
|
+
if line == "---":
|
|
97
|
+
return fields, None
|
|
98
|
+
match = FIELD_PATTERN.match(line)
|
|
99
|
+
if match:
|
|
100
|
+
key = match.group("key")
|
|
101
|
+
if key in fields:
|
|
102
|
+
return None, f"frontmatter repeats field: {key}"
|
|
103
|
+
value = _strip_yaml_comment(match.group("value"))
|
|
104
|
+
inline = INLINE_LIST_PATTERN.fullmatch(value)
|
|
105
|
+
if inline is not None:
|
|
106
|
+
body = inline.group("body").strip()
|
|
107
|
+
fields[key] = [
|
|
108
|
+
_unquote(item)
|
|
109
|
+
for item in body.split(",")
|
|
110
|
+
if item.strip()
|
|
111
|
+
]
|
|
112
|
+
index += 1
|
|
113
|
+
continue
|
|
114
|
+
if value:
|
|
115
|
+
fields[key] = _unquote(value)
|
|
116
|
+
index += 1
|
|
117
|
+
continue
|
|
118
|
+
items: List[str] = []
|
|
119
|
+
cursor = index + 1
|
|
120
|
+
while cursor < len(lines):
|
|
121
|
+
item = LIST_ITEM_PATTERN.match(lines[cursor])
|
|
122
|
+
if not item:
|
|
123
|
+
break
|
|
124
|
+
items.append(_unquote(item.group("value")))
|
|
125
|
+
cursor += 1
|
|
126
|
+
fields[key] = items
|
|
127
|
+
index = cursor
|
|
128
|
+
continue
|
|
129
|
+
if line.strip() and not line.lstrip().startswith("#"):
|
|
130
|
+
return None, f"malformed frontmatter line: {line}"
|
|
131
|
+
index += 1
|
|
132
|
+
return None, "frontmatter is not closed"
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def _sections(text: str) -> Dict[str, str]:
|
|
136
|
+
headings = list(HEADING_PATTERN.finditer(text))
|
|
137
|
+
sections: Dict[str, str] = {}
|
|
138
|
+
for position, heading in enumerate(headings):
|
|
139
|
+
end = headings[position + 1].start() if position + 1 < len(headings) else len(text)
|
|
140
|
+
sections[heading.group("name")] = text[heading.end() : end]
|
|
141
|
+
return sections
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def _clean(body: str) -> str:
|
|
145
|
+
return COMMENT_PATTERN.sub("", body).strip()
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def _looks_like_path(token: str) -> bool:
|
|
149
|
+
return (
|
|
150
|
+
"/" in token
|
|
151
|
+
and PATH_TOKEN_PATTERN.fullmatch(token) is not None
|
|
152
|
+
and "://" not in token
|
|
153
|
+
and not token.startswith(("-", "/")) # /api/users is a route, not a repo path
|
|
154
|
+
and "<" not in token
|
|
155
|
+
and ">" not in token
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def _normalize(path: str) -> str:
|
|
160
|
+
return str(PurePosixPath(path))
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def _prose_tokens(body: str) -> List[str]:
|
|
164
|
+
cleaned = COMMENT_PATTERN.sub("", body)
|
|
165
|
+
return [
|
|
166
|
+
_normalize(match.group(1).strip())
|
|
167
|
+
for match in BACKTICK_PATTERN.finditer(cleaned)
|
|
168
|
+
if _looks_like_path(match.group(1).strip())
|
|
169
|
+
]
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def _verify_tokens(block: str) -> List[str]:
|
|
173
|
+
tokens = []
|
|
174
|
+
for line in block.splitlines():
|
|
175
|
+
parts = line.split()
|
|
176
|
+
for token in parts[1:]:
|
|
177
|
+
token = token.strip("\"'")
|
|
178
|
+
if _looks_like_path(token):
|
|
179
|
+
tokens.append(_normalize(token))
|
|
180
|
+
return tokens
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def _lint_task(
|
|
184
|
+
repo: Path, base: str, path: Path,
|
|
185
|
+
dependency_files: Optional[Dict[str, Set[str]]] = None,
|
|
186
|
+
landed_bases: Optional[Dict[str, str]] = None,
|
|
187
|
+
) -> Tuple[str, List[str], Optional[str], int]:
|
|
188
|
+
text = path.read_text(encoding="utf-8")
|
|
189
|
+
fields, error = _frontmatter(text)
|
|
190
|
+
task_id = path.stem
|
|
191
|
+
if fields and isinstance(fields.get("id"), str):
|
|
192
|
+
task_id = fields["id"] # type: ignore[assignment]
|
|
193
|
+
base = (landed_bases or {}).get(task_id, base)
|
|
194
|
+
problems: List[str] = []
|
|
195
|
+
checked = 0
|
|
196
|
+
if fields is None:
|
|
197
|
+
return task_id, [error or "invalid frontmatter"], None, checked
|
|
198
|
+
|
|
199
|
+
for field in REQUIRED_FIELDS:
|
|
200
|
+
if field not in fields:
|
|
201
|
+
problems.append(f"missing frontmatter field: {field}")
|
|
202
|
+
for field in FORBIDDEN_FIELDS:
|
|
203
|
+
if field in fields:
|
|
204
|
+
problems.append(f"forbidden frontmatter field: {field}")
|
|
205
|
+
|
|
206
|
+
sections = _sections(text)
|
|
207
|
+
for name in REQUIRED_SECTIONS:
|
|
208
|
+
body = sections.get(name)
|
|
209
|
+
if body is None:
|
|
210
|
+
problems.append(f"missing ## {name} section")
|
|
211
|
+
elif not _clean(body):
|
|
212
|
+
problems.append(f"## {name} section is empty")
|
|
213
|
+
|
|
214
|
+
supplied = (dependency_files or {}).get(task_id, set())
|
|
215
|
+
declared: Set[str] = set()
|
|
216
|
+
files = fields.get("files")
|
|
217
|
+
if isinstance(files, str):
|
|
218
|
+
problems.append("frontmatter files must be a list")
|
|
219
|
+
elif isinstance(files, list):
|
|
220
|
+
for entry in files:
|
|
221
|
+
candidate = PurePosixPath(entry)
|
|
222
|
+
if candidate.is_absolute():
|
|
223
|
+
problems.append(f"files entry is not repo-relative: {entry}")
|
|
224
|
+
continue
|
|
225
|
+
if ".." in candidate.parts:
|
|
226
|
+
problems.append(f"files entry must not contain '..': {entry}")
|
|
227
|
+
continue
|
|
228
|
+
normalized = str(candidate)
|
|
229
|
+
declared.add(normalized)
|
|
230
|
+
checked += 1
|
|
231
|
+
if ".git" in candidate.parts:
|
|
232
|
+
problems.append(f"files entry must not enter .git: {normalized}")
|
|
233
|
+
continue
|
|
234
|
+
for parent in candidate.parents:
|
|
235
|
+
if str(parent) == ".":
|
|
236
|
+
break
|
|
237
|
+
kind = _run_git(repo, "cat-file", "-t", f"{base}:{parent}")
|
|
238
|
+
if kind.returncode == 0 and kind.stdout.strip() != "tree":
|
|
239
|
+
problems.append(
|
|
240
|
+
f"files entry {normalized} has non-directory ancestor "
|
|
241
|
+
f"{parent} at the layer base"
|
|
242
|
+
)
|
|
243
|
+
break
|
|
244
|
+
|
|
245
|
+
for name in PROSE_SECTIONS:
|
|
246
|
+
body = sections.get(name)
|
|
247
|
+
if body is None or not _clean(body):
|
|
248
|
+
continue
|
|
249
|
+
for token in _prose_tokens(body):
|
|
250
|
+
checked += 1
|
|
251
|
+
if (
|
|
252
|
+
token not in declared
|
|
253
|
+
and token not in supplied
|
|
254
|
+
and not _supplied_contract(repo, path, token)
|
|
255
|
+
and not _base_exists(repo, base, token)
|
|
256
|
+
):
|
|
257
|
+
problems.append(f"## {name} names a path missing at the layer base: {token}")
|
|
258
|
+
|
|
259
|
+
verify_body = sections.get("Verify")
|
|
260
|
+
if verify_body is not None and _clean(verify_body):
|
|
261
|
+
block = VERIFY_BLOCK_PATTERN.search(verify_body)
|
|
262
|
+
if block is None or not block.group("block").strip():
|
|
263
|
+
problems.append("## Verify must contain a non-empty fenced bash block")
|
|
264
|
+
else:
|
|
265
|
+
for token in _verify_tokens(block.group("block")):
|
|
266
|
+
checked += 1
|
|
267
|
+
if token not in declared and token not in supplied and not _base_exists(repo, base, token):
|
|
268
|
+
problems.append(f"## Verify names a path missing at the layer base: {token}")
|
|
269
|
+
|
|
270
|
+
contract = None
|
|
271
|
+
contract_body = sections.get("Interface contract")
|
|
272
|
+
if contract_body is not None:
|
|
273
|
+
normalized = " ".join(_clean(contract_body).split())
|
|
274
|
+
if normalized and normalized not in {"None", "- None"}:
|
|
275
|
+
contract = _clean(contract_body)
|
|
276
|
+
|
|
277
|
+
return task_id, problems, contract, checked
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def validate_task_briefs(
|
|
281
|
+
root: Path, base: str, tasks_dir: str = DEFAULT_TASKS_DIR,
|
|
282
|
+
*, dependency_files: Optional[Dict[str, Set[str]]] = None,
|
|
283
|
+
landed_bases: Optional[Dict[str, str]] = None,
|
|
284
|
+
) -> Dict[str, object]:
|
|
285
|
+
"""Lint task briefs using the caller's validation bases.
|
|
286
|
+
|
|
287
|
+
Dispatch uses the layer base without overrides. Plan approval supplies
|
|
288
|
+
transitive dependency files for pending tasks and resolved historical bases
|
|
289
|
+
for landed tasks, whose immutable briefs must survive later file changes.
|
|
290
|
+
The caller validates the task graph and landed metadata before supplying
|
|
291
|
+
these mappings; landed tasks receive no dependency-file allowance.
|
|
292
|
+
"""
|
|
293
|
+
|
|
294
|
+
resolved_base = _resolve_base(root, base)
|
|
295
|
+
tasks_path = root / tasks_dir
|
|
296
|
+
if not tasks_path.is_dir():
|
|
297
|
+
raise BriefError(f"tasks directory not found: {tasks_dir}")
|
|
298
|
+
|
|
299
|
+
problems: List[str] = []
|
|
300
|
+
contracts: Dict[str, Set[str]] = {}
|
|
301
|
+
checked = 0
|
|
302
|
+
task_files = sorted(tasks_path.glob("*.md"))
|
|
303
|
+
if not task_files:
|
|
304
|
+
raise BriefError(f"no task briefs found in {tasks_dir}")
|
|
305
|
+
for path in task_files:
|
|
306
|
+
task_id, task_problems, contract, task_checked = _lint_task(
|
|
307
|
+
root, resolved_base, path, dependency_files, landed_bases
|
|
308
|
+
)
|
|
309
|
+
problems.extend(f"{task_id}: {problem}" for problem in task_problems)
|
|
310
|
+
if contract is not None:
|
|
311
|
+
for entry in re.split(r"(?m)^-[ \t]+", contract):
|
|
312
|
+
shape = " ".join(entry.split())
|
|
313
|
+
if shape:
|
|
314
|
+
contracts.setdefault(shape, set()).add(task_id)
|
|
315
|
+
checked += task_checked
|
|
316
|
+
|
|
317
|
+
for shape, task_ids in contracts.items():
|
|
318
|
+
if len(task_ids) == 1:
|
|
319
|
+
task_id = next(iter(task_ids))
|
|
320
|
+
problems.append(
|
|
321
|
+
f"{task_id}: interface contract is not shared by any other task: {shape}"
|
|
322
|
+
)
|
|
323
|
+
|
|
324
|
+
if problems:
|
|
325
|
+
raise BriefError("\n".join(problems))
|
|
326
|
+
return {"base": resolved_base, "checked": checked, "tasks": len(task_files)}
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
def _tasks_dir(value: str) -> str:
|
|
330
|
+
"""Argparse type for --tasks-dir: a relative POSIX-style path."""
|
|
331
|
+
|
|
332
|
+
path = PurePosixPath(value)
|
|
333
|
+
if path.is_absolute():
|
|
334
|
+
raise argparse.ArgumentTypeError(
|
|
335
|
+
f"--tasks-dir must be a relative path, found absolute: {value}"
|
|
336
|
+
)
|
|
337
|
+
if ".." in path.parts:
|
|
338
|
+
raise argparse.ArgumentTypeError(
|
|
339
|
+
f"--tasks-dir must not contain '..', found: {value}"
|
|
340
|
+
)
|
|
341
|
+
return str(path)
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
def parser() -> argparse.ArgumentParser:
|
|
345
|
+
argument_parser = argparse.ArgumentParser(description=__doc__)
|
|
346
|
+
argument_parser.add_argument("--repo", type=Path, required=True)
|
|
347
|
+
argument_parser.add_argument(
|
|
348
|
+
"--base",
|
|
349
|
+
required=True,
|
|
350
|
+
help="full SHA of the clean layer base commit the briefs are checked against",
|
|
351
|
+
)
|
|
352
|
+
argument_parser.add_argument(
|
|
353
|
+
"--tasks-dir",
|
|
354
|
+
type=_tasks_dir,
|
|
355
|
+
default=DEFAULT_TASKS_DIR,
|
|
356
|
+
help="relative POSIX-style directory under --repo holding the task files "
|
|
357
|
+
"(default: .project/tasks)",
|
|
358
|
+
)
|
|
359
|
+
return argument_parser
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
def main(argv: Optional[Sequence[str]] = None) -> int:
|
|
363
|
+
arguments = parser().parse_args(argv)
|
|
364
|
+
try:
|
|
365
|
+
result = validate_task_briefs(
|
|
366
|
+
arguments.repo.resolve(), arguments.base, arguments.tasks_dir
|
|
367
|
+
)
|
|
368
|
+
except BriefError as error:
|
|
369
|
+
print(f"task brief validation failed: {error}", file=sys.stderr)
|
|
370
|
+
return 1
|
|
371
|
+
print(json.dumps(result, sort_keys=True))
|
|
372
|
+
return 0
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
if __name__ == "__main__":
|
|
376
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Print a one-line notice when a newer GSD Path is published to npm.
|
|
3
|
+
|
|
4
|
+
Installed beside the router skill; the installer stamps ../VERSION. Silent on
|
|
5
|
+
any failure (offline, unpublished package, missing stamp) and rate-limited by
|
|
6
|
+
a 24h cache, so the router can run it unconditionally.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
import time
|
|
11
|
+
import urllib.request
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
REGISTRY_URL = "https://registry.npmjs.org/@opengsd%2Fgsd-path/latest"
|
|
15
|
+
CACHE_PATH = Path.home() / ".cache" / "gsd-path" / "update-check.json"
|
|
16
|
+
CACHE_TTL_SECONDS = 24 * 3600
|
|
17
|
+
NETWORK_TIMEOUT_SECONDS = 3
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def parse_version(value):
|
|
21
|
+
try:
|
|
22
|
+
return tuple(int(part) for part in value.strip().split("."))
|
|
23
|
+
except (ValueError, AttributeError):
|
|
24
|
+
return None
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def is_newer(latest, installed):
|
|
28
|
+
latest_parts = parse_version(latest)
|
|
29
|
+
installed_parts = parse_version(installed)
|
|
30
|
+
return (
|
|
31
|
+
latest_parts is not None
|
|
32
|
+
and installed_parts is not None
|
|
33
|
+
and latest_parts > installed_parts
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _write_cache(now, latest):
|
|
38
|
+
try:
|
|
39
|
+
CACHE_PATH.parent.mkdir(parents=True, exist_ok=True)
|
|
40
|
+
CACHE_PATH.write_text(
|
|
41
|
+
json.dumps({"checked_at": now, "latest": latest}), encoding="utf-8"
|
|
42
|
+
)
|
|
43
|
+
except OSError:
|
|
44
|
+
pass
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def latest_version():
|
|
48
|
+
now = time.time()
|
|
49
|
+
try:
|
|
50
|
+
cached = json.loads(CACHE_PATH.read_text(encoding="utf-8"))
|
|
51
|
+
if now - cached["checked_at"] < CACHE_TTL_SECONDS:
|
|
52
|
+
return cached["latest"]
|
|
53
|
+
except Exception:
|
|
54
|
+
pass
|
|
55
|
+
try:
|
|
56
|
+
with urllib.request.urlopen(
|
|
57
|
+
REGISTRY_URL, timeout=NETWORK_TIMEOUT_SECONDS
|
|
58
|
+
) as response:
|
|
59
|
+
latest = json.load(response)["version"]
|
|
60
|
+
except Exception:
|
|
61
|
+
# Cache the failure too, so offline or pre-publish routers do not
|
|
62
|
+
# retry the network on every conversation within the TTL.
|
|
63
|
+
_write_cache(now, None)
|
|
64
|
+
return None
|
|
65
|
+
_write_cache(now, latest)
|
|
66
|
+
return latest
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def notice():
|
|
70
|
+
version_file = Path(__file__).resolve().parent.parent / "VERSION"
|
|
71
|
+
try:
|
|
72
|
+
installed = version_file.read_text(encoding="utf-8").strip()
|
|
73
|
+
latest = latest_version()
|
|
74
|
+
except Exception:
|
|
75
|
+
return None
|
|
76
|
+
if is_newer(latest, installed):
|
|
77
|
+
return (
|
|
78
|
+
f"GSD Path {latest} is available (installed {installed}). "
|
|
79
|
+
"Update: npx @opengsd/gsd-path@latest --update "
|
|
80
|
+
"(add --local for a project install)."
|
|
81
|
+
)
|
|
82
|
+
return None
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def main():
|
|
86
|
+
line = notice()
|
|
87
|
+
if line:
|
|
88
|
+
print(line)
|
|
89
|
+
return 0
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
if __name__ == "__main__":
|
|
93
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Run an existing Core hook except in explicitly migrated project roots."""
|
|
3
|
+
|
|
4
|
+
import argparse
|
|
5
|
+
import json
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
import subprocess
|
|
8
|
+
import sys
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def main():
|
|
12
|
+
parser = argparse.ArgumentParser(description=__doc__)
|
|
13
|
+
parser.add_argument("--repo", required=True, action="append", type=Path)
|
|
14
|
+
parser.add_argument("--command", required=True)
|
|
15
|
+
args = parser.parse_args()
|
|
16
|
+
payload = sys.stdin.buffer.read()
|
|
17
|
+
try:
|
|
18
|
+
event = json.loads(payload)
|
|
19
|
+
cwd = event.get("cwd") if isinstance(event, dict) else None
|
|
20
|
+
if isinstance(cwd, str) and Path(cwd).is_absolute():
|
|
21
|
+
directory = Path(cwd).resolve(strict=True)
|
|
22
|
+
for project in args.repo:
|
|
23
|
+
root = project.resolve(strict=True)
|
|
24
|
+
if directory.is_dir() and (directory == root or root in directory.parents):
|
|
25
|
+
return 0
|
|
26
|
+
except (OSError, ValueError, RuntimeError):
|
|
27
|
+
# An uncertain event keeps the original hook's protection and errors.
|
|
28
|
+
pass
|
|
29
|
+
result = subprocess.run(args.command, shell=True, input=payload)
|
|
30
|
+
return result.returncode if result.returncode >= 0 else 128 - result.returncode
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
if __name__ == "__main__":
|
|
34
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Prepare, apply or restore one explicitly selected standalone Core hook."""
|
|
3
|
+
|
|
4
|
+
import argparse
|
|
5
|
+
import base64
|
|
6
|
+
import json
|
|
7
|
+
import os
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
import shlex
|
|
10
|
+
import stat
|
|
11
|
+
import subprocess
|
|
12
|
+
import sys
|
|
13
|
+
import tempfile
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def read_regular(path):
|
|
17
|
+
metadata = path.lstat()
|
|
18
|
+
if not stat.S_ISREG(metadata.st_mode) or metadata.st_nlink != 1:
|
|
19
|
+
raise ValueError(f"expected an unlinked regular file: {path}")
|
|
20
|
+
return path.read_bytes()
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def plan(args):
|
|
24
|
+
if args.settings is None or args.repo is None or args.location is None:
|
|
25
|
+
raise ValueError("plan requires --settings, --repo and --location")
|
|
26
|
+
before = read_regular(args.settings)
|
|
27
|
+
data = json.loads(before)
|
|
28
|
+
location = json.loads(args.location)
|
|
29
|
+
if not isinstance(location, list) or len(location) < 2 or location[0] not in ("hooks", "statusLine") or location[-1] != "command":
|
|
30
|
+
raise ValueError("location must select a hooks or statusLine command")
|
|
31
|
+
entry = data
|
|
32
|
+
for key in location[:-1]:
|
|
33
|
+
if isinstance(entry, dict) and isinstance(key, str):
|
|
34
|
+
entry = entry[key]
|
|
35
|
+
elif isinstance(entry, list) and type(key) is int and key >= 0:
|
|
36
|
+
entry = entry[key]
|
|
37
|
+
else:
|
|
38
|
+
raise ValueError("invalid command location")
|
|
39
|
+
if not isinstance(entry, dict) or entry.get("type", "command") != "command":
|
|
40
|
+
raise ValueError("selected hook is not a command hook")
|
|
41
|
+
original = entry.get("command")
|
|
42
|
+
if not isinstance(original, str) or not original.strip():
|
|
43
|
+
raise ValueError("selected command must be a nonempty string")
|
|
44
|
+
repo = args.repo.resolve(strict=True)
|
|
45
|
+
if not repo.is_dir():
|
|
46
|
+
raise ValueError("--repo must be a directory")
|
|
47
|
+
gate = Path(__file__).resolve().with_name("core_hook_gate.py")
|
|
48
|
+
if not gate.is_file():
|
|
49
|
+
raise ValueError(f"missing installed hook gate: {gate}")
|
|
50
|
+
argv = [sys.executable, "-B", str(gate), "--repo", str(repo), "--command", original]
|
|
51
|
+
replacement = subprocess.list2cmdline(argv) if os.name == "nt" else shlex.join(argv)
|
|
52
|
+
entry["command"] = replacement
|
|
53
|
+
after = (json.dumps(data, indent=2) + "\n").encode()
|
|
54
|
+
receipt = {
|
|
55
|
+
"schema": "gsd-path/core-hook-change/v1",
|
|
56
|
+
"settings": str(args.settings.absolute()),
|
|
57
|
+
"repo": str(repo), "location": location,
|
|
58
|
+
"original_command": original, "replacement_command": replacement,
|
|
59
|
+
"before": base64.b64encode(before).decode(),
|
|
60
|
+
"after": base64.b64encode(after).decode(),
|
|
61
|
+
}
|
|
62
|
+
# Settings may contain secrets; the review/rollback copy is owner-only.
|
|
63
|
+
with open(args.receipt, "x", encoding="utf-8", opener=lambda path, flags: os.open(path, flags, 0o600)) as handle:
|
|
64
|
+
json.dump(receipt, handle, indent=2)
|
|
65
|
+
handle.write("\n")
|
|
66
|
+
return {"status": "planned", "receipt": str(args.receipt.absolute())}
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def change(args):
|
|
70
|
+
if args.settings is not None or args.repo is not None or args.location is not None:
|
|
71
|
+
raise ValueError("apply and restore accept only --receipt")
|
|
72
|
+
receipt = json.loads(read_regular(args.receipt))
|
|
73
|
+
if not isinstance(receipt, dict) or receipt.get("schema") != "gsd-path/core-hook-change/v1":
|
|
74
|
+
raise ValueError("unsupported hook change receipt")
|
|
75
|
+
path = Path(receipt["settings"])
|
|
76
|
+
if not path.is_absolute():
|
|
77
|
+
raise ValueError("receipt settings path must be absolute")
|
|
78
|
+
before = base64.b64decode(receipt["before"], validate=True)
|
|
79
|
+
after = base64.b64decode(receipt["after"], validate=True)
|
|
80
|
+
expected, desired = (before, after) if args.action == "apply" else (after, before)
|
|
81
|
+
current = read_regular(path)
|
|
82
|
+
if current == desired:
|
|
83
|
+
return {"status": args.action, "settings": str(path), "changed": False}
|
|
84
|
+
if current != expected:
|
|
85
|
+
raise ValueError("settings changed since planning; refusing to overwrite later edits")
|
|
86
|
+
temporary = None
|
|
87
|
+
try:
|
|
88
|
+
with tempfile.NamedTemporaryFile(dir=path.parent, delete=False) as handle:
|
|
89
|
+
temporary = Path(handle.name)
|
|
90
|
+
handle.write(desired)
|
|
91
|
+
os.chmod(temporary, stat.S_IMODE(path.stat().st_mode))
|
|
92
|
+
if read_regular(path) != expected:
|
|
93
|
+
raise ValueError("settings changed during update")
|
|
94
|
+
os.replace(temporary, path)
|
|
95
|
+
finally:
|
|
96
|
+
if temporary is not None:
|
|
97
|
+
temporary.unlink(missing_ok=True)
|
|
98
|
+
return {"status": args.action, "settings": str(path), "changed": True}
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def main():
|
|
102
|
+
parser = argparse.ArgumentParser(description=__doc__)
|
|
103
|
+
parser.add_argument("action", choices=("plan", "apply", "restore"))
|
|
104
|
+
parser.add_argument("--receipt", type=Path, required=True)
|
|
105
|
+
parser.add_argument("--settings", type=Path)
|
|
106
|
+
parser.add_argument("--repo", type=Path)
|
|
107
|
+
parser.add_argument("--location", help="JSON array locating the selected command")
|
|
108
|
+
args = parser.parse_args()
|
|
109
|
+
try:
|
|
110
|
+
result = plan(args) if args.action == "plan" else change(args)
|
|
111
|
+
print(json.dumps(result))
|
|
112
|
+
except (OSError, ValueError, KeyError, IndexError, TypeError) as error:
|
|
113
|
+
print(f"hook settings error: {error}", file=sys.stderr)
|
|
114
|
+
return 1
|
|
115
|
+
return 0
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
if __name__ == "__main__":
|
|
119
|
+
raise SystemExit(main())
|