@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,122 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Launch the project status runtime across an atomic refresh handoff."""
|
|
3
|
+
|
|
4
|
+
import errno
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
import subprocess
|
|
8
|
+
import sys
|
|
9
|
+
import time
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Sequence
|
|
12
|
+
|
|
13
|
+
PROJECT_STATUS_MARKER = "gsd-path project status launcher"
|
|
14
|
+
INSTALL_LOCK_OWNER = "owner.json"
|
|
15
|
+
INSTALL_LOCK_SCHEMA = "gsd-path/install-lock/v2"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def process_identity(pid: int):
|
|
19
|
+
if os.name == "nt":
|
|
20
|
+
command = [
|
|
21
|
+
"powershell.exe",
|
|
22
|
+
"-NoProfile",
|
|
23
|
+
"-NonInteractive",
|
|
24
|
+
"-Command",
|
|
25
|
+
f"(Get-Process -Id {pid} -ErrorAction Stop).StartTime.ToUniversalTime().Ticks",
|
|
26
|
+
]
|
|
27
|
+
else:
|
|
28
|
+
command = ["ps", "-o", "lstart=", "-p", str(pid)]
|
|
29
|
+
try:
|
|
30
|
+
result = subprocess.run(command, capture_output=True, text=True, check=False)
|
|
31
|
+
except OSError:
|
|
32
|
+
return None
|
|
33
|
+
value = result.stdout.strip()
|
|
34
|
+
return f"{os.name}:{value}" if result.returncode == 0 and value else None
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def runtime_identity(runtime: Path):
|
|
38
|
+
try:
|
|
39
|
+
status = runtime.stat()
|
|
40
|
+
except FileNotFoundError:
|
|
41
|
+
return None
|
|
42
|
+
return (status.st_dev, status.st_ino, status.st_mtime_ns, status.st_size)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def process_alive(pid: int) -> bool:
|
|
46
|
+
try:
|
|
47
|
+
os.kill(pid, 0)
|
|
48
|
+
except OSError as error:
|
|
49
|
+
return error.errno == errno.EPERM
|
|
50
|
+
return True
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def install_lock_active(lock: Path) -> bool:
|
|
54
|
+
if lock.is_symlink() or not lock.is_dir():
|
|
55
|
+
return False
|
|
56
|
+
try:
|
|
57
|
+
owner = json.loads((lock / INSTALL_LOCK_OWNER).read_text(encoding="utf-8"))
|
|
58
|
+
except (OSError, UnicodeError, json.JSONDecodeError):
|
|
59
|
+
return False
|
|
60
|
+
pid = owner.get("pid")
|
|
61
|
+
valid_pid = isinstance(pid, int) and not isinstance(pid, bool) and pid > 0
|
|
62
|
+
identity = owner.get("identity")
|
|
63
|
+
if (
|
|
64
|
+
owner.get("schema") != INSTALL_LOCK_SCHEMA
|
|
65
|
+
or not valid_pid
|
|
66
|
+
or not isinstance(identity, str)
|
|
67
|
+
):
|
|
68
|
+
return False
|
|
69
|
+
current_identity = process_identity(pid)
|
|
70
|
+
if current_identity is None:
|
|
71
|
+
return process_alive(pid)
|
|
72
|
+
return current_identity == identity
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def launch(repo: Path) -> int:
|
|
76
|
+
parent = repo / ".gsd-path"
|
|
77
|
+
runtime = parent / "runtime" / "pipeline_state.py"
|
|
78
|
+
lock = repo / ".gsd-path-install-lock"
|
|
79
|
+
command = [sys.executable, "-B", str(runtime), "status", "--repo", str(repo)]
|
|
80
|
+
environment = os.environ.copy()
|
|
81
|
+
environment["GIT_OPTIONAL_LOCKS"] = "0"
|
|
82
|
+
while True:
|
|
83
|
+
if install_lock_active(lock):
|
|
84
|
+
time.sleep(0.05)
|
|
85
|
+
continue
|
|
86
|
+
before = runtime_identity(runtime)
|
|
87
|
+
if before is None:
|
|
88
|
+
if install_lock_active(lock):
|
|
89
|
+
continue
|
|
90
|
+
if runtime_identity(runtime) is not None:
|
|
91
|
+
continue
|
|
92
|
+
if install_lock_active(lock):
|
|
93
|
+
continue
|
|
94
|
+
print(f"GSD Path status runtime is unavailable: {runtime}", file=sys.stderr)
|
|
95
|
+
return 2
|
|
96
|
+
try:
|
|
97
|
+
result = subprocess.run(
|
|
98
|
+
command, capture_output=True, check=False, env=environment
|
|
99
|
+
)
|
|
100
|
+
except OSError as error:
|
|
101
|
+
print(f"GSD Path status runtime failed: {error}", file=sys.stderr)
|
|
102
|
+
return 2
|
|
103
|
+
if install_lock_active(lock) or runtime_identity(runtime) != before:
|
|
104
|
+
continue
|
|
105
|
+
sys.stdout.write(result.stdout.decode())
|
|
106
|
+
sys.stderr.write(result.stderr.decode())
|
|
107
|
+
return result.returncode
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def main(argv: Sequence[str] = sys.argv[1:]) -> int:
|
|
111
|
+
if len(argv) != 2 or argv[0] != "--repo":
|
|
112
|
+
print("usage: status_runtime.py --repo <absolute-root>", file=sys.stderr)
|
|
113
|
+
return 2
|
|
114
|
+
repo = Path(argv[1])
|
|
115
|
+
if not repo.is_absolute():
|
|
116
|
+
print("GSD Path status repository must be absolute", file=sys.stderr)
|
|
117
|
+
return 2
|
|
118
|
+
return launch(repo.resolve())
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
if __name__ == "__main__":
|
|
122
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Synchronize canonical GSD Path resources into standalone phase skills."""
|
|
3
|
+
|
|
4
|
+
import argparse
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
import shutil
|
|
8
|
+
import sys
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Iterable, Optional, Sequence, Tuple
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
# Single source of truth for the resource tables, shared with install.mjs.
|
|
14
|
+
RESOURCE_MANIFEST = json.loads(
|
|
15
|
+
(Path(__file__).resolve().parent / "skill-resources.json").read_text(
|
|
16
|
+
encoding="utf-8"
|
|
17
|
+
)
|
|
18
|
+
)
|
|
19
|
+
PHASE_RESOURCES = {
|
|
20
|
+
skill: tuple(resources)
|
|
21
|
+
for skill, resources in RESOURCE_MANIFEST["phase_resources"].items()
|
|
22
|
+
}
|
|
23
|
+
SCRIPT_TARGETS = tuple(tuple(pair) for pair in RESOURCE_MANIFEST["script_targets"])
|
|
24
|
+
PHASE_CONTRACT_TARGETS = tuple(
|
|
25
|
+
tuple(pair) for pair in RESOURCE_MANIFEST["phase_contract_targets"]
|
|
26
|
+
)
|
|
27
|
+
SHARED_DISPATCH_TARGETS = tuple(
|
|
28
|
+
tuple(pair) for pair in RESOURCE_MANIFEST["shared_dispatch_targets"]
|
|
29
|
+
)
|
|
30
|
+
SKILL_NAMES = tuple(RESOURCE_MANIFEST["skills"])
|
|
31
|
+
SKILL_ALIASES = dict(RESOURCE_MANIFEST["skill_aliases"])
|
|
32
|
+
ROUTER_ALIASES = dict(RESOURCE_MANIFEST["router_aliases"])
|
|
33
|
+
PACKAGE_FILES = tuple(RESOURCE_MANIFEST["package_files"])
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def resource_pairs(root: Path) -> Iterable[Tuple[Path, Path]]:
|
|
37
|
+
for source, destination in SHARED_DISPATCH_TARGETS:
|
|
38
|
+
yield root / source, root / destination
|
|
39
|
+
canonical = root / "skills" / "gsd-path"
|
|
40
|
+
for skill, resources in PHASE_RESOURCES.items():
|
|
41
|
+
for relative in resources:
|
|
42
|
+
yield canonical / relative, root / "skills" / skill / relative
|
|
43
|
+
for source, destination in SCRIPT_TARGETS:
|
|
44
|
+
yield root / source, root / destination
|
|
45
|
+
for source, destination in PHASE_CONTRACT_TARGETS:
|
|
46
|
+
yield root / source, root / destination
|
|
47
|
+
for legacy, canonical_skill in SKILL_ALIASES.items():
|
|
48
|
+
canonical_directory = root / "skills" / canonical_skill
|
|
49
|
+
legacy_directory = root / "skills" / legacy
|
|
50
|
+
yield canonical_directory / "SKILL.md", legacy_directory / "CANONICAL.md"
|
|
51
|
+
for relative in PHASE_RESOURCES.get(canonical_skill, ()):
|
|
52
|
+
yield canonical / relative, legacy_directory / relative
|
|
53
|
+
canonical_prefix = f"skills/{canonical_skill}/"
|
|
54
|
+
for source, destination in SCRIPT_TARGETS:
|
|
55
|
+
if destination.startswith(canonical_prefix):
|
|
56
|
+
relative = destination.removeprefix(canonical_prefix)
|
|
57
|
+
yield root / source, legacy_directory / relative
|
|
58
|
+
for alias, canonical_skill in ROUTER_ALIASES.items():
|
|
59
|
+
canonical_directory = root / "skills" / canonical_skill
|
|
60
|
+
alias_directory = root / "skills" / alias
|
|
61
|
+
for source in _skill_files(canonical_directory):
|
|
62
|
+
yield source, alias_directory / source.relative_to(canonical_directory)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _skill_files(skill_root: Path) -> Iterable[Path]:
|
|
66
|
+
for directory, names, files in os.walk(skill_root):
|
|
67
|
+
names[:] = [name for name in names if name not in (".git", "node_modules", "__pycache__")]
|
|
68
|
+
for name in files:
|
|
69
|
+
if name == ".DS_Store":
|
|
70
|
+
continue
|
|
71
|
+
yield Path(directory) / name
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def rewrite_router_alias_skill(text: str, alias: str, canonical: str) -> str:
|
|
75
|
+
"""Keep the router contract; change only the slash/catalog name."""
|
|
76
|
+
if not text.startswith("---\n"):
|
|
77
|
+
raise ValueError("SKILL.md is missing YAML frontmatter")
|
|
78
|
+
end = text.find("\n---\n", 4)
|
|
79
|
+
if end == -1:
|
|
80
|
+
raise ValueError("SKILL.md has unterminated YAML frontmatter")
|
|
81
|
+
header = text[4:end]
|
|
82
|
+
rest = text[end:]
|
|
83
|
+
expected = f"name: {canonical}"
|
|
84
|
+
replacement = f"name: {alias}"
|
|
85
|
+
lines = header.split("\n")
|
|
86
|
+
if expected not in lines:
|
|
87
|
+
raise ValueError(f"router alias {alias} is missing {expected}")
|
|
88
|
+
rewritten = [replacement if line == expected else line for line in lines]
|
|
89
|
+
if rewritten.count(replacement) != 1:
|
|
90
|
+
raise ValueError(f"router alias {alias} name rewrite was not unique")
|
|
91
|
+
invoke = f"invokes ${canonical}."
|
|
92
|
+
invoke_with_alias = f"invokes ${alias} or ${canonical}."
|
|
93
|
+
header = "\n".join(rewritten)
|
|
94
|
+
if invoke not in header:
|
|
95
|
+
raise ValueError(f"router alias {alias} is missing {invoke}")
|
|
96
|
+
header = header.replace(invoke, invoke_with_alias, 1)
|
|
97
|
+
return "---\n" + header + rest
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def materialized_resource_bytes(source: Path, destination: Path) -> bytes:
|
|
101
|
+
data = source.read_bytes()
|
|
102
|
+
if destination.name != "SKILL.md":
|
|
103
|
+
return data
|
|
104
|
+
alias = destination.parent.name
|
|
105
|
+
canonical = ROUTER_ALIASES.get(alias)
|
|
106
|
+
if canonical is None:
|
|
107
|
+
return data
|
|
108
|
+
return rewrite_router_alias_skill(
|
|
109
|
+
data.decode("utf-8"), alias, canonical
|
|
110
|
+
).encode("utf-8")
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def package_metadata(root: Path) -> Iterable[Path]:
|
|
114
|
+
for directory, names, files in os.walk(root):
|
|
115
|
+
names[:] = [name for name in names if name not in (".git", "node_modules")]
|
|
116
|
+
for name in names + files:
|
|
117
|
+
if name == ".DS_Store":
|
|
118
|
+
yield Path(directory) / name
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def divergence_warning(source: Path, destination: Path, root: Path) -> Optional[str]:
|
|
122
|
+
"""Flag the wrong-direction-edit signature: a generated copy that differs
|
|
123
|
+
from canonical and is NEWER than it (sync preserves canonical mtimes via
|
|
124
|
+
copy2, so a just-edited copy is the only newer-and-different case)."""
|
|
125
|
+
if not destination.is_file():
|
|
126
|
+
return None
|
|
127
|
+
if destination.read_bytes() == materialized_resource_bytes(source, destination):
|
|
128
|
+
return None
|
|
129
|
+
if destination.stat().st_mtime > source.stat().st_mtime:
|
|
130
|
+
return (
|
|
131
|
+
f"generated resource diverges from canonical and is newer "
|
|
132
|
+
f"(possible wrong-direction edit; edit {source.relative_to(root)} "
|
|
133
|
+
f"instead): {destination.relative_to(root)}"
|
|
134
|
+
)
|
|
135
|
+
return None
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def mismatches(root: Path) -> Sequence[str]:
|
|
139
|
+
problems = []
|
|
140
|
+
expected_skills = (
|
|
141
|
+
"gsd-path",
|
|
142
|
+
*sorted((*PHASE_RESOURCES, *SKILL_ALIASES, *ROUTER_ALIASES)),
|
|
143
|
+
)
|
|
144
|
+
if SKILL_NAMES != expected_skills:
|
|
145
|
+
problems.append(
|
|
146
|
+
"manifest skills must be root gsd-path plus every canonical skill and alias"
|
|
147
|
+
)
|
|
148
|
+
if set(SKILL_ALIASES) & set(PHASE_RESOURCES):
|
|
149
|
+
problems.append("skill aliases cannot also own phase resources")
|
|
150
|
+
if set(ROUTER_ALIASES) & set(PHASE_RESOURCES):
|
|
151
|
+
problems.append("router aliases cannot also own phase resources")
|
|
152
|
+
if set(ROUTER_ALIASES) & set(SKILL_ALIASES):
|
|
153
|
+
problems.append("router aliases cannot also be skill aliases")
|
|
154
|
+
for legacy, canonical in SKILL_ALIASES.items():
|
|
155
|
+
if canonical not in PHASE_RESOURCES:
|
|
156
|
+
problems.append(f"skill alias target is not canonical: {legacy} -> {canonical}")
|
|
157
|
+
for alias, canonical in ROUTER_ALIASES.items():
|
|
158
|
+
if canonical != "gsd-path":
|
|
159
|
+
problems.append(f"router alias target is not the router: {alias} -> {canonical}")
|
|
160
|
+
package_path = root / "package.json"
|
|
161
|
+
try:
|
|
162
|
+
package_files = tuple(json.loads(package_path.read_text(encoding="utf-8"))["files"])
|
|
163
|
+
except (OSError, json.JSONDecodeError, KeyError, TypeError):
|
|
164
|
+
package_files = ()
|
|
165
|
+
if package_files != PACKAGE_FILES:
|
|
166
|
+
problems.append("package.json files do not match manifest package_files")
|
|
167
|
+
for source, destination in resource_pairs(root):
|
|
168
|
+
if not source.is_file():
|
|
169
|
+
problems.append(f"missing canonical resource: {source.relative_to(root)}")
|
|
170
|
+
elif not destination.is_file():
|
|
171
|
+
problems.append(f"missing generated resource: {destination.relative_to(root)}")
|
|
172
|
+
else:
|
|
173
|
+
try:
|
|
174
|
+
expected = materialized_resource_bytes(source, destination)
|
|
175
|
+
except ValueError as error:
|
|
176
|
+
problems.append(str(error))
|
|
177
|
+
continue
|
|
178
|
+
if expected != destination.read_bytes():
|
|
179
|
+
warning = divergence_warning(source, destination, root)
|
|
180
|
+
if warning:
|
|
181
|
+
problems.append(warning)
|
|
182
|
+
else:
|
|
183
|
+
problems.append(
|
|
184
|
+
f"stale generated resource: {destination.relative_to(root)}"
|
|
185
|
+
)
|
|
186
|
+
for path in package_metadata(root):
|
|
187
|
+
problems.append(f"unexpected package metadata: {path.relative_to(root)}")
|
|
188
|
+
return problems
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def synchronize(root: Path) -> int:
|
|
192
|
+
copied = 0
|
|
193
|
+
warnings = 0
|
|
194
|
+
for source, destination in resource_pairs(root):
|
|
195
|
+
if not source.is_file():
|
|
196
|
+
print(f"missing canonical resource: {source}", file=sys.stderr)
|
|
197
|
+
return 1
|
|
198
|
+
warning = divergence_warning(source, destination, root)
|
|
199
|
+
if warning:
|
|
200
|
+
print(f"warning: {warning}", file=sys.stderr)
|
|
201
|
+
warnings += 1
|
|
202
|
+
destination.parent.mkdir(parents=True, exist_ok=True)
|
|
203
|
+
expected = materialized_resource_bytes(source, destination)
|
|
204
|
+
if expected != source.read_bytes():
|
|
205
|
+
destination.write_bytes(expected)
|
|
206
|
+
else:
|
|
207
|
+
shutil.copy2(source, destination)
|
|
208
|
+
copied += 1
|
|
209
|
+
removed_metadata = 0
|
|
210
|
+
for path in package_metadata(root):
|
|
211
|
+
if not path.is_file() and not path.is_symlink():
|
|
212
|
+
print(f"unexpected non-file package metadata: {path}", file=sys.stderr)
|
|
213
|
+
return 1
|
|
214
|
+
path.unlink()
|
|
215
|
+
removed_metadata += 1
|
|
216
|
+
package_path = root / "package.json"
|
|
217
|
+
package = json.loads(package_path.read_text(encoding="utf-8"))
|
|
218
|
+
package["files"] = list(PACKAGE_FILES)
|
|
219
|
+
package_path.write_text(json.dumps(package, indent=2) + "\n", encoding="utf-8")
|
|
220
|
+
print(
|
|
221
|
+
json.dumps(
|
|
222
|
+
{
|
|
223
|
+
"removed_metadata": removed_metadata,
|
|
224
|
+
"resources": copied,
|
|
225
|
+
"warnings": warnings,
|
|
226
|
+
},
|
|
227
|
+
sort_keys=True,
|
|
228
|
+
)
|
|
229
|
+
)
|
|
230
|
+
return 0
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def parser() -> argparse.ArgumentParser:
|
|
234
|
+
argument_parser = argparse.ArgumentParser(description=__doc__)
|
|
235
|
+
argument_parser.add_argument(
|
|
236
|
+
"--root",
|
|
237
|
+
type=Path,
|
|
238
|
+
default=Path(__file__).resolve().parents[1],
|
|
239
|
+
help="repository root",
|
|
240
|
+
)
|
|
241
|
+
argument_parser.add_argument("--check", action="store_true")
|
|
242
|
+
return argument_parser
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def main(argv: Optional[Sequence[str]] = None) -> int:
|
|
246
|
+
arguments = parser().parse_args(argv)
|
|
247
|
+
root = arguments.root.resolve()
|
|
248
|
+
if arguments.check:
|
|
249
|
+
problems = mismatches(root)
|
|
250
|
+
if problems:
|
|
251
|
+
print("\n".join(problems), file=sys.stderr)
|
|
252
|
+
return 1
|
|
253
|
+
print(json.dumps({"resources": sum(1 for _ in resource_pairs(root))}, sort_keys=True))
|
|
254
|
+
return 0
|
|
255
|
+
return synchronize(root)
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
if __name__ == "__main__":
|
|
259
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Persist observed Codex output usage and block admission after exhaustion.
|
|
3
|
+
|
|
4
|
+
This is an admission gate, not an in-flight generation cap. Codex CLI does not
|
|
5
|
+
expose a hard output-token cap. Unobserved calls are not budget evidence.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import argparse
|
|
9
|
+
import json
|
|
10
|
+
import sys
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
try:
|
|
14
|
+
from _common import atomic_write
|
|
15
|
+
from loop_run import log_lock
|
|
16
|
+
except ModuleNotFoundError: # pragma: no cover - package imports
|
|
17
|
+
from scripts._common import atomic_write
|
|
18
|
+
from scripts.loop_run import log_lock
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
SCHEMA = "gsd-path/token-budget/v1"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def positive(value):
|
|
25
|
+
result = int(value)
|
|
26
|
+
if result <= 0:
|
|
27
|
+
raise argparse.ArgumentTypeError("budget must be positive")
|
|
28
|
+
return result
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def output_usage(path: Path) -> int:
|
|
32
|
+
usages = []
|
|
33
|
+
child_usage = None
|
|
34
|
+
child = False
|
|
35
|
+
starts = 0
|
|
36
|
+
complete = False
|
|
37
|
+
text = path.read_text(encoding="utf-8")
|
|
38
|
+
try:
|
|
39
|
+
document = json.loads(text)
|
|
40
|
+
except ValueError:
|
|
41
|
+
document = None
|
|
42
|
+
if isinstance(document, dict) and isinstance(document.get("usage"), dict):
|
|
43
|
+
# Claude Code `-p --output-format json`: one document with the run's usage.
|
|
44
|
+
usages.append(document["usage"].get("output_tokens"))
|
|
45
|
+
text = ""
|
|
46
|
+
for line in text.splitlines():
|
|
47
|
+
event = json.loads(line)
|
|
48
|
+
if "raw" in event:
|
|
49
|
+
event = json.loads(event["raw"])
|
|
50
|
+
if event.get("type") == "turn.completed":
|
|
51
|
+
usages.append(event["usage"]["output_tokens"])
|
|
52
|
+
payload = event.get("payload", {})
|
|
53
|
+
if event.get("type") == "session_meta":
|
|
54
|
+
source = payload.get("source", {})
|
|
55
|
+
child = isinstance(source, dict) and "subagent" in source
|
|
56
|
+
if event.get("type") == "event_msg":
|
|
57
|
+
if payload.get("type") == "task_started":
|
|
58
|
+
starts += 1
|
|
59
|
+
complete = False
|
|
60
|
+
elif payload.get("type") == "token_count" and payload.get("info"):
|
|
61
|
+
child_usage = payload["info"]["total_token_usage"]["output_tokens"]
|
|
62
|
+
elif payload.get("type") == "task_complete":
|
|
63
|
+
complete = True
|
|
64
|
+
if not usages and child and starts == 1 and complete and child_usage is not None:
|
|
65
|
+
# Only a single native invocation has an unambiguous cumulative counter.
|
|
66
|
+
usages = [child_usage]
|
|
67
|
+
if not usages or any(type(value) is not int or value < 0 for value in usages):
|
|
68
|
+
raise ValueError("complete supported output usage is unavailable; budget is unproven")
|
|
69
|
+
return sum(usages)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def operate(args):
|
|
73
|
+
ledger = args.ledger.absolute()
|
|
74
|
+
if ledger.is_symlink():
|
|
75
|
+
raise ValueError("ledger must be a real file")
|
|
76
|
+
with log_lock({"log": str(ledger)}):
|
|
77
|
+
if args.action == "configure":
|
|
78
|
+
policy = {"schema": SCHEMA, "host": "codex-cli", "metric": "output_tokens",
|
|
79
|
+
"task_limit": args.task_limit, "session_limit": args.session_limit,
|
|
80
|
+
"authority": args.authority}
|
|
81
|
+
if ledger.exists():
|
|
82
|
+
data = json.loads(ledger.read_text())
|
|
83
|
+
if any(data.get(key) != value for key, value in policy.items()):
|
|
84
|
+
raise ValueError("existing session policy differs; configuration cannot reset usage")
|
|
85
|
+
else:
|
|
86
|
+
data = {**policy, "observations": {}}
|
|
87
|
+
atomic_write(ledger, json.dumps(data, indent=2) + "\n")
|
|
88
|
+
return {"status": "configured", "hard_cap_supported": False}
|
|
89
|
+
data = json.loads(ledger.read_text())
|
|
90
|
+
if data.get("schema") != SCHEMA or data.get("host") != "codex-cli":
|
|
91
|
+
raise ValueError("unsupported budget ledger")
|
|
92
|
+
if args.action == "record":
|
|
93
|
+
source = str(args.events.resolve())
|
|
94
|
+
observation = {"task": args.task, "output_tokens": output_usage(args.events)}
|
|
95
|
+
previous = data["observations"].get(source)
|
|
96
|
+
if previous and (previous["task"] != args.task or
|
|
97
|
+
previous["output_tokens"] > observation["output_tokens"]):
|
|
98
|
+
raise ValueError("observation cannot change task or reduce recorded usage")
|
|
99
|
+
data["observations"][source] = observation
|
|
100
|
+
atomic_write(ledger, json.dumps(data, indent=2) + "\n")
|
|
101
|
+
return {"status": "recorded", **observation, "hard_cap_supported": False}
|
|
102
|
+
observations = data["observations"].values()
|
|
103
|
+
task_used = sum(row["output_tokens"] for row in observations if row["task"] == args.task)
|
|
104
|
+
session_used = sum(row["output_tokens"] for row in observations)
|
|
105
|
+
task_remaining = max(0, data["task_limit"] - task_used)
|
|
106
|
+
session_remaining = max(0, data["session_limit"] - session_used)
|
|
107
|
+
reason = "observed usage is within budget; unobserved and in-flight usage is unproven"
|
|
108
|
+
blocked = not task_remaining or not session_remaining
|
|
109
|
+
if blocked:
|
|
110
|
+
reason = "observed output budget exhausted"
|
|
111
|
+
if args.require_hard_cap:
|
|
112
|
+
blocked = True
|
|
113
|
+
reason = "hard generation cap unsupported by Codex CLI"
|
|
114
|
+
return {"decision": "blocked" if blocked else "admit", "reason": reason,
|
|
115
|
+
"task_output_tokens": task_used, "session_output_tokens": session_used,
|
|
116
|
+
"task_remaining": task_remaining, "session_remaining": session_remaining,
|
|
117
|
+
"hard_cap_supported": False, "enforcement": "observed-admission-only"}
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def main(argv=None):
|
|
121
|
+
parser = argparse.ArgumentParser(description=__doc__)
|
|
122
|
+
subparsers = parser.add_subparsers(dest="action", required=True)
|
|
123
|
+
for name in ("configure", "record", "admit"):
|
|
124
|
+
command = subparsers.add_parser(name)
|
|
125
|
+
command.add_argument("--ledger", type=Path, required=True)
|
|
126
|
+
if name == "configure":
|
|
127
|
+
command.add_argument("--task-limit", type=positive, required=True)
|
|
128
|
+
command.add_argument("--session-limit", type=positive, required=True)
|
|
129
|
+
command.add_argument("--authority", required=True)
|
|
130
|
+
else:
|
|
131
|
+
command.add_argument("--task", required=True)
|
|
132
|
+
if name == "record":
|
|
133
|
+
command.add_argument("--events", type=Path, required=True)
|
|
134
|
+
if name == "admit":
|
|
135
|
+
command.add_argument("--require-hard-cap", action="store_true")
|
|
136
|
+
try:
|
|
137
|
+
result = operate(parser.parse_args(argv))
|
|
138
|
+
except (ValueError, OSError, KeyError, TypeError) as error:
|
|
139
|
+
print(json.dumps({"decision": "blocked", "reason": str(error)}))
|
|
140
|
+
return 1
|
|
141
|
+
print(json.dumps(result, sort_keys=True))
|
|
142
|
+
return 1 if result.get("decision") == "blocked" else 0
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
if __name__ == "__main__":
|
|
146
|
+
raise SystemExit(main())
|