@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,619 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# gsd-path project runtime
|
|
3
|
+
"""Manage GSD Path discussion records as a recoverable paired transaction."""
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import json
|
|
7
|
+
import os
|
|
8
|
+
import shutil
|
|
9
|
+
import sys
|
|
10
|
+
from contextlib import contextmanager
|
|
11
|
+
from datetime import date
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import Iterator, Optional, Sequence
|
|
14
|
+
|
|
15
|
+
def _load_pipeline_modules():
|
|
16
|
+
try:
|
|
17
|
+
import archive_milestone
|
|
18
|
+
import pipeline_state
|
|
19
|
+
import _common
|
|
20
|
+
return _common, archive_milestone, pipeline_state
|
|
21
|
+
except ModuleNotFoundError as error:
|
|
22
|
+
if error.name not in {"archive_milestone", "pipeline_state", "_common"}:
|
|
23
|
+
raise
|
|
24
|
+
try:
|
|
25
|
+
from scripts import archive_milestone, pipeline_state, _common
|
|
26
|
+
return _common, archive_milestone, pipeline_state
|
|
27
|
+
except ModuleNotFoundError as error:
|
|
28
|
+
if error.name not in {
|
|
29
|
+
"scripts",
|
|
30
|
+
"scripts.archive_milestone",
|
|
31
|
+
"scripts.pipeline_state",
|
|
32
|
+
"scripts._common",
|
|
33
|
+
}:
|
|
34
|
+
raise
|
|
35
|
+
shared_scripts = Path(__file__).resolve().parents[2] / "gsd-path" / "scripts"
|
|
36
|
+
sys.path.insert(0, str(shared_scripts))
|
|
37
|
+
import archive_milestone
|
|
38
|
+
import pipeline_state
|
|
39
|
+
import _common
|
|
40
|
+
return _common, archive_milestone, pipeline_state
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
_common, archive_milestone, pipeline_state = _load_pipeline_modules()
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class DiscussionError(RuntimeError):
|
|
47
|
+
pass
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
APPEND_TRANSACTION = ".discussion-append-transaction.json"
|
|
51
|
+
FILES = ("DIALOGUE.md", "ANSWERS.md")
|
|
52
|
+
PHASES = {"inspect", "define", "research", "decide", "roadmap", "plan", "build", "ship"}
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
atomic_write = _common.atomic_write
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def project_root(repo: Path) -> tuple[Path, Path, Path]:
|
|
59
|
+
root = repo.resolve()
|
|
60
|
+
project = root / ".project"
|
|
61
|
+
state = project / "STATE.md"
|
|
62
|
+
if project.is_symlink() or not project.is_dir():
|
|
63
|
+
raise DiscussionError(f".project must be a real directory: {project}")
|
|
64
|
+
if state.is_symlink() or not state.is_file():
|
|
65
|
+
raise DiscussionError(f"STATE.md must be a real file: {state}")
|
|
66
|
+
return root, project, state
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@contextmanager
|
|
70
|
+
def state_lock(state: Path) -> Iterator[None]:
|
|
71
|
+
with archive_milestone.discussion_lock(state.parent):
|
|
72
|
+
yield
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def state_context(state: Path) -> tuple[str, str, str]:
|
|
76
|
+
try:
|
|
77
|
+
current, _content, loaded = pipeline_state.load_state(state.parent.parent)
|
|
78
|
+
except pipeline_state.PipelineStateError as error:
|
|
79
|
+
raise DiscussionError(str(error)) from error
|
|
80
|
+
if loaded != state:
|
|
81
|
+
raise DiscussionError(f"canonical STATE.md path changed: {loaded}")
|
|
82
|
+
phase = current.phase
|
|
83
|
+
status = current.status
|
|
84
|
+
configured_archive = current.archive or "null"
|
|
85
|
+
if phase not in PHASES or status not in {"active", "blocked", "done"}:
|
|
86
|
+
raise DiscussionError(f"discussion is not available in state {phase}/{status}")
|
|
87
|
+
return phase, status, configured_archive
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def template_header(path: Path, record_prefix: str) -> str:
|
|
91
|
+
if path.is_symlink() or not path.is_file() or not path.is_absolute():
|
|
92
|
+
raise DiscussionError(f"template must be an absolute real file: {path}")
|
|
93
|
+
content = path.read_text(encoding="utf-8")
|
|
94
|
+
marker = f"\n{record_prefix}"
|
|
95
|
+
if marker not in content:
|
|
96
|
+
raise DiscussionError(f"template lacks sample record: {path}")
|
|
97
|
+
return content.split(marker, 1)[0].rstrip() + "\n"
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def record_count(directory: Path) -> int:
|
|
101
|
+
lines = (directory / "DIALOGUE.md").read_text(encoding="utf-8").splitlines()
|
|
102
|
+
return sum(
|
|
103
|
+
archive_milestone.DIALOGUE_HEADING_PATTERN.fullmatch(line) is not None
|
|
104
|
+
for line in lines
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def validate_or_empty(directory: Path) -> None:
|
|
109
|
+
if directory.is_symlink() or not directory.is_dir():
|
|
110
|
+
raise DiscussionError("discussion records must be a real directory")
|
|
111
|
+
if sorted(path.name for path in directory.iterdir()) != sorted(FILES):
|
|
112
|
+
raise DiscussionError("discussion directory must contain exactly DIALOGUE.md and ANSWERS.md")
|
|
113
|
+
if record_count(directory) == 0:
|
|
114
|
+
if "### D" in (directory / "DIALOGUE.md").read_text(encoding="utf-8"):
|
|
115
|
+
raise DiscussionError("empty discussion has a malformed dialogue record")
|
|
116
|
+
if "## Answer A" in (directory / "ANSWERS.md").read_text(encoding="utf-8"):
|
|
117
|
+
raise DiscussionError("empty discussion has a malformed answer record")
|
|
118
|
+
return
|
|
119
|
+
try:
|
|
120
|
+
archive_milestone.validate_discussion_directory(
|
|
121
|
+
directory, require_dispositions=False
|
|
122
|
+
)
|
|
123
|
+
except archive_milestone.ArchiveError as error:
|
|
124
|
+
raise DiscussionError(str(error)) from error
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def finish_append(project: Path, discussion: Path) -> None:
|
|
128
|
+
transaction = project / APPEND_TRANSACTION
|
|
129
|
+
if not transaction.exists():
|
|
130
|
+
return
|
|
131
|
+
if transaction.is_symlink() or not transaction.is_file():
|
|
132
|
+
raise DiscussionError("discussion append journal must be a real file")
|
|
133
|
+
try:
|
|
134
|
+
payload = json.loads(transaction.read_text(encoding="utf-8"))
|
|
135
|
+
except (OSError, json.JSONDecodeError) as error:
|
|
136
|
+
raise DiscussionError(f"discussion append journal is unreadable: {error}") from error
|
|
137
|
+
if payload.get("schema") != "gsd-path/discussion-append/v1":
|
|
138
|
+
raise DiscussionError("discussion append journal has the wrong schema")
|
|
139
|
+
files = payload.get("files")
|
|
140
|
+
if (
|
|
141
|
+
not isinstance(files, dict)
|
|
142
|
+
or set(files) != set(FILES)
|
|
143
|
+
or any(not isinstance(files[name], str) for name in FILES)
|
|
144
|
+
):
|
|
145
|
+
raise DiscussionError("discussion append journal has invalid files")
|
|
146
|
+
if project.is_symlink() or not project.is_dir():
|
|
147
|
+
raise DiscussionError("discussion recovery requires a real .project directory")
|
|
148
|
+
if discussion.is_symlink() or (
|
|
149
|
+
discussion.exists() and not discussion.is_dir()
|
|
150
|
+
):
|
|
151
|
+
raise DiscussionError("discussion recovery destination must be a real directory")
|
|
152
|
+
if discussion.resolve(strict=False).parent != project.resolve():
|
|
153
|
+
raise DiscussionError("discussion recovery destination escapes .project")
|
|
154
|
+
discussion.mkdir(exist_ok=True)
|
|
155
|
+
for name in FILES:
|
|
156
|
+
atomic_write(discussion / name, files[name])
|
|
157
|
+
validate_or_empty(discussion)
|
|
158
|
+
transaction.unlink()
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def validate_contents(project: Path, files: dict) -> None:
|
|
162
|
+
staging = project / ".discussion-validation-tmp"
|
|
163
|
+
if staging.exists() or staging.is_symlink():
|
|
164
|
+
if staging.is_symlink() or not staging.is_dir():
|
|
165
|
+
raise DiscussionError(f"discussion validation path is invalid: {staging}")
|
|
166
|
+
shutil.rmtree(staging)
|
|
167
|
+
staging.mkdir()
|
|
168
|
+
try:
|
|
169
|
+
for name in FILES:
|
|
170
|
+
(staging / name).write_text(files[name], encoding="utf-8")
|
|
171
|
+
validate_or_empty(staging)
|
|
172
|
+
finally:
|
|
173
|
+
shutil.rmtree(staging)
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def initialize_records(discussion: Path, dialogue_template: Path, answers_template: Path) -> None:
|
|
177
|
+
if discussion.exists() or discussion.is_symlink():
|
|
178
|
+
validate_or_empty(discussion)
|
|
179
|
+
return
|
|
180
|
+
staging = discussion.parent / ".discuss.gsd-path-tmp"
|
|
181
|
+
if staging.exists() or staging.is_symlink():
|
|
182
|
+
if staging.is_symlink() or not staging.is_dir():
|
|
183
|
+
raise DiscussionError(f"discussion staging path is invalid: {staging}")
|
|
184
|
+
shutil.rmtree(staging)
|
|
185
|
+
staging.mkdir()
|
|
186
|
+
try:
|
|
187
|
+
(staging / "DIALOGUE.md").write_text(
|
|
188
|
+
template_header(dialogue_template, "### D001"), encoding="utf-8"
|
|
189
|
+
)
|
|
190
|
+
(staging / "ANSWERS.md").write_text(
|
|
191
|
+
template_header(answers_template, "## Answer A001"), encoding="utf-8"
|
|
192
|
+
)
|
|
193
|
+
os.replace(staging, discussion)
|
|
194
|
+
finally:
|
|
195
|
+
if staging.exists():
|
|
196
|
+
shutil.rmtree(staging)
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def prepare_locked(
|
|
200
|
+
root: Path,
|
|
201
|
+
project: Path,
|
|
202
|
+
state: Path,
|
|
203
|
+
dialogue_template: Path,
|
|
204
|
+
answers_template: Path,
|
|
205
|
+
) -> tuple[Path, str, str]:
|
|
206
|
+
phase, status, configured = state_context(state)
|
|
207
|
+
discussion = project / "discuss"
|
|
208
|
+
finish_append(project, discussion)
|
|
209
|
+
if not archive_milestone.is_unset(configured):
|
|
210
|
+
if (phase, status) != ("ship", "active"):
|
|
211
|
+
raise DiscussionError("archive recovery is allowed only in ship/active")
|
|
212
|
+
try:
|
|
213
|
+
archive = archive_milestone.archive_from_state(root, configured)
|
|
214
|
+
except archive_milestone.ArchiveError as error:
|
|
215
|
+
raise DiscussionError(str(error)) from error
|
|
216
|
+
if archive_milestone.archive_is_committed(root, configured):
|
|
217
|
+
raise DiscussionError("committed discussion archives are immutable")
|
|
218
|
+
archived = archive / "discuss"
|
|
219
|
+
if not archived.is_dir():
|
|
220
|
+
raise DiscussionError("uncommitted archive has no discussion records to resume")
|
|
221
|
+
if not discussion.exists():
|
|
222
|
+
staging = project / ".discuss.gsd-path-tmp"
|
|
223
|
+
if staging.exists():
|
|
224
|
+
shutil.rmtree(staging)
|
|
225
|
+
shutil.copytree(archived, staging)
|
|
226
|
+
os.replace(staging, discussion)
|
|
227
|
+
else:
|
|
228
|
+
try:
|
|
229
|
+
archive_milestone.require_append_only_discussion(discussion, archived)
|
|
230
|
+
except archive_milestone.ArchiveError as error:
|
|
231
|
+
raise DiscussionError(str(error)) from error
|
|
232
|
+
else:
|
|
233
|
+
initialize_records(discussion, dialogue_template, answers_template)
|
|
234
|
+
validate_or_empty(discussion)
|
|
235
|
+
if state_context(state) != (phase, status, configured):
|
|
236
|
+
raise DiscussionError("STATE.md changed while discussion records were prepared")
|
|
237
|
+
return discussion, phase, status
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def numbered_sections(lines: Sequence[str], pattern, boundaries) -> list[tuple]:
|
|
241
|
+
return list(archive_milestone.record_sections(lines, pattern, boundaries))
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def field(lines: Sequence[str], name: str) -> str:
|
|
245
|
+
try:
|
|
246
|
+
return archive_milestone.completed_bullet_field(lines, name, "ANSWERS.md")
|
|
247
|
+
except archive_milestone.ArchiveError as error:
|
|
248
|
+
raise DiscussionError(str(error)) from error
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def disposed_answers(answer_lines: Sequence[str]) -> set[str]:
|
|
252
|
+
boundaries = (
|
|
253
|
+
archive_milestone.ANSWER_HEADING_PATTERN,
|
|
254
|
+
archive_milestone.DISPOSITION_HEADING_PATTERN,
|
|
255
|
+
)
|
|
256
|
+
dispositions = numbered_sections(
|
|
257
|
+
answer_lines, archive_milestone.DISPOSITION_HEADING_PATTERN, boundaries
|
|
258
|
+
)
|
|
259
|
+
return {field(section, "Answer") for _, section in dispositions}
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def dialogue_threads(dialogue_lines: Sequence[str]) -> dict[str, list[tuple]]:
|
|
263
|
+
sections = numbered_sections(
|
|
264
|
+
dialogue_lines,
|
|
265
|
+
archive_milestone.DIALOGUE_HEADING_PATTERN,
|
|
266
|
+
(archive_milestone.DIALOGUE_HEADING_PATTERN,),
|
|
267
|
+
)
|
|
268
|
+
threads: dict[str, list[tuple]] = {}
|
|
269
|
+
for match, section in sections:
|
|
270
|
+
threads.setdefault(field(section, "Thread"), []).append((match, section))
|
|
271
|
+
return threads
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
def thread_records(discussion: Path) -> list[dict]:
|
|
275
|
+
lines = (discussion / "DIALOGUE.md").read_text(encoding="utf-8").splitlines()
|
|
276
|
+
records = []
|
|
277
|
+
for thread, sections in dialogue_threads(lines).items():
|
|
278
|
+
match, section = sections[-1]
|
|
279
|
+
records.append(
|
|
280
|
+
{
|
|
281
|
+
"thread": thread,
|
|
282
|
+
"topic": match.group(5),
|
|
283
|
+
"last_turn": f"D{int(match.group(1)):03d}",
|
|
284
|
+
"status": field(section, "Thread status"),
|
|
285
|
+
}
|
|
286
|
+
)
|
|
287
|
+
return records
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
def pending_records(discussion: Path) -> list[dict]:
|
|
291
|
+
if record_count(discussion) == 0:
|
|
292
|
+
return []
|
|
293
|
+
lines = (discussion / "ANSWERS.md").read_text(encoding="utf-8").splitlines()
|
|
294
|
+
boundaries = (
|
|
295
|
+
archive_milestone.ANSWER_HEADING_PATTERN,
|
|
296
|
+
archive_milestone.DISPOSITION_HEADING_PATTERN,
|
|
297
|
+
)
|
|
298
|
+
answers = numbered_sections(lines, archive_milestone.ANSWER_HEADING_PATTERN, boundaries)
|
|
299
|
+
resolved = disposed_answers(lines)
|
|
300
|
+
pending = []
|
|
301
|
+
for match, section in answers:
|
|
302
|
+
identifier = f"A{int(match.group(1)):03d}"
|
|
303
|
+
if (
|
|
304
|
+
field(section, "Follow-up") == "required"
|
|
305
|
+
and field(section, "Status") in {"final", "NEEDS-USER"}
|
|
306
|
+
and identifier not in resolved
|
|
307
|
+
):
|
|
308
|
+
pending.append(
|
|
309
|
+
{
|
|
310
|
+
"answer": identifier,
|
|
311
|
+
"owner": field(section, "Next owner"),
|
|
312
|
+
"target": field(section, "Target artifact"),
|
|
313
|
+
"status": field(section, "Status"),
|
|
314
|
+
}
|
|
315
|
+
)
|
|
316
|
+
return pending
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
def append_disposition(discussion: Path, project: Path, payload: dict) -> dict:
|
|
320
|
+
answer = required_text(payload, "answer")
|
|
321
|
+
pending = {record["answer"]: record for record in pending_records(discussion)}
|
|
322
|
+
if answer not in pending:
|
|
323
|
+
raise DiscussionError(f"answer is not pending disposition: {answer}")
|
|
324
|
+
disposition_status = required_text(payload, "status")
|
|
325
|
+
if disposition_status not in {
|
|
326
|
+
"applied",
|
|
327
|
+
"acknowledged-no-change",
|
|
328
|
+
"superseded",
|
|
329
|
+
"rejected-by-user",
|
|
330
|
+
}:
|
|
331
|
+
raise DiscussionError("disposition status is invalid")
|
|
332
|
+
expected = pending[answer]
|
|
333
|
+
owner = single_line(payload, "owner")
|
|
334
|
+
artifact = single_line(payload, "artifact")
|
|
335
|
+
expected_owner = (
|
|
336
|
+
"user" if disposition_status == "rejected-by-user" else expected["owner"]
|
|
337
|
+
)
|
|
338
|
+
if owner != expected_owner:
|
|
339
|
+
raise DiscussionError(f"disposition owner must be {expected_owner}")
|
|
340
|
+
if artifact != expected["target"]:
|
|
341
|
+
raise DiscussionError(f"disposition artifact must be {expected['target']}")
|
|
342
|
+
lines = (discussion / "ANSWERS.md").read_text(encoding="utf-8").splitlines()
|
|
343
|
+
boundaries = (
|
|
344
|
+
archive_milestone.ANSWER_HEADING_PATTERN,
|
|
345
|
+
archive_milestone.DISPOSITION_HEADING_PATTERN,
|
|
346
|
+
)
|
|
347
|
+
existing = numbered_sections(
|
|
348
|
+
lines, archive_milestone.DISPOSITION_HEADING_PATTERN, boundaries
|
|
349
|
+
)
|
|
350
|
+
number = len(existing) + 1
|
|
351
|
+
record_date = iso_date(payload)
|
|
352
|
+
disposition = f"""\n## Disposition X{number:03d} — {record_date}
|
|
353
|
+
|
|
354
|
+
- **Answer**: {answer}
|
|
355
|
+
- **Status**: {disposition_status}
|
|
356
|
+
- **Owner**: {owner}
|
|
357
|
+
- **Artifact**: {artifact}
|
|
358
|
+
- **Evidence**: {single_line(payload, "evidence")}
|
|
359
|
+
"""
|
|
360
|
+
files = {
|
|
361
|
+
"DIALOGUE.md": (discussion / "DIALOGUE.md").read_text(encoding="utf-8"),
|
|
362
|
+
"ANSWERS.md": (discussion / "ANSWERS.md").read_text(encoding="utf-8").rstrip()
|
|
363
|
+
+ "\n"
|
|
364
|
+
+ disposition,
|
|
365
|
+
}
|
|
366
|
+
validate_contents(project, files)
|
|
367
|
+
atomic_write(
|
|
368
|
+
project / APPEND_TRANSACTION,
|
|
369
|
+
json.dumps(
|
|
370
|
+
{"schema": "gsd-path/discussion-append/v1", "files": files},
|
|
371
|
+
sort_keys=True,
|
|
372
|
+
)
|
|
373
|
+
+ "\n",
|
|
374
|
+
)
|
|
375
|
+
finish_append(project, discussion)
|
|
376
|
+
return {"answer": answer, "disposition": f"X{number:03d}"}
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
MULTILINE_FIELDS = ("user", "assistant")
|
|
380
|
+
SINGLE_LINE_FIELDS = (
|
|
381
|
+
"topic",
|
|
382
|
+
"thread",
|
|
383
|
+
"question",
|
|
384
|
+
"status",
|
|
385
|
+
"thread_status",
|
|
386
|
+
"conclusion",
|
|
387
|
+
"reasoning",
|
|
388
|
+
"evidence",
|
|
389
|
+
"research",
|
|
390
|
+
"confidence",
|
|
391
|
+
"unresolved",
|
|
392
|
+
"next_owner",
|
|
393
|
+
"target_artifact",
|
|
394
|
+
"follow_up",
|
|
395
|
+
)
|
|
396
|
+
|
|
397
|
+
|
|
398
|
+
def validate_payload_fields(payload: dict) -> None:
|
|
399
|
+
problems = []
|
|
400
|
+
for name in MULTILINE_FIELDS + SINGLE_LINE_FIELDS:
|
|
401
|
+
value = payload.get(name)
|
|
402
|
+
if not isinstance(value, str) or not value.strip():
|
|
403
|
+
problems.append(f"{name} (missing or empty)")
|
|
404
|
+
elif name in SINGLE_LINE_FIELDS and ("\n" in value or "\r" in value):
|
|
405
|
+
problems.append(f"{name} (must be one line)")
|
|
406
|
+
if problems:
|
|
407
|
+
raise DiscussionError("append payload invalid: " + ", ".join(problems))
|
|
408
|
+
|
|
409
|
+
|
|
410
|
+
def required_text(payload: dict, name: str) -> str:
|
|
411
|
+
value = payload.get(name)
|
|
412
|
+
if not isinstance(value, str) or not value.strip():
|
|
413
|
+
raise DiscussionError(f"append payload requires non-empty {name}")
|
|
414
|
+
return value.strip()
|
|
415
|
+
|
|
416
|
+
|
|
417
|
+
def single_line(payload: dict, name: str) -> str:
|
|
418
|
+
value = required_text(payload, name)
|
|
419
|
+
if "\n" in value or "\r" in value:
|
|
420
|
+
raise DiscussionError(f"append payload {name} must be one line")
|
|
421
|
+
return value
|
|
422
|
+
|
|
423
|
+
|
|
424
|
+
def quote_block(value: str) -> str:
|
|
425
|
+
return "\n".join(f"> {line}" if line else ">" for line in value.splitlines())
|
|
426
|
+
|
|
427
|
+
|
|
428
|
+
def indent_block(value: str) -> str:
|
|
429
|
+
return "\n".join(f" {line}" if line else " " for line in value.splitlines())
|
|
430
|
+
|
|
431
|
+
|
|
432
|
+
def iso_date(payload: dict) -> str:
|
|
433
|
+
value = payload.get("date", date.today().isoformat())
|
|
434
|
+
if not isinstance(value, str):
|
|
435
|
+
raise DiscussionError("date must be YYYY-MM-DD")
|
|
436
|
+
try:
|
|
437
|
+
parsed = date.fromisoformat(value)
|
|
438
|
+
except ValueError as error:
|
|
439
|
+
raise DiscussionError("date must be YYYY-MM-DD") from error
|
|
440
|
+
if parsed.isoformat() != value:
|
|
441
|
+
raise DiscussionError("date must be YYYY-MM-DD")
|
|
442
|
+
return value
|
|
443
|
+
|
|
444
|
+
|
|
445
|
+
def append_record(discussion: Path, project: Path, phase: str, status: str, payload: dict) -> dict:
|
|
446
|
+
count = record_count(discussion)
|
|
447
|
+
number = count + 1
|
|
448
|
+
dialogue_id = f"D{number:03d}"
|
|
449
|
+
answer_id = f"A{number:03d}"
|
|
450
|
+
dialogue_text = (discussion / "DIALOGUE.md").read_text(encoding="utf-8")
|
|
451
|
+
dialogue_lines = dialogue_text.splitlines()
|
|
452
|
+
if count:
|
|
453
|
+
archive_milestone.validate_discussion_directory(
|
|
454
|
+
discussion, require_dispositions=False
|
|
455
|
+
)
|
|
456
|
+
validate_payload_fields(payload)
|
|
457
|
+
answers_text = (discussion / "ANSWERS.md").read_text(encoding="utf-8")
|
|
458
|
+
requested_thread = payload["thread"]
|
|
459
|
+
existing_threads = dialogue_threads(dialogue_lines) if count else {}
|
|
460
|
+
if requested_thread == "new":
|
|
461
|
+
thread_number = max((int(value[1:]) for value in existing_threads), default=0) + 1
|
|
462
|
+
thread = f"T{thread_number:03d}"
|
|
463
|
+
reply_to = supersedes = "none"
|
|
464
|
+
elif requested_thread in existing_threads:
|
|
465
|
+
thread = requested_thread
|
|
466
|
+
prior = int(existing_threads[thread][-1][0].group(1))
|
|
467
|
+
reply_to = f"D{prior:03d}"
|
|
468
|
+
prior_answer = f"A{prior:03d}"
|
|
469
|
+
# A disposed answer is settled; a continuation replies without superseding it.
|
|
470
|
+
supersedes = (
|
|
471
|
+
"none"
|
|
472
|
+
if prior_answer in disposed_answers(answers_text.splitlines())
|
|
473
|
+
else prior_answer
|
|
474
|
+
)
|
|
475
|
+
else:
|
|
476
|
+
raise DiscussionError("thread must be new or an existing T### id")
|
|
477
|
+
record_date = iso_date(payload)
|
|
478
|
+
thread_status = single_line(payload, "thread_status")
|
|
479
|
+
answer_status = single_line(payload, "status")
|
|
480
|
+
if thread_status not in {"working", "NEEDS-USER", "final"}:
|
|
481
|
+
raise DiscussionError("thread_status is invalid")
|
|
482
|
+
if answer_status not in {"working", "NEEDS-USER", "final"}:
|
|
483
|
+
raise DiscussionError("status is invalid")
|
|
484
|
+
topic = single_line(payload, "topic")
|
|
485
|
+
evidence = single_line(payload, "evidence")
|
|
486
|
+
research = single_line(payload, "research")
|
|
487
|
+
dialogue = f"""\n### {dialogue_id} — {record_date} — {phase}/{status} — {topic}
|
|
488
|
+
|
|
489
|
+
- **Thread**: {thread}
|
|
490
|
+
- **Reply to**: {reply_to}
|
|
491
|
+
- **User (verbatim)**:
|
|
492
|
+
|
|
493
|
+
{quote_block(required_text(payload, "user"))}
|
|
494
|
+
|
|
495
|
+
- **Assistant**:
|
|
496
|
+
|
|
497
|
+
{indent_block(required_text(payload, "assistant"))}
|
|
498
|
+
|
|
499
|
+
- **Evidence checked**: {evidence}
|
|
500
|
+
- **Research**: {research}
|
|
501
|
+
- **Thread status**: {thread_status}
|
|
502
|
+
"""
|
|
503
|
+
answer = f"""\n## Answer {answer_id} — {record_date} — {topic}
|
|
504
|
+
|
|
505
|
+
- **Thread**: {thread}
|
|
506
|
+
- **Turn**: {dialogue_id}
|
|
507
|
+
- **Supersedes**: {supersedes}
|
|
508
|
+
- **Question**: {single_line(payload, "question")}
|
|
509
|
+
- **Status**: {answer_status}
|
|
510
|
+
- **Phase/status**: {phase}/{status}
|
|
511
|
+
- **Conclusion**: {single_line(payload, "conclusion")}
|
|
512
|
+
- **Reasoning / pushback**: {single_line(payload, "reasoning")}
|
|
513
|
+
- **Evidence**: {evidence}
|
|
514
|
+
- **Research**: {research}
|
|
515
|
+
- **Confidence**: {single_line(payload, "confidence")}
|
|
516
|
+
- **Unresolved**: {single_line(payload, "unresolved")}
|
|
517
|
+
- **Next owner**: {single_line(payload, "next_owner")}
|
|
518
|
+
- **Target artifact**: {single_line(payload, "target_artifact")}
|
|
519
|
+
- **Follow-up**: {single_line(payload, "follow_up")}
|
|
520
|
+
"""
|
|
521
|
+
files = {
|
|
522
|
+
"DIALOGUE.md": dialogue_text.rstrip() + "\n" + dialogue,
|
|
523
|
+
"ANSWERS.md": answers_text.rstrip() + "\n" + answer,
|
|
524
|
+
}
|
|
525
|
+
validate_contents(project, files)
|
|
526
|
+
transaction = project / APPEND_TRANSACTION
|
|
527
|
+
atomic_write(
|
|
528
|
+
transaction,
|
|
529
|
+
json.dumps(
|
|
530
|
+
{"schema": "gsd-path/discussion-append/v1", "files": files},
|
|
531
|
+
sort_keys=True,
|
|
532
|
+
)
|
|
533
|
+
+ "\n",
|
|
534
|
+
)
|
|
535
|
+
finish_append(project, discussion)
|
|
536
|
+
return {"answer": answer_id, "dialogue": dialogue_id, "thread": thread}
|
|
537
|
+
|
|
538
|
+
|
|
539
|
+
def load_payload(path: Path) -> dict:
|
|
540
|
+
if path.is_symlink() or not path.is_file():
|
|
541
|
+
raise DiscussionError(f"append input must be a real file: {path}")
|
|
542
|
+
try:
|
|
543
|
+
payload = json.loads(path.read_text(encoding="utf-8"))
|
|
544
|
+
except (OSError, json.JSONDecodeError) as error:
|
|
545
|
+
raise DiscussionError(f"append input is unreadable: {error}") from error
|
|
546
|
+
if not isinstance(payload, dict):
|
|
547
|
+
raise DiscussionError("append input must contain one JSON object")
|
|
548
|
+
return payload
|
|
549
|
+
|
|
550
|
+
|
|
551
|
+
def parser() -> argparse.ArgumentParser:
|
|
552
|
+
result = argparse.ArgumentParser(description=__doc__)
|
|
553
|
+
result.add_argument(
|
|
554
|
+
"command", choices=("prepare", "append", "pending", "dispose", "threads")
|
|
555
|
+
)
|
|
556
|
+
result.add_argument("--repo", type=Path, required=True)
|
|
557
|
+
result.add_argument("--dialogue-template", type=Path)
|
|
558
|
+
result.add_argument("--answers-template", type=Path)
|
|
559
|
+
result.add_argument("--input", type=Path)
|
|
560
|
+
return result
|
|
561
|
+
|
|
562
|
+
|
|
563
|
+
def main(argv: Optional[Sequence[str]] = None) -> int:
|
|
564
|
+
try:
|
|
565
|
+
arguments = parser().parse_args(argv)
|
|
566
|
+
root, project, state = project_root(arguments.repo)
|
|
567
|
+
with state_lock(state):
|
|
568
|
+
state_context(state)
|
|
569
|
+
if arguments.command in {"pending", "dispose", "threads"}:
|
|
570
|
+
discussion = project / "discuss"
|
|
571
|
+
finish_append(project, discussion)
|
|
572
|
+
if not discussion.exists():
|
|
573
|
+
if arguments.command == "dispose":
|
|
574
|
+
raise DiscussionError("discussion records do not exist")
|
|
575
|
+
result = {arguments.command: []}
|
|
576
|
+
else:
|
|
577
|
+
validate_or_empty(discussion)
|
|
578
|
+
if arguments.command == "pending":
|
|
579
|
+
result = {"pending": pending_records(discussion)}
|
|
580
|
+
elif arguments.command == "threads":
|
|
581
|
+
result = {"threads": thread_records(discussion)}
|
|
582
|
+
else:
|
|
583
|
+
if arguments.input is None:
|
|
584
|
+
raise DiscussionError("dispose requires --input")
|
|
585
|
+
result = append_disposition(
|
|
586
|
+
discussion, project, load_payload(arguments.input)
|
|
587
|
+
)
|
|
588
|
+
else:
|
|
589
|
+
if arguments.dialogue_template is None or arguments.answers_template is None:
|
|
590
|
+
raise DiscussionError("prepare and append require both bundled template paths")
|
|
591
|
+
discussion, phase, status = prepare_locked(
|
|
592
|
+
root,
|
|
593
|
+
project,
|
|
594
|
+
state,
|
|
595
|
+
arguments.dialogue_template.resolve(),
|
|
596
|
+
arguments.answers_template.resolve(),
|
|
597
|
+
)
|
|
598
|
+
if arguments.command == "prepare":
|
|
599
|
+
result = {
|
|
600
|
+
"dialogue": str((discussion / "DIALOGUE.md").resolve()),
|
|
601
|
+
"answers": str((discussion / "ANSWERS.md").resolve()),
|
|
602
|
+
"phase_status": f"{phase}/{status}",
|
|
603
|
+
"records": record_count(discussion),
|
|
604
|
+
}
|
|
605
|
+
else:
|
|
606
|
+
if arguments.input is None:
|
|
607
|
+
raise DiscussionError("append requires --input")
|
|
608
|
+
result = append_record(
|
|
609
|
+
discussion, project, phase, status, load_payload(arguments.input)
|
|
610
|
+
)
|
|
611
|
+
print(json.dumps(result, sort_keys=True))
|
|
612
|
+
return 0
|
|
613
|
+
except (DiscussionError, archive_milestone.ArchiveError) as error:
|
|
614
|
+
print(str(error), file=sys.stderr)
|
|
615
|
+
return 1
|
|
616
|
+
|
|
617
|
+
|
|
618
|
+
if __name__ == "__main__":
|
|
619
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# GSD Path Discussion — Answers
|
|
2
|
+
|
|
3
|
+
<!-- Written by $gsd-path-discuss. Append one record per turn; never rewrite prior answers. -->
|
|
4
|
+
|
|
5
|
+
## Answer A001 — <YYYY-MM-DD> — <topic>
|
|
6
|
+
|
|
7
|
+
- **Thread**: T001
|
|
8
|
+
- **Turn**: D001
|
|
9
|
+
- **Supersedes**: none | A###
|
|
10
|
+
- **Question**: <the question being answered>
|
|
11
|
+
- **Status**: working | NEEDS-USER | final
|
|
12
|
+
- **Phase/status**: <phase>/<status>
|
|
13
|
+
- **Conclusion**: <answer first>
|
|
14
|
+
- **Reasoning / pushback**: <why, including rejected premise or safer alternative>
|
|
15
|
+
- **Evidence**: <exact repository paths and line references; commands only if run>
|
|
16
|
+
- **Research**: <not needed — reason, or query, checked sources, and confidence>
|
|
17
|
+
- **Confidence**: high | medium | low | unverifiable
|
|
18
|
+
- **Unresolved**: <none, or NEEDS-USER / RESEARCH question>
|
|
19
|
+
- **Next owner**: <none, user, or named GSD Path phase>
|
|
20
|
+
- **Target artifact**: <none or canonical .project path>
|
|
21
|
+
- **Follow-up**: none | required
|
|
22
|
+
|
|
23
|
+
## Disposition X001 — <YYYY-MM-DD>
|
|
24
|
+
|
|
25
|
+
- **Answer**: A###
|
|
26
|
+
- **Status**: applied | acknowledged-no-change | superseded | rejected-by-user
|
|
27
|
+
- **Owner**: <phase or router>
|
|
28
|
+
- **Artifact**: <canonical path or none>
|
|
29
|
+
- **Evidence**: <what was changed, why no change was needed, or the user ruling>
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# GSD Path Discussion — Dialogue
|
|
2
|
+
|
|
3
|
+
<!-- Written by $gsd-path-discuss. Append turns; never rewrite prior turns. -->
|
|
4
|
+
|
|
5
|
+
## Turns
|
|
6
|
+
|
|
7
|
+
### D001 — <YYYY-MM-DD> — <phase>/<status> — <topic>
|
|
8
|
+
|
|
9
|
+
- **Thread**: T001
|
|
10
|
+
- **Reply to**: none | D###
|
|
11
|
+
- **User (verbatim)**:
|
|
12
|
+
|
|
13
|
+
> <copy the user's message exactly>
|
|
14
|
+
|
|
15
|
+
- **Assistant**:
|
|
16
|
+
|
|
17
|
+
<copy the answer returned to the user>
|
|
18
|
+
|
|
19
|
+
- **Evidence checked**: <repository paths, line references, commands actually run, or none>
|
|
20
|
+
- **Research**: <not needed — reason, or query/sources/confidence>
|
|
21
|
+
- **Thread status**: working | NEEDS-USER | final
|