@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,714 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# gsd-path project runtime
|
|
3
|
+
"""Compute the wave review finding sets for build step 7 without model reasoning.
|
|
4
|
+
|
|
5
|
+
The orchestrator names the wave and cycle. This helper reads PLAN.md Config
|
|
6
|
+
(`max_review_cycles`, `finding_skeptics`, `wave_budget`), the cycle's lens or
|
|
7
|
+
canonical review files, and every skeptic file for this wave. It groups
|
|
8
|
+
blocking findings by canonical criterion locator (`t001_ac2`, `sc3`,
|
|
9
|
+
`t001_files`, `t001_interface_contract`), collapses true duplicate
|
|
10
|
+
observations, selects the groups that need a skeptic, carries refutations
|
|
11
|
+
forward across cycles, batches fix-task groups by disjoint file scope, keeps
|
|
12
|
+
structural blockers out of skeptic accounting, and reports whether the cycle
|
|
13
|
+
cap is reached. It never decides the wave verdict.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import argparse
|
|
19
|
+
import json
|
|
20
|
+
import re
|
|
21
|
+
import sys
|
|
22
|
+
from pathlib import Path
|
|
23
|
+
from typing import Dict, List, Optional, Sequence
|
|
24
|
+
|
|
25
|
+
DEFAULT_PROJECT_DIR = ".project"
|
|
26
|
+
DEFAULT_MAX_REVIEW_CYCLES = 3
|
|
27
|
+
CONFIG_KEYS = ("max_review_cycles", "wave_budget", "review_panel", "finding_skeptics")
|
|
28
|
+
CONFIG_LINE = re.compile(r"^-\s*(?P<key>[A-Za-z0-9_-]+):\s*(?P<value>.*?)\s*$")
|
|
29
|
+
COMMENT_PATTERN = re.compile(r"<!--.*?-->", re.DOTALL)
|
|
30
|
+
PLAN_WAVE_HEADING = re.compile(r"(?m)^## Wave (?P<wave>\d+) — (?P<title>.+)$")
|
|
31
|
+
TASK_FILE_NAME = re.compile(r"^(?P<id>T\d{3})-[a-z0-9][a-z0-9-]*\.md$")
|
|
32
|
+
WAVE_TASK_HEADING = re.compile(
|
|
33
|
+
r"^## (?P<task>T\d{3}) — (?P<title>.+): (?P<verdict>pass|fail)$"
|
|
34
|
+
)
|
|
35
|
+
WAVE_SC_HEADING = re.compile(r"^### (?P<sc>SC[1-9]\d*) — (?P<rest>.+)$")
|
|
36
|
+
NUMBERED_ITEM = re.compile(r"^(\d+)\.\s+(\S.*)$")
|
|
37
|
+
SKEPTIC_FILE_NAME = re.compile(
|
|
38
|
+
r"^wave-(?P<wave>[1-9]\d*)\.cycle(?P<cycle>[1-9]\d*)\.skeptic-(?P<locator>[a-z0-9_]+)\.md$"
|
|
39
|
+
)
|
|
40
|
+
SKEPTIC_OBSERVATION = re.compile(r"^### Observation (?P<n>\d+) — (?P<lens>contract|adversarial|both)$")
|
|
41
|
+
SKEPTIC_OBSERVATION_VERDICT = re.compile(r"^### Observation (?P<n>\d+): (?P<verdict>refuted|stands)$")
|
|
42
|
+
FAIL_MARKER = "- ❌ "
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class ReviewFindingsError(Exception):
|
|
46
|
+
"""Raised when an input cannot be read as the templates define it."""
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _normalize(text: str) -> str:
|
|
50
|
+
return " ".join(text.split())
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _strip_comments(text: str) -> str:
|
|
54
|
+
return COMMENT_PATTERN.sub("", text)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _section(text: str, heading: str) -> Optional[str]:
|
|
58
|
+
match = re.search(rf"(?m)^## {re.escape(heading)}\s*$", text)
|
|
59
|
+
if not match:
|
|
60
|
+
return None
|
|
61
|
+
rest = text[match.end() :]
|
|
62
|
+
following = re.search(r"(?m)^## ", rest)
|
|
63
|
+
return rest[: following.start()] if following else rest
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _read(path: Path, label: str) -> str:
|
|
67
|
+
try:
|
|
68
|
+
return path.read_text(encoding="utf-8")
|
|
69
|
+
except OSError as error:
|
|
70
|
+
raise ReviewFindingsError(f"{label} is not readable: {path} ({error.strerror})")
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
# --- PLAN.md ---------------------------------------------------------------
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def parse_config(plan_text: str) -> Dict[str, object]:
|
|
77
|
+
"""Read and validate the PLAN.md Config keys this helper owns."""
|
|
78
|
+
|
|
79
|
+
section = _section(_strip_comments(plan_text), "Config")
|
|
80
|
+
if section is None:
|
|
81
|
+
raise ReviewFindingsError("PLAN.md has no ## Config section")
|
|
82
|
+
seen: Dict[str, str] = {}
|
|
83
|
+
for line in section.splitlines():
|
|
84
|
+
match = CONFIG_LINE.match(line.strip())
|
|
85
|
+
if not match:
|
|
86
|
+
continue
|
|
87
|
+
key, value = match.group("key"), match.group("value")
|
|
88
|
+
if key not in CONFIG_KEYS:
|
|
89
|
+
raise ReviewFindingsError(
|
|
90
|
+
f"PLAN.md Config key `{key}` is unknown; expected one of {', '.join(CONFIG_KEYS)}"
|
|
91
|
+
)
|
|
92
|
+
if key in seen:
|
|
93
|
+
raise ReviewFindingsError(f"PLAN.md Config repeats `{key}`")
|
|
94
|
+
seen[key] = value
|
|
95
|
+
cycles_raw = seen.get("max_review_cycles", str(DEFAULT_MAX_REVIEW_CYCLES))
|
|
96
|
+
if not cycles_raw.isdigit() or int(cycles_raw) < 1:
|
|
97
|
+
raise ReviewFindingsError(
|
|
98
|
+
f"PLAN.md Config `max_review_cycles` must be a positive integer, got `{cycles_raw}`"
|
|
99
|
+
)
|
|
100
|
+
skeptics = seen.get("finding_skeptics", "off")
|
|
101
|
+
if skeptics not in {"off", "on"}:
|
|
102
|
+
raise ReviewFindingsError(
|
|
103
|
+
f"PLAN.md Config `finding_skeptics` must be `off` or `on`, got `{skeptics}`"
|
|
104
|
+
)
|
|
105
|
+
budget = seen.get("wave_budget", "none")
|
|
106
|
+
if not budget:
|
|
107
|
+
raise ReviewFindingsError("PLAN.md Config `wave_budget` must be `none` or a resource cap")
|
|
108
|
+
return {
|
|
109
|
+
"max_review_cycles": int(cycles_raw),
|
|
110
|
+
"finding_skeptics": skeptics,
|
|
111
|
+
"wave_budget": budget,
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def wave_depth(plan_text: str, wave: int) -> str:
|
|
116
|
+
headings = list(PLAN_WAVE_HEADING.finditer(plan_text))
|
|
117
|
+
for index, heading in enumerate(headings):
|
|
118
|
+
if int(heading.group("wave")) != wave:
|
|
119
|
+
continue
|
|
120
|
+
end = headings[index + 1].start() if index + 1 < len(headings) else len(plan_text)
|
|
121
|
+
depth = re.search(r"(?m)^Review depth:\s*(\S+)", plan_text[heading.end() : end])
|
|
122
|
+
if not depth or depth.group(1) not in {"full", "deep", "verify-only"}:
|
|
123
|
+
raise ReviewFindingsError(f"PLAN.md Wave {wave} has an invalid Review depth")
|
|
124
|
+
return depth.group(1)
|
|
125
|
+
raise ReviewFindingsError(f"PLAN.md has no Wave {wave}")
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
# --- task files ------------------------------------------------------------
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def _frontmatter_list(frontmatter: str, key: str) -> List[str]:
|
|
132
|
+
inline = re.search(rf"(?m)^{key}:\s*\[(?P<body>.*)\]\s*(?:#.*)?$", frontmatter)
|
|
133
|
+
if inline:
|
|
134
|
+
return [part.strip().strip("\"'") for part in inline.group("body").split(",") if part.strip()]
|
|
135
|
+
block = re.search(rf"(?m)^{key}:\s*(?:#.*)?$\n(?P<items>(?:[ \t]+-\s+.*\n?)*)", frontmatter)
|
|
136
|
+
if not block:
|
|
137
|
+
return []
|
|
138
|
+
return [
|
|
139
|
+
_normalize(re.sub(r"\s+#.*$", "", item.strip()[1:])).strip("\"'")
|
|
140
|
+
for item in block.group("items").splitlines()
|
|
141
|
+
if item.strip().startswith("-")
|
|
142
|
+
]
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def _numbered_items(section: Optional[str]) -> List[str]:
|
|
146
|
+
items: List[str] = []
|
|
147
|
+
if section is None:
|
|
148
|
+
return items
|
|
149
|
+
for line in _strip_comments(section).splitlines():
|
|
150
|
+
match = NUMBERED_ITEM.match(line.strip())
|
|
151
|
+
if match:
|
|
152
|
+
items.append(_normalize(match.group(2)))
|
|
153
|
+
elif items and line.startswith(" ") and line.strip():
|
|
154
|
+
items[-1] = _normalize(items[-1] + " " + line.strip())
|
|
155
|
+
return items
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def _bullets(section: Optional[str]) -> List[str]:
|
|
159
|
+
if section is None:
|
|
160
|
+
return []
|
|
161
|
+
return [
|
|
162
|
+
_normalize(line.strip()[1:])
|
|
163
|
+
for line in _strip_comments(section).splitlines()
|
|
164
|
+
if line.strip().startswith("- ")
|
|
165
|
+
]
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def load_wave_tasks(project: Path, wave: int) -> Dict[str, dict]:
|
|
169
|
+
tasks_dir = project / "tasks"
|
|
170
|
+
if not tasks_dir.is_dir():
|
|
171
|
+
raise ReviewFindingsError(f"tasks directory missing: {tasks_dir}")
|
|
172
|
+
tasks: Dict[str, dict] = {}
|
|
173
|
+
for path in sorted(tasks_dir.iterdir()):
|
|
174
|
+
named = TASK_FILE_NAME.fullmatch(path.name)
|
|
175
|
+
if not named:
|
|
176
|
+
continue
|
|
177
|
+
text = _read(path, f"task {path.name}")
|
|
178
|
+
parts = text.split("---", 2)
|
|
179
|
+
if len(parts) < 3:
|
|
180
|
+
raise ReviewFindingsError(f"task {path.name} has no frontmatter")
|
|
181
|
+
frontmatter = parts[1]
|
|
182
|
+
task_wave = re.search(r"(?m)^wave:\s*(\d+)", frontmatter)
|
|
183
|
+
if not task_wave or int(task_wave.group(1)) != wave:
|
|
184
|
+
continue
|
|
185
|
+
body = parts[2]
|
|
186
|
+
tasks[named.group("id")] = {
|
|
187
|
+
"id": named.group("id"),
|
|
188
|
+
"path": str(path),
|
|
189
|
+
"files": _frontmatter_list(frontmatter, "files"),
|
|
190
|
+
"acceptance": _numbered_items(_section(body, "Acceptance criteria")),
|
|
191
|
+
"interface": [
|
|
192
|
+
item for item in _bullets(_section(body, "Interface contract")) if item != "None"
|
|
193
|
+
],
|
|
194
|
+
"owned": [
|
|
195
|
+
item for item in _bullets(_section(body, "Intent coverage")) if item != "None"
|
|
196
|
+
],
|
|
197
|
+
}
|
|
198
|
+
if not tasks:
|
|
199
|
+
raise ReviewFindingsError(f"PLAN.md Wave {wave} has no task files")
|
|
200
|
+
return tasks
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
# --- lens files ------------------------------------------------------------
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def review_paths(project: Path, wave: int, cycle: int, depth: str) -> Dict[str, Path]:
|
|
207
|
+
review = project / "review"
|
|
208
|
+
if depth == "deep":
|
|
209
|
+
return {
|
|
210
|
+
lens: review / f"wave-{wave}.cycle{cycle}.{lens}.md"
|
|
211
|
+
for lens in ("contract", "adversarial")
|
|
212
|
+
}
|
|
213
|
+
return {"canonical": review / f"wave-{wave}.cycle{cycle}.md"}
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def _fail_items(lines: Sequence[str]) -> List[str]:
|
|
217
|
+
"""Collect `- ❌` items with their indented continuation lines."""
|
|
218
|
+
|
|
219
|
+
items: List[str] = []
|
|
220
|
+
open_item = False
|
|
221
|
+
for line in lines:
|
|
222
|
+
stripped = line.strip()
|
|
223
|
+
if stripped.startswith(FAIL_MARKER):
|
|
224
|
+
items.append(stripped[len(FAIL_MARKER) :])
|
|
225
|
+
open_item = True
|
|
226
|
+
elif open_item and line.startswith(" ") and stripped and not stripped.startswith("- "):
|
|
227
|
+
items[-1] += " " + stripped
|
|
228
|
+
elif stripped.startswith("- ") or stripped.startswith("#"):
|
|
229
|
+
open_item = False
|
|
230
|
+
return [_normalize(item) for item in items]
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def _criterion_of(observation: str) -> str:
|
|
234
|
+
return _normalize(observation.split(" — ", 1)[0])
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def parse_lens(text: str, lens: str, tasks: Dict[str, dict]) -> dict:
|
|
238
|
+
"""Return the lens verdict and every blocking observation with its locator."""
|
|
239
|
+
|
|
240
|
+
verdict = re.search(r"(?m)^Wave verdict:\s*(\S+)", text)
|
|
241
|
+
if not verdict or verdict.group(1) not in {"pass", "blocked"}:
|
|
242
|
+
raise ReviewFindingsError(f"{lens} lens has no valid Wave verdict")
|
|
243
|
+
lines = _strip_comments(text).splitlines()
|
|
244
|
+
findings: List[dict] = []
|
|
245
|
+
problems: List[str] = []
|
|
246
|
+
task_headings = [
|
|
247
|
+
(index, match) for index, line in enumerate(lines)
|
|
248
|
+
if (match := WAVE_TASK_HEADING.fullmatch(line.strip()))
|
|
249
|
+
]
|
|
250
|
+
for start, heading in task_headings:
|
|
251
|
+
task_id = heading.group("task")
|
|
252
|
+
end = next(
|
|
253
|
+
(index for index in range(start + 1, len(lines)) if lines[index].startswith("## ")),
|
|
254
|
+
len(lines),
|
|
255
|
+
)
|
|
256
|
+
if task_id not in tasks:
|
|
257
|
+
problems.append(f"{lens} lens reviews {task_id}, which is not a wave task")
|
|
258
|
+
continue
|
|
259
|
+
task = tasks[task_id]
|
|
260
|
+
block = lines[start + 1 : end]
|
|
261
|
+
zones = {"criteria": [], "warnings": [], "violations": []}
|
|
262
|
+
zone = "criteria"
|
|
263
|
+
for line in block:
|
|
264
|
+
label = line.strip().casefold()
|
|
265
|
+
if label.startswith("warnings (non-blocking)"):
|
|
266
|
+
zone = "warnings"
|
|
267
|
+
continue
|
|
268
|
+
if label.startswith("contract violations (blocking)"):
|
|
269
|
+
zone = "violations"
|
|
270
|
+
continue
|
|
271
|
+
zones[zone].append(line)
|
|
272
|
+
for observation in _fail_items(zones["criteria"]):
|
|
273
|
+
criterion = _criterion_of(observation)
|
|
274
|
+
locator = None
|
|
275
|
+
if criterion in task["acceptance"]:
|
|
276
|
+
locator = f"{task_id.lower()}_ac{task['acceptance'].index(criterion) + 1}"
|
|
277
|
+
elif criterion in task["interface"]:
|
|
278
|
+
locator = f"{task_id.lower()}_interface_contract"
|
|
279
|
+
if locator is None:
|
|
280
|
+
problems.append(
|
|
281
|
+
f"{lens} lens {task_id} finding names no task criterion: {criterion}"
|
|
282
|
+
)
|
|
283
|
+
continue
|
|
284
|
+
findings.append(
|
|
285
|
+
{"locator": locator, "criterion": criterion, "task": task_id,
|
|
286
|
+
"lens": lens, "observation": observation}
|
|
287
|
+
)
|
|
288
|
+
for observation in _fail_items(zones["violations"]):
|
|
289
|
+
findings.append(
|
|
290
|
+
{"locator": f"{task_id.lower()}_files", "criterion": _criterion_of(observation),
|
|
291
|
+
"task": task_id, "lens": lens, "observation": observation}
|
|
292
|
+
)
|
|
293
|
+
coverage = _section("\n".join(lines), "Intent coverage")
|
|
294
|
+
if coverage is not None:
|
|
295
|
+
coverage_lines = coverage.splitlines()
|
|
296
|
+
sc_headings = [
|
|
297
|
+
(index, match) for index, line in enumerate(coverage_lines)
|
|
298
|
+
if (match := WAVE_SC_HEADING.fullmatch(line.strip()))
|
|
299
|
+
]
|
|
300
|
+
for position, (start, heading) in enumerate(sc_headings):
|
|
301
|
+
text_part, _sep, sc_verdict = heading.group("rest").rpartition(":")
|
|
302
|
+
if sc_verdict.strip() != "fail":
|
|
303
|
+
continue
|
|
304
|
+
sc_id = heading.group("sc")
|
|
305
|
+
end = sc_headings[position + 1][0] if position + 1 < len(sc_headings) else len(coverage_lines)
|
|
306
|
+
observations = _fail_items(coverage_lines[start + 1 : end])
|
|
307
|
+
if not observations:
|
|
308
|
+
problems.append(f"{lens} lens {sc_id} fails without a ❌ observation")
|
|
309
|
+
continue
|
|
310
|
+
owners = [task_id for task_id, task in tasks.items() if sc_id in task["owned"]]
|
|
311
|
+
for observation in observations:
|
|
312
|
+
findings.append(
|
|
313
|
+
{"locator": sc_id.lower(), "criterion": _normalize(text_part),
|
|
314
|
+
"task": ", ".join(owners), "tasks": owners, "lens": lens,
|
|
315
|
+
"observation": observation}
|
|
316
|
+
)
|
|
317
|
+
return {"verdict": verdict.group(1), "findings": findings, "problems": problems}
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
# --- skeptic files ---------------------------------------------------------
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
def parse_skeptic(path: Path, wave: int, expected_observations: Optional[Sequence[str]] = None) -> dict:
|
|
324
|
+
"""Validate one skeptic file against the template; return its verdict and observations."""
|
|
325
|
+
|
|
326
|
+
named = SKEPTIC_FILE_NAME.fullmatch(path.name)
|
|
327
|
+
if not named or int(named.group("wave")) != wave:
|
|
328
|
+
raise ReviewFindingsError(f"skeptic file name does not belong to wave {wave}: {path.name}")
|
|
329
|
+
text = _strip_comments(_read(path, f"skeptic {path.name}"))
|
|
330
|
+
locator = re.search(r"(?m)^- Criterion locator:\s*(\S+)\s*$", text)
|
|
331
|
+
if not locator or locator.group(1) != named.group("locator"):
|
|
332
|
+
raise ReviewFindingsError(f"{path.name} Criterion locator does not match its filename")
|
|
333
|
+
verdict_section = _section(text, "Verdict")
|
|
334
|
+
verdict = _normalize(verdict_section or "")
|
|
335
|
+
if verdict not in {"refuted", "stands"}:
|
|
336
|
+
raise ReviewFindingsError(f"{path.name} Verdict must be `refuted` or `stands`")
|
|
337
|
+
observations: List[str] = []
|
|
338
|
+
lines = (_section(text, "Observations") or "").splitlines()
|
|
339
|
+
starts = [(index, int(match.group("n"))) for index, line in enumerate(lines)
|
|
340
|
+
if (match := SKEPTIC_OBSERVATION.match(line.strip()))]
|
|
341
|
+
observation_numbers = {number for _, number in starts}
|
|
342
|
+
if len(observation_numbers) != len(starts):
|
|
343
|
+
raise ReviewFindingsError(f"{path.name} has duplicate observation numbers")
|
|
344
|
+
for position, (start, _) in enumerate(starts):
|
|
345
|
+
end = starts[position + 1][0] if position + 1 < len(starts) else len(lines)
|
|
346
|
+
observations.append(_normalize(" ".join(lines[start + 1 : end])))
|
|
347
|
+
if not observations or any(not item for item in observations):
|
|
348
|
+
raise ReviewFindingsError(f"{path.name} records no observation text")
|
|
349
|
+
if expected_observations is not None and not {
|
|
350
|
+
_normalize(item) for item in expected_observations
|
|
351
|
+
} <= set(observations):
|
|
352
|
+
raise ReviewFindingsError(f"{path.name} omits dispatched observations")
|
|
353
|
+
observation_verdicts = [
|
|
354
|
+
(int(match.group("n")), match.group("verdict"))
|
|
355
|
+
for line in (_section(text, "Observation verdicts") or "").splitlines()
|
|
356
|
+
if (match := SKEPTIC_OBSERVATION_VERDICT.match(line.strip()))
|
|
357
|
+
]
|
|
358
|
+
verdict_numbers = {number for number, _ in observation_verdicts}
|
|
359
|
+
if len(verdict_numbers) != len(observation_verdicts) or verdict_numbers != observation_numbers:
|
|
360
|
+
raise ReviewFindingsError(
|
|
361
|
+
f"{path.name} must have exactly one matching verdict per observation number"
|
|
362
|
+
)
|
|
363
|
+
if verdict == "refuted" and any(item != "refuted" for _, item in observation_verdicts):
|
|
364
|
+
raise ReviewFindingsError(
|
|
365
|
+
f"{path.name} records Verdict refuted while an observation stands"
|
|
366
|
+
)
|
|
367
|
+
return {
|
|
368
|
+
"path": str(path),
|
|
369
|
+
"cycle": int(named.group("cycle")),
|
|
370
|
+
"locator": named.group("locator"),
|
|
371
|
+
"verdict": verdict,
|
|
372
|
+
"observations": observations,
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
def load_skeptics(project: Path, wave: int, cycle: int) -> tuple:
|
|
377
|
+
"""Return (skeptics by cycle then locator, structural blockers for invalid files)."""
|
|
378
|
+
|
|
379
|
+
review = project / "review"
|
|
380
|
+
by_cycle: Dict[int, Dict[str, dict]] = {}
|
|
381
|
+
blockers: List[dict] = []
|
|
382
|
+
if not review.is_dir():
|
|
383
|
+
return by_cycle, blockers
|
|
384
|
+
for path in sorted(review.iterdir()):
|
|
385
|
+
named = SKEPTIC_FILE_NAME.fullmatch(path.name)
|
|
386
|
+
if not named or int(named.group("wave")) != wave or int(named.group("cycle")) > cycle:
|
|
387
|
+
continue
|
|
388
|
+
try:
|
|
389
|
+
parsed = parse_skeptic(path, wave)
|
|
390
|
+
except ReviewFindingsError as error:
|
|
391
|
+
blockers.append({"kind": "invalid-skeptic", "path": str(path), "detail": str(error)})
|
|
392
|
+
continue
|
|
393
|
+
by_cycle.setdefault(parsed["cycle"], {})[parsed["locator"]] = parsed
|
|
394
|
+
return by_cycle, blockers
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
# --- grouping and dispositions ---------------------------------------------
|
|
398
|
+
|
|
399
|
+
|
|
400
|
+
def group_findings(findings: Sequence[dict]) -> List[dict]:
|
|
401
|
+
"""Group by locator; collapse only observations whose text is identical."""
|
|
402
|
+
|
|
403
|
+
groups: Dict[str, dict] = {}
|
|
404
|
+
for finding in findings:
|
|
405
|
+
group = groups.setdefault(
|
|
406
|
+
finding["locator"],
|
|
407
|
+
{"locator": finding["locator"], "criterion": finding["criterion"],
|
|
408
|
+
"tasks": [], "lenses": [], "observations": []},
|
|
409
|
+
)
|
|
410
|
+
for task_id in finding.get("tasks", [finding["task"]]):
|
|
411
|
+
if task_id and task_id not in group["tasks"]:
|
|
412
|
+
group["tasks"].append(task_id)
|
|
413
|
+
if finding["lens"] not in group["lenses"]:
|
|
414
|
+
group["lenses"].append(finding["lens"])
|
|
415
|
+
if all(item["text"] != finding["observation"] for item in group["observations"]):
|
|
416
|
+
group["observations"].append({"lens": finding["lens"], "text": finding["observation"]})
|
|
417
|
+
return [groups[key] for key in sorted(groups)]
|
|
418
|
+
|
|
419
|
+
|
|
420
|
+
def _earlier_lens_locators(project: Path, wave: int, cycle: int, depth: str, tasks: Dict[str, dict]) -> Dict[str, int]:
|
|
421
|
+
first_seen: Dict[str, int] = {}
|
|
422
|
+
for earlier in range(1, cycle):
|
|
423
|
+
for lens, path in review_paths(project, wave, earlier, depth).items():
|
|
424
|
+
if not path.is_file():
|
|
425
|
+
continue
|
|
426
|
+
try:
|
|
427
|
+
parsed = parse_lens(path.read_text(encoding="utf-8"), lens, tasks)
|
|
428
|
+
except (ReviewFindingsError, OSError):
|
|
429
|
+
continue
|
|
430
|
+
for finding in parsed["findings"]:
|
|
431
|
+
first_seen.setdefault(finding["locator"], earlier)
|
|
432
|
+
return first_seen
|
|
433
|
+
|
|
434
|
+
|
|
435
|
+
def _fix_batches(groups: Sequence[dict], tasks: Dict[str, dict]) -> List[dict]:
|
|
436
|
+
"""One batch per disjoint file scope: groups whose task files overlap share a batch."""
|
|
437
|
+
|
|
438
|
+
batches: List[dict] = []
|
|
439
|
+
for group in groups:
|
|
440
|
+
files = sorted({path for task_id in group["tasks"] for path in tasks.get(task_id, {}).get("files", [])})
|
|
441
|
+
merged = {"locators": [group["locator"]], "files": files}
|
|
442
|
+
keep: List[dict] = []
|
|
443
|
+
for batch in batches:
|
|
444
|
+
if set(batch["files"]) & set(files):
|
|
445
|
+
merged["locators"] = batch["locators"] + merged["locators"]
|
|
446
|
+
merged["files"] = sorted(set(batch["files"]) | set(merged["files"]))
|
|
447
|
+
else:
|
|
448
|
+
keep.append(batch)
|
|
449
|
+
keep.append(merged)
|
|
450
|
+
batches = keep
|
|
451
|
+
return batches
|
|
452
|
+
|
|
453
|
+
|
|
454
|
+
def compute(
|
|
455
|
+
repo: Path,
|
|
456
|
+
project_dir: str,
|
|
457
|
+
wave: int,
|
|
458
|
+
cycle: int,
|
|
459
|
+
helper_failures: Sequence[str] = (),
|
|
460
|
+
) -> dict:
|
|
461
|
+
project = repo / project_dir
|
|
462
|
+
plan_path = project / "plan" / "PLAN.md"
|
|
463
|
+
plan_text = _read(plan_path, "PLAN.md")
|
|
464
|
+
config = parse_config(plan_text)
|
|
465
|
+
depth = wave_depth(plan_text, wave)
|
|
466
|
+
tasks = load_wave_tasks(project, wave)
|
|
467
|
+
structural: List[dict] = [
|
|
468
|
+
{"kind": "helper-failure", "path": None, "detail": item} for item in helper_failures
|
|
469
|
+
]
|
|
470
|
+
findings: List[dict] = []
|
|
471
|
+
verdicts: Dict[str, Optional[str]] = {}
|
|
472
|
+
for lens, path in review_paths(project, wave, cycle, depth).items():
|
|
473
|
+
if not path.is_file():
|
|
474
|
+
structural.append({"kind": "missing-artifact", "path": str(path), "detail": f"{lens} review file is missing"})
|
|
475
|
+
verdicts[lens] = None
|
|
476
|
+
continue
|
|
477
|
+
try:
|
|
478
|
+
parsed = parse_lens(_read(path, f"{lens} review"), lens, tasks)
|
|
479
|
+
except ReviewFindingsError as error:
|
|
480
|
+
structural.append({"kind": "invalid-artifact", "path": str(path), "detail": str(error)})
|
|
481
|
+
verdicts[lens] = None
|
|
482
|
+
continue
|
|
483
|
+
verdicts[lens] = parsed["verdict"]
|
|
484
|
+
findings.extend(parsed["findings"])
|
|
485
|
+
for problem in parsed["problems"]:
|
|
486
|
+
structural.append({"kind": "invalid-artifact", "path": str(path), "detail": problem})
|
|
487
|
+
if parsed["verdict"] == "blocked" and not parsed["findings"] and not parsed["problems"]:
|
|
488
|
+
structural.append({"kind": "invalid-artifact", "path": str(path), "detail": f"{lens} lens is blocked without a ❌ finding"})
|
|
489
|
+
|
|
490
|
+
skeptics_by_cycle, skeptic_blockers = load_skeptics(project, wave, cycle)
|
|
491
|
+
structural.extend(skeptic_blockers)
|
|
492
|
+
panel = None
|
|
493
|
+
panel_path = project / "review" / f"wave-{wave}.cycle{cycle}.panel.md"
|
|
494
|
+
if panel_path.is_file():
|
|
495
|
+
actionable = re.search(r"(?m)^Actionable:\s*(\d+)\s*$", _read(panel_path, "panel"))
|
|
496
|
+
if not actionable:
|
|
497
|
+
structural.append({"kind": "invalid-artifact", "path": str(panel_path), "detail": "panel file has no Actionable count"})
|
|
498
|
+
else:
|
|
499
|
+
panel = {"path": str(panel_path), "actionable": int(actionable.group(1))}
|
|
500
|
+
|
|
501
|
+
skeptics_active = config["finding_skeptics"] == "on" and depth == "deep"
|
|
502
|
+
first_seen = _earlier_lens_locators(project, wave, cycle, depth, tasks)
|
|
503
|
+
earlier_refuted: Dict[str, dict] = {}
|
|
504
|
+
for earlier in sorted(skeptics_by_cycle):
|
|
505
|
+
if earlier >= cycle:
|
|
506
|
+
continue
|
|
507
|
+
for locator, skeptic in skeptics_by_cycle[earlier].items():
|
|
508
|
+
if skeptic["verdict"] == "refuted":
|
|
509
|
+
earlier_refuted.setdefault(locator, skeptic)
|
|
510
|
+
current_skeptics = skeptics_by_cycle.get(cycle, {})
|
|
511
|
+
|
|
512
|
+
groups = group_findings(findings)
|
|
513
|
+
for group in groups:
|
|
514
|
+
locator = group["locator"]
|
|
515
|
+
group["first_seen_cycle"] = first_seen.get(locator, cycle)
|
|
516
|
+
group["repeat"] = locator in first_seen
|
|
517
|
+
group["skeptic"] = None
|
|
518
|
+
earlier = earlier_refuted.get(locator)
|
|
519
|
+
current = current_skeptics.get(locator)
|
|
520
|
+
if current is not None:
|
|
521
|
+
try:
|
|
522
|
+
parse_skeptic(Path(current["path"]), wave,
|
|
523
|
+
[item["text"] for item in group["observations"]])
|
|
524
|
+
except ReviewFindingsError as error:
|
|
525
|
+
structural.append({"kind": "invalid-skeptic", "path": current["path"], "detail": str(error)})
|
|
526
|
+
current = None
|
|
527
|
+
if not skeptics_active:
|
|
528
|
+
group["disposition"] = "fix"
|
|
529
|
+
elif earlier is not None:
|
|
530
|
+
texts = {item["text"] for item in group["observations"]}
|
|
531
|
+
group["skeptic"] = {"cycle": earlier["cycle"], "verdict": "refuted", "path": earlier["path"]}
|
|
532
|
+
if texts <= set(earlier["observations"]):
|
|
533
|
+
group["disposition"] = "refuted-earlier"
|
|
534
|
+
else:
|
|
535
|
+
group["disposition"] = "fix"
|
|
536
|
+
group["new_evidence"] = sorted(texts - set(earlier["observations"]))
|
|
537
|
+
elif current is not None:
|
|
538
|
+
group["skeptic"] = {"cycle": cycle, "verdict": current["verdict"], "path": current["path"]}
|
|
539
|
+
group["disposition"] = "refuted" if current["verdict"] == "refuted" else "fix"
|
|
540
|
+
else:
|
|
541
|
+
group["disposition"] = "needs-skeptic"
|
|
542
|
+
|
|
543
|
+
refuted = [g["locator"] for g in groups if g["disposition"] in {"refuted", "refuted-earlier"}]
|
|
544
|
+
fix_groups = [g for g in groups if g["disposition"] == "fix"]
|
|
545
|
+
skeptic_groups = [g["locator"] for g in groups if g["disposition"] == "needs-skeptic"]
|
|
546
|
+
lens_blocked = any(value != "pass" for value in verdicts.values())
|
|
547
|
+
cap_reached = cycle >= config["max_review_cycles"]
|
|
548
|
+
return {
|
|
549
|
+
"status": "ok",
|
|
550
|
+
"wave": wave,
|
|
551
|
+
"cycle": cycle,
|
|
552
|
+
"depth": depth,
|
|
553
|
+
"config": config,
|
|
554
|
+
"lens_verdicts": verdicts,
|
|
555
|
+
"blocked": lens_blocked or bool(structural),
|
|
556
|
+
"cap_reached": cap_reached,
|
|
557
|
+
"skeptics_active": skeptics_active,
|
|
558
|
+
"groups": groups,
|
|
559
|
+
"skeptic_groups": skeptic_groups,
|
|
560
|
+
"refuted_groups": refuted,
|
|
561
|
+
"carried_refutations": [
|
|
562
|
+
{"locator": locator, "cycle": skeptic["cycle"], "path": skeptic["path"]}
|
|
563
|
+
for locator, skeptic in sorted(earlier_refuted.items())
|
|
564
|
+
],
|
|
565
|
+
"fix_groups": [g["locator"] for g in fix_groups],
|
|
566
|
+
"fix_batches": _fix_batches(fix_groups, tasks),
|
|
567
|
+
"structural_blockers": structural,
|
|
568
|
+
"all_refuted": bool(refuted) and not fix_groups and not skeptic_groups and not structural and not cap_reached,
|
|
569
|
+
"panel": panel,
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
|
|
573
|
+
def emit(payload: dict) -> int:
|
|
574
|
+
json.dump(payload, sys.stdout, indent=2, sort_keys=True)
|
|
575
|
+
sys.stdout.write("\n")
|
|
576
|
+
return 0
|
|
577
|
+
|
|
578
|
+
|
|
579
|
+
def repair_evidence(repo: Path, project_dir: str, wave: int, cycle: int, task_id: str) -> dict:
|
|
580
|
+
"""Bind a carried finding batch to a proven, independently verified repair."""
|
|
581
|
+
try:
|
|
582
|
+
from . import _common, build_state, isolation
|
|
583
|
+
except ImportError: # installed standalone helper
|
|
584
|
+
import _common
|
|
585
|
+
import build_state
|
|
586
|
+
import isolation
|
|
587
|
+
|
|
588
|
+
try:
|
|
589
|
+
repo = isolation.require_directory(repo, "repository")
|
|
590
|
+
project = repo / isolation.relative_posix(project_dir)
|
|
591
|
+
if not re.fullmatch(r"T\d{3}", task_id or ""):
|
|
592
|
+
raise ReviewFindingsError("--task must name a repair task id")
|
|
593
|
+
paths = list((project / "tasks").glob(f"{task_id}-*.md"))
|
|
594
|
+
if len(paths) != 1:
|
|
595
|
+
raise ReviewFindingsError("repair task must resolve to exactly one task file")
|
|
596
|
+
repair_path = paths[0]
|
|
597
|
+
text = _read(repair_path, "repair task")
|
|
598
|
+
fields, error = isolation.task_frontmatter(text)
|
|
599
|
+
if error or fields is None or fields.get("status") != "done":
|
|
600
|
+
raise ReviewFindingsError(error or "repair task must be done")
|
|
601
|
+
head = isolation.current_sha(repo)
|
|
602
|
+
proof = isolation.verify_landed_task_files(
|
|
603
|
+
repo, [repair_path], f"{project_dir}/tasks", head
|
|
604
|
+
)["tasks"][0]
|
|
605
|
+
if proof["verdict"] != "recovered":
|
|
606
|
+
raise ReviewFindingsError("repair requires a proven landing")
|
|
607
|
+
base, commit = proof["base"], proof["commit"]
|
|
608
|
+
findings = _section(text, "Review findings") or ""
|
|
609
|
+
headings = list(re.finditer(r"(?m)^### ([a-z0-9_]+)\s*$", findings))
|
|
610
|
+
locators = [heading.group(1) for heading in headings]
|
|
611
|
+
if not locators or len(set(locators)) != len(locators):
|
|
612
|
+
raise ReviewFindingsError("repair needs unique carried finding locators")
|
|
613
|
+
source = compute(repo, project_dir, wave, cycle)
|
|
614
|
+
if source["structural_blockers"]:
|
|
615
|
+
raise ReviewFindingsError("source findings have structural blockers")
|
|
616
|
+
batches = [batch for batch in source["fix_batches"]
|
|
617
|
+
if set(batch["locators"]) == set(locators)]
|
|
618
|
+
if len(batches) != 1:
|
|
619
|
+
raise ReviewFindingsError("repair must carry one eligible source finding batch")
|
|
620
|
+
groups = [group for group in source["groups"] if group["locator"] in locators]
|
|
621
|
+
for index, heading in enumerate(headings):
|
|
622
|
+
end = headings[index + 1].start() if index + 1 < len(headings) else len(findings)
|
|
623
|
+
block = _normalize(findings[heading.end():end])
|
|
624
|
+
group = next(group for group in groups if group["locator"] == heading.group(1))
|
|
625
|
+
if ("Criterion: " + group["criterion"] not in block
|
|
626
|
+
or any(item["text"] not in block for item in group["observations"])):
|
|
627
|
+
raise ReviewFindingsError("repair does not carry the exact criterion and observations")
|
|
628
|
+
frontmatter = text.split("---", 2)[1]
|
|
629
|
+
files = _frontmatter_list(frontmatter, "files")
|
|
630
|
+
originals = sorted({task for group in groups for task in group["tasks"]})
|
|
631
|
+
if (task_id in originals or not set(originals) <= set(_frontmatter_list(frontmatter, "deps"))
|
|
632
|
+
or set(files) != set(batches[0]["files"])):
|
|
633
|
+
raise ReviewFindingsError("repair dependencies/files do not match its source batch")
|
|
634
|
+
sources = load_wave_tasks(project, wave)
|
|
635
|
+
original_paths = [Path(sources[task]["path"]) for task in originals]
|
|
636
|
+
original_proofs = isolation.verify_landed_task_files(
|
|
637
|
+
repo, original_paths, f"{project_dir}/tasks", head
|
|
638
|
+
)["tasks"]
|
|
639
|
+
for original in original_proofs:
|
|
640
|
+
original_id = original["task_id"]
|
|
641
|
+
if (original["verdict"] != "recovered"
|
|
642
|
+
or isolation.run_git(repo, "merge-base", "--is-ancestor", original["commit"], base).returncode
|
|
643
|
+
or isolation.run_git(repo, "diff", "--quiet", original["commit"], base,
|
|
644
|
+
"--", *sources[original_id]["files"]).returncode):
|
|
645
|
+
raise ReviewFindingsError("repair base contains unlinked changes to original product")
|
|
646
|
+
source_paths = list(review_paths(project, wave, cycle, source["depth"]).values())
|
|
647
|
+
source_paths.extend(Path(group["skeptic"]["path"]) for group in groups if group["skeptic"])
|
|
648
|
+
for path in set(source_paths):
|
|
649
|
+
relative = path.relative_to(repo).as_posix()
|
|
650
|
+
at_base = isolation.run_git(repo, "show", f"{base}:{relative}")
|
|
651
|
+
if at_base.returncode or at_base.stdout != _read(path, "source review"):
|
|
652
|
+
raise ReviewFindingsError("source review must be unchanged and precede the repair")
|
|
653
|
+
verification = build_state.verify_lookup(str(repo), _common.task_verify_command(text), commit)
|
|
654
|
+
if not verification["reuse"]:
|
|
655
|
+
raise ReviewFindingsError("repair has no reusable passing Verify at its landing")
|
|
656
|
+
if isolation.run_git(repo, "diff", "--quiet", commit, "--", *files).returncode:
|
|
657
|
+
raise ReviewFindingsError("product changed after the repair landing")
|
|
658
|
+
return {
|
|
659
|
+
"status": "ok", "command": "repair-evidence", "head": head,
|
|
660
|
+
"source": {"wave": wave, "cycle": cycle}, "groups": groups,
|
|
661
|
+
"originals": [{"task": item["task_id"], "base": item["base"],
|
|
662
|
+
"commit": item["commit"], "files": sources[item["task_id"]]["files"]}
|
|
663
|
+
for item in original_proofs],
|
|
664
|
+
"repair": {"task": task_id, "task_file": repair_path.relative_to(repo).as_posix(),
|
|
665
|
+
"base": base, "commit": commit, "files": files},
|
|
666
|
+
"verification": verification,
|
|
667
|
+
"verdict": "evidence-ready; reviewer judges the original unchanged criteria",
|
|
668
|
+
}
|
|
669
|
+
except (isolation.IsolationError, build_state.BuildStateError, ValueError) as error:
|
|
670
|
+
raise ReviewFindingsError(str(error)) from error
|
|
671
|
+
|
|
672
|
+
|
|
673
|
+
def parser() -> argparse.ArgumentParser:
|
|
674
|
+
result = argparse.ArgumentParser(description=__doc__)
|
|
675
|
+
result.add_argument("command", choices=("collect", "repair-evidence"))
|
|
676
|
+
result.add_argument("--repo", type=Path, required=True, help="absolute repository root")
|
|
677
|
+
result.add_argument("--project-dir", default=DEFAULT_PROJECT_DIR)
|
|
678
|
+
result.add_argument("--wave", type=int, required=True)
|
|
679
|
+
result.add_argument("--cycle", type=int, required=True)
|
|
680
|
+
result.add_argument("--task", help="landed repair task for repair-evidence")
|
|
681
|
+
result.add_argument(
|
|
682
|
+
"--helper-failure",
|
|
683
|
+
action="append",
|
|
684
|
+
default=[],
|
|
685
|
+
help="a bundled helper non-zero exit for this cycle; repeatable; recorded as a structural blocker",
|
|
686
|
+
)
|
|
687
|
+
return result
|
|
688
|
+
|
|
689
|
+
|
|
690
|
+
def main(argv: Optional[Sequence[str]] = None) -> int:
|
|
691
|
+
arguments = parser().parse_args(argv)
|
|
692
|
+
try:
|
|
693
|
+
if arguments.wave < 1 or arguments.cycle < 1:
|
|
694
|
+
raise ReviewFindingsError("--wave and --cycle must be positive integers")
|
|
695
|
+
if arguments.command == "repair-evidence":
|
|
696
|
+
return emit(repair_evidence(arguments.repo, arguments.project_dir,
|
|
697
|
+
arguments.wave, arguments.cycle, arguments.task))
|
|
698
|
+
return emit(
|
|
699
|
+
compute(
|
|
700
|
+
arguments.repo,
|
|
701
|
+
arguments.project_dir,
|
|
702
|
+
arguments.wave,
|
|
703
|
+
arguments.cycle,
|
|
704
|
+
arguments.helper_failure,
|
|
705
|
+
)
|
|
706
|
+
)
|
|
707
|
+
except ReviewFindingsError as error:
|
|
708
|
+
json.dump({"status": "error", "error": str(error)}, sys.stdout, indent=2)
|
|
709
|
+
sys.stdout.write("\n")
|
|
710
|
+
return 2
|
|
711
|
+
|
|
712
|
+
|
|
713
|
+
if __name__ == "__main__":
|
|
714
|
+
sys.exit(main())
|