@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.
Files changed (398) hide show
  1. package/AGENTS.md +397 -0
  2. package/DOCS.md +341 -0
  3. package/FULL.md +475 -0
  4. package/GUIDE.md +23 -0
  5. package/HOOKS.md +273 -0
  6. package/LICENSE +21 -0
  7. package/MIGRATE.md +100 -0
  8. package/QUICK.md +146 -0
  9. package/README.md +280 -0
  10. package/RUNTIME.md +229 -0
  11. package/UPDATE.md +196 -0
  12. package/WORKFLOW.md +683 -0
  13. package/package.json +80 -0
  14. package/platforms/claude/dispatch.md +63 -0
  15. package/platforms/copilot/dispatch.md +61 -0
  16. package/platforms/cursor/agent.md +17 -0
  17. package/platforms/cursor/dispatch.md +63 -0
  18. package/platforms/grok/dispatch.md +61 -0
  19. package/platforms/kimi/dispatch.md +64 -0
  20. package/platforms/kiro/dispatch.md +58 -0
  21. package/platforms/opencode/dispatch.md +62 -0
  22. package/platforms/qwen/dispatch.md +60 -0
  23. package/platforms/shared-agents/dispatch.md +131 -0
  24. package/plugin.json +14 -0
  25. package/scripts/_common.py +180 -0
  26. package/scripts/archive_milestone.py +2265 -0
  27. package/scripts/bootstrap_repository.py +710 -0
  28. package/scripts/build_state.py +854 -0
  29. package/scripts/check_docs_audit.py +463 -0
  30. package/scripts/check_handoffs.py +1855 -0
  31. package/scripts/check_task_briefs.py +376 -0
  32. package/scripts/check_update.py +93 -0
  33. package/scripts/core_hook_gate.py +34 -0
  34. package/scripts/core_hook_settings.py +119 -0
  35. package/scripts/detect_project.py +1517 -0
  36. package/scripts/discussion_records.py +619 -0
  37. package/scripts/discussion_validate.py +1321 -0
  38. package/scripts/dispatch_driver.py +1812 -0
  39. package/scripts/git_guard.py +759 -0
  40. package/scripts/guard_hook.py +2124 -0
  41. package/scripts/install.mjs +3239 -0
  42. package/scripts/install.py +3008 -0
  43. package/scripts/integration.py +1433 -0
  44. package/scripts/isolation.py +3538 -0
  45. package/scripts/lean_verification.py +256 -0
  46. package/scripts/loop_run.py +837 -0
  47. package/scripts/migrate_core.py +169 -0
  48. package/scripts/pipeline_diagnose.py +715 -0
  49. package/scripts/pipeline_git.py +940 -0
  50. package/scripts/pipeline_state.py +2291 -0
  51. package/scripts/pipeline_undo.py +1039 -0
  52. package/scripts/promote_lookahead.py +415 -0
  53. package/scripts/review_findings.py +714 -0
  54. package/scripts/review_panel.py +553 -0
  55. package/scripts/skill-resources.json +340 -0
  56. package/scripts/state_checkpoint.py +1039 -0
  57. package/scripts/state_promote.py +743 -0
  58. package/scripts/status_runtime.py +122 -0
  59. package/scripts/sync_skill_resources.py +259 -0
  60. package/scripts/token_budget.py +146 -0
  61. package/scripts/wizard.mjs +179 -0
  62. package/scripts/workflow_run.py +117 -0
  63. package/skills/gsd-path/BUILD.md +700 -0
  64. package/skills/gsd-path/DECIDE.md +115 -0
  65. package/skills/gsd-path/DEFINE.md +297 -0
  66. package/skills/gsd-path/DOCS-AUDIT.md +190 -0
  67. package/skills/gsd-path/INSPECT.md +167 -0
  68. package/skills/gsd-path/PLAN.md +411 -0
  69. package/skills/gsd-path/RESEARCH.md +159 -0
  70. package/skills/gsd-path/ROADMAP.md +215 -0
  71. package/skills/gsd-path/SHIP.md +412 -0
  72. package/skills/gsd-path/SKILL.md +471 -0
  73. package/skills/gsd-path/agents/openai.yaml +6 -0
  74. package/skills/gsd-path/references/codebase-mapper.md +52 -0
  75. package/skills/gsd-path/references/coder.md +90 -0
  76. package/skills/gsd-path/references/decider.md +36 -0
  77. package/skills/gsd-path/references/dispatch.md +131 -0
  78. package/skills/gsd-path/references/docs-auditor.md +60 -0
  79. package/skills/gsd-path/references/planner.md +104 -0
  80. package/skills/gsd-path/references/researcher.md +42 -0
  81. package/skills/gsd-path/references/reviewer.md +188 -0
  82. package/skills/gsd-path/references/roadmapper.md +48 -0
  83. package/skills/gsd-path/scripts/_common.py +180 -0
  84. package/skills/gsd-path/scripts/archive_milestone.py +2265 -0
  85. package/skills/gsd-path/scripts/bootstrap_repository.py +710 -0
  86. package/skills/gsd-path/scripts/build_state.py +854 -0
  87. package/skills/gsd-path/scripts/check_docs_audit.py +463 -0
  88. package/skills/gsd-path/scripts/check_handoffs.py +1855 -0
  89. package/skills/gsd-path/scripts/check_task_briefs.py +376 -0
  90. package/skills/gsd-path/scripts/check_update.py +93 -0
  91. package/skills/gsd-path/scripts/detect_project.py +1517 -0
  92. package/skills/gsd-path/scripts/discussion_records.py +619 -0
  93. package/skills/gsd-path/scripts/discussion_validate.py +1321 -0
  94. package/skills/gsd-path/scripts/dispatch_driver.py +1812 -0
  95. package/skills/gsd-path/scripts/integration.py +1433 -0
  96. package/skills/gsd-path/scripts/isolation.py +3538 -0
  97. package/skills/gsd-path/scripts/lean_verification.py +256 -0
  98. package/skills/gsd-path/scripts/loop_run.py +837 -0
  99. package/skills/gsd-path/scripts/pipeline_git.py +940 -0
  100. package/skills/gsd-path/scripts/pipeline_state.py +2291 -0
  101. package/skills/gsd-path/scripts/promote_lookahead.py +415 -0
  102. package/skills/gsd-path/scripts/review_findings.py +714 -0
  103. package/skills/gsd-path/scripts/review_panel.py +553 -0
  104. package/skills/gsd-path/scripts/state_checkpoint.py +1039 -0
  105. package/skills/gsd-path/scripts/state_promote.py +743 -0
  106. package/skills/gsd-path/scripts/token_budget.py +146 -0
  107. package/skills/gsd-path/scripts/workflow_run.py +117 -0
  108. package/skills/gsd-path/templates/answers.md +29 -0
  109. package/skills/gsd-path/templates/archive-manifest.md +37 -0
  110. package/skills/gsd-path/templates/charter.md +50 -0
  111. package/skills/gsd-path/templates/codebase.md +46 -0
  112. package/skills/gsd-path/templates/dialogue.md +21 -0
  113. package/skills/gsd-path/templates/docs-audit.md +69 -0
  114. package/skills/gsd-path/templates/evidence.md +24 -0
  115. package/skills/gsd-path/templates/final-review.md +27 -0
  116. package/skills/gsd-path/templates/gap-review.md +20 -0
  117. package/skills/gsd-path/templates/intent.md +98 -0
  118. package/skills/gsd-path/templates/loop.md +59 -0
  119. package/skills/gsd-path/templates/patch-findings.md +16 -0
  120. package/skills/gsd-path/templates/plan-panel.md +25 -0
  121. package/skills/gsd-path/templates/plan.md +89 -0
  122. package/skills/gsd-path/templates/repository.md +16 -0
  123. package/skills/gsd-path/templates/research-handoff.md +25 -0
  124. package/skills/gsd-path/templates/roadmap.md +38 -0
  125. package/skills/gsd-path/templates/skeptic.md +36 -0
  126. package/skills/gsd-path/templates/state.md +27 -0
  127. package/skills/gsd-path/templates/synthesis.md +36 -0
  128. package/skills/gsd-path/templates/task.md +74 -0
  129. package/skills/gsd-path/templates/wave-panel.md +25 -0
  130. package/skills/gsd-path/templates/wave-review.md +64 -0
  131. package/skills/gsd-path-build/SKILL.md +700 -0
  132. package/skills/gsd-path-build/agents/openai.yaml +6 -0
  133. package/skills/gsd-path-build/references/coder.md +90 -0
  134. package/skills/gsd-path-build/references/dispatch.md +131 -0
  135. package/skills/gsd-path-build/references/reviewer.md +188 -0
  136. package/skills/gsd-path-build/scripts/_common.py +180 -0
  137. package/skills/gsd-path-build/scripts/archive_milestone.py +2265 -0
  138. package/skills/gsd-path-build/scripts/build_state.py +854 -0
  139. package/skills/gsd-path-build/scripts/check_handoffs.py +1855 -0
  140. package/skills/gsd-path-build/scripts/check_task_briefs.py +376 -0
  141. package/skills/gsd-path-build/scripts/discussion_records.py +619 -0
  142. package/skills/gsd-path-build/scripts/discussion_validate.py +1321 -0
  143. package/skills/gsd-path-build/scripts/dispatch_driver.py +1812 -0
  144. package/skills/gsd-path-build/scripts/integration.py +1433 -0
  145. package/skills/gsd-path-build/scripts/isolation.py +3538 -0
  146. package/skills/gsd-path-build/scripts/loop_run.py +837 -0
  147. package/skills/gsd-path-build/scripts/pipeline_git.py +940 -0
  148. package/skills/gsd-path-build/scripts/pipeline_state.py +2291 -0
  149. package/skills/gsd-path-build/scripts/review_findings.py +714 -0
  150. package/skills/gsd-path-build/scripts/review_panel.py +553 -0
  151. package/skills/gsd-path-build/scripts/state_checkpoint.py +1039 -0
  152. package/skills/gsd-path-build/scripts/state_promote.py +743 -0
  153. package/skills/gsd-path-build/scripts/token_budget.py +146 -0
  154. package/skills/gsd-path-build/scripts/workflow_run.py +117 -0
  155. package/skills/gsd-path-build/templates/skeptic.md +36 -0
  156. package/skills/gsd-path-build/templates/task.md +74 -0
  157. package/skills/gsd-path-build/templates/wave-panel.md +25 -0
  158. package/skills/gsd-path-build/templates/wave-review.md +64 -0
  159. package/skills/gsd-path-decide/SKILL.md +115 -0
  160. package/skills/gsd-path-decide/agents/openai.yaml +6 -0
  161. package/skills/gsd-path-decide/references/decider.md +36 -0
  162. package/skills/gsd-path-decide/references/dispatch.md +131 -0
  163. package/skills/gsd-path-decide/scripts/_common.py +180 -0
  164. package/skills/gsd-path-decide/scripts/check_handoffs.py +1855 -0
  165. package/skills/gsd-path-decide/scripts/check_task_briefs.py +376 -0
  166. package/skills/gsd-path-decide/scripts/discussion_records.py +619 -0
  167. package/skills/gsd-path-decide/scripts/isolation.py +3538 -0
  168. package/skills/gsd-path-decide/scripts/pipeline_git.py +940 -0
  169. package/skills/gsd-path-decide/scripts/pipeline_state.py +2291 -0
  170. package/skills/gsd-path-decide/scripts/state_checkpoint.py +1039 -0
  171. package/skills/gsd-path-decide/scripts/state_promote.py +743 -0
  172. package/skills/gsd-path-decide/templates/synthesis.md +36 -0
  173. package/skills/gsd-path-define/SKILL.md +297 -0
  174. package/skills/gsd-path-define/agents/openai.yaml +6 -0
  175. package/skills/gsd-path-define/scripts/_common.py +180 -0
  176. package/skills/gsd-path-define/scripts/check_handoffs.py +1855 -0
  177. package/skills/gsd-path-define/scripts/check_task_briefs.py +376 -0
  178. package/skills/gsd-path-define/scripts/detect_project.py +1517 -0
  179. package/skills/gsd-path-define/scripts/discussion_records.py +619 -0
  180. package/skills/gsd-path-define/scripts/isolation.py +3538 -0
  181. package/skills/gsd-path-define/scripts/pipeline_git.py +940 -0
  182. package/skills/gsd-path-define/scripts/pipeline_state.py +2291 -0
  183. package/skills/gsd-path-define/scripts/state_checkpoint.py +1039 -0
  184. package/skills/gsd-path-define/scripts/state_promote.py +743 -0
  185. package/skills/gsd-path-define/templates/charter.md +50 -0
  186. package/skills/gsd-path-define/templates/intent.md +98 -0
  187. package/skills/gsd-path-define/templates/state.md +27 -0
  188. package/skills/gsd-path-discuss/SKILL.md +162 -0
  189. package/skills/gsd-path-discuss/agents/openai.yaml +6 -0
  190. package/skills/gsd-path-discuss/scripts/_common.py +180 -0
  191. package/skills/gsd-path-discuss/scripts/discussion_records.py +619 -0
  192. package/skills/gsd-path-discuss/templates/answers.md +29 -0
  193. package/skills/gsd-path-discuss/templates/dialogue.md +21 -0
  194. package/skills/gsd-path-docs-audit/SKILL.md +190 -0
  195. package/skills/gsd-path-docs-audit/agents/openai.yaml +6 -0
  196. package/skills/gsd-path-docs-audit/references/dispatch.md +131 -0
  197. package/skills/gsd-path-docs-audit/references/docs-auditor.md +60 -0
  198. package/skills/gsd-path-docs-audit/scripts/_common.py +180 -0
  199. package/skills/gsd-path-docs-audit/scripts/check_docs_audit.py +463 -0
  200. package/skills/gsd-path-docs-audit/scripts/check_handoffs.py +1855 -0
  201. package/skills/gsd-path-docs-audit/scripts/check_task_briefs.py +376 -0
  202. package/skills/gsd-path-docs-audit/scripts/discussion_records.py +619 -0
  203. package/skills/gsd-path-docs-audit/scripts/isolation.py +3538 -0
  204. package/skills/gsd-path-docs-audit/scripts/pipeline_git.py +940 -0
  205. package/skills/gsd-path-docs-audit/scripts/pipeline_state.py +2291 -0
  206. package/skills/gsd-path-docs-audit/scripts/state_checkpoint.py +1039 -0
  207. package/skills/gsd-path-docs-audit/scripts/state_promote.py +743 -0
  208. package/skills/gsd-path-docs-audit/templates/docs-audit.md +69 -0
  209. package/skills/gsd-path-forensics/SKILL.md +39 -0
  210. package/skills/gsd-path-forensics/agents/openai.yaml +6 -0
  211. package/skills/gsd-path-forensics/scripts/_common.py +180 -0
  212. package/skills/gsd-path-forensics/scripts/discussion_records.py +619 -0
  213. package/skills/gsd-path-forensics/scripts/pipeline_diagnose.py +715 -0
  214. package/skills/gsd-path-forensics/scripts/pipeline_undo.py +1039 -0
  215. package/skills/gsd-path-inspect/SKILL.md +167 -0
  216. package/skills/gsd-path-inspect/agents/openai.yaml +6 -0
  217. package/skills/gsd-path-inspect/references/codebase-mapper.md +52 -0
  218. package/skills/gsd-path-inspect/references/dispatch.md +131 -0
  219. package/skills/gsd-path-inspect/references/docs-auditor.md +60 -0
  220. package/skills/gsd-path-inspect/scripts/_common.py +180 -0
  221. package/skills/gsd-path-inspect/scripts/check_docs_audit.py +463 -0
  222. package/skills/gsd-path-inspect/scripts/check_handoffs.py +1855 -0
  223. package/skills/gsd-path-inspect/scripts/check_task_briefs.py +376 -0
  224. package/skills/gsd-path-inspect/scripts/detect_project.py +1517 -0
  225. package/skills/gsd-path-inspect/scripts/discussion_records.py +619 -0
  226. package/skills/gsd-path-inspect/scripts/isolation.py +3538 -0
  227. package/skills/gsd-path-inspect/scripts/pipeline_git.py +940 -0
  228. package/skills/gsd-path-inspect/scripts/pipeline_state.py +2291 -0
  229. package/skills/gsd-path-inspect/scripts/state_checkpoint.py +1039 -0
  230. package/skills/gsd-path-inspect/scripts/state_promote.py +743 -0
  231. package/skills/gsd-path-inspect/templates/codebase.md +46 -0
  232. package/skills/gsd-path-inspect/templates/docs-audit.md +69 -0
  233. package/skills/gsd-path-inspect/templates/state.md +27 -0
  234. package/skills/gsd-path-loop/SKILL.md +98 -0
  235. package/skills/gsd-path-loop/agents/openai.yaml +6 -0
  236. package/skills/gsd-path-loop/examples/ci-repair.LOOP.md +61 -0
  237. package/skills/gsd-path-loop/scripts/_common.py +180 -0
  238. package/skills/gsd-path-loop/scripts/discussion_records.py +619 -0
  239. package/skills/gsd-path-loop/scripts/loop_run.py +837 -0
  240. package/skills/gsd-path-loop/templates/loop.md +59 -0
  241. package/skills/gsd-path-migrate/SKILL.md +134 -0
  242. package/skills/gsd-path-migrate/agents/openai.yaml +6 -0
  243. package/skills/gsd-path-migrate/scripts/core_hook_gate.py +34 -0
  244. package/skills/gsd-path-migrate/scripts/core_hook_settings.py +119 -0
  245. package/skills/gsd-path-migrate/scripts/discussion_records.py +619 -0
  246. package/skills/gsd-path-migrate/scripts/migrate_core.py +169 -0
  247. package/skills/gsd-path-plan/SKILL.md +411 -0
  248. package/skills/gsd-path-plan/agents/openai.yaml +6 -0
  249. package/skills/gsd-path-plan/references/dispatch.md +131 -0
  250. package/skills/gsd-path-plan/references/planner.md +104 -0
  251. package/skills/gsd-path-plan/scripts/_common.py +180 -0
  252. package/skills/gsd-path-plan/scripts/check_handoffs.py +1855 -0
  253. package/skills/gsd-path-plan/scripts/check_task_briefs.py +376 -0
  254. package/skills/gsd-path-plan/scripts/discussion_records.py +619 -0
  255. package/skills/gsd-path-plan/scripts/isolation.py +3538 -0
  256. package/skills/gsd-path-plan/scripts/pipeline_git.py +940 -0
  257. package/skills/gsd-path-plan/scripts/pipeline_state.py +2291 -0
  258. package/skills/gsd-path-plan/scripts/review_panel.py +553 -0
  259. package/skills/gsd-path-plan/scripts/state_checkpoint.py +1039 -0
  260. package/skills/gsd-path-plan/scripts/state_promote.py +743 -0
  261. package/skills/gsd-path-plan/scripts/workflow_run.py +117 -0
  262. package/skills/gsd-path-plan/templates/patch-findings.md +16 -0
  263. package/skills/gsd-path-plan/templates/plan-panel.md +25 -0
  264. package/skills/gsd-path-plan/templates/plan.md +89 -0
  265. package/skills/gsd-path-plan/templates/task.md +74 -0
  266. package/skills/gsd-path-research/SKILL.md +159 -0
  267. package/skills/gsd-path-research/agents/openai.yaml +6 -0
  268. package/skills/gsd-path-research/references/dispatch.md +131 -0
  269. package/skills/gsd-path-research/references/researcher.md +42 -0
  270. package/skills/gsd-path-research/scripts/_common.py +180 -0
  271. package/skills/gsd-path-research/scripts/check_handoffs.py +1855 -0
  272. package/skills/gsd-path-research/scripts/check_task_briefs.py +376 -0
  273. package/skills/gsd-path-research/scripts/discussion_records.py +619 -0
  274. package/skills/gsd-path-research/scripts/isolation.py +3538 -0
  275. package/skills/gsd-path-research/scripts/pipeline_git.py +940 -0
  276. package/skills/gsd-path-research/scripts/pipeline_state.py +2291 -0
  277. package/skills/gsd-path-research/scripts/state_checkpoint.py +1039 -0
  278. package/skills/gsd-path-research/scripts/state_promote.py +743 -0
  279. package/skills/gsd-path-research/templates/evidence.md +24 -0
  280. package/skills/gsd-path-research/templates/research-handoff.md +25 -0
  281. package/skills/gsd-path-roadmap/SKILL.md +215 -0
  282. package/skills/gsd-path-roadmap/agents/openai.yaml +6 -0
  283. package/skills/gsd-path-roadmap/references/dispatch.md +131 -0
  284. package/skills/gsd-path-roadmap/references/roadmapper.md +48 -0
  285. package/skills/gsd-path-roadmap/scripts/_common.py +180 -0
  286. package/skills/gsd-path-roadmap/scripts/archive_milestone.py +2265 -0
  287. package/skills/gsd-path-roadmap/scripts/check_handoffs.py +1855 -0
  288. package/skills/gsd-path-roadmap/scripts/check_task_briefs.py +376 -0
  289. package/skills/gsd-path-roadmap/scripts/detect_project.py +1517 -0
  290. package/skills/gsd-path-roadmap/scripts/discussion_records.py +619 -0
  291. package/skills/gsd-path-roadmap/scripts/discussion_validate.py +1321 -0
  292. package/skills/gsd-path-roadmap/scripts/integration.py +1433 -0
  293. package/skills/gsd-path-roadmap/scripts/isolation.py +3538 -0
  294. package/skills/gsd-path-roadmap/scripts/pipeline_git.py +940 -0
  295. package/skills/gsd-path-roadmap/scripts/pipeline_state.py +2291 -0
  296. package/skills/gsd-path-roadmap/scripts/promote_lookahead.py +415 -0
  297. package/skills/gsd-path-roadmap/scripts/review_panel.py +553 -0
  298. package/skills/gsd-path-roadmap/scripts/state_checkpoint.py +1039 -0
  299. package/skills/gsd-path-roadmap/scripts/state_promote.py +743 -0
  300. package/skills/gsd-path-roadmap/templates/roadmap.md +38 -0
  301. package/skills/gsd-path-ship/SKILL.md +412 -0
  302. package/skills/gsd-path-ship/agents/openai.yaml +6 -0
  303. package/skills/gsd-path-ship/references/dispatch.md +131 -0
  304. package/skills/gsd-path-ship/references/reviewer.md +188 -0
  305. package/skills/gsd-path-ship/scripts/_common.py +180 -0
  306. package/skills/gsd-path-ship/scripts/archive_milestone.py +2265 -0
  307. package/skills/gsd-path-ship/scripts/build_state.py +854 -0
  308. package/skills/gsd-path-ship/scripts/check_handoffs.py +1855 -0
  309. package/skills/gsd-path-ship/scripts/check_task_briefs.py +376 -0
  310. package/skills/gsd-path-ship/scripts/discussion_records.py +619 -0
  311. package/skills/gsd-path-ship/scripts/discussion_validate.py +1321 -0
  312. package/skills/gsd-path-ship/scripts/integration.py +1433 -0
  313. package/skills/gsd-path-ship/scripts/isolation.py +3538 -0
  314. package/skills/gsd-path-ship/scripts/lean_verification.py +256 -0
  315. package/skills/gsd-path-ship/scripts/pipeline_git.py +940 -0
  316. package/skills/gsd-path-ship/scripts/pipeline_state.py +2291 -0
  317. package/skills/gsd-path-ship/scripts/review_panel.py +553 -0
  318. package/skills/gsd-path-ship/scripts/state_checkpoint.py +1039 -0
  319. package/skills/gsd-path-ship/scripts/state_promote.py +743 -0
  320. package/skills/gsd-path-ship/scripts/workflow_run.py +117 -0
  321. package/skills/gsd-path-ship/templates/archive-manifest.md +37 -0
  322. package/skills/gsd-path-ship/templates/final-review.md +27 -0
  323. package/skills/gsd-path-ship/templates/gap-review.md +20 -0
  324. package/skills/gsd-path-ship/templates/patch-findings.md +16 -0
  325. package/skills/gsd-path-ship/templates/wave-review.md +64 -0
  326. package/skills/gsd-path-undo/SKILL.md +45 -0
  327. package/skills/gsd-path-undo/agents/openai.yaml +6 -0
  328. package/skills/gsd-path-undo/scripts/_common.py +180 -0
  329. package/skills/gsd-path-undo/scripts/discussion_records.py +619 -0
  330. package/skills/gsd-path-undo/scripts/pipeline_undo.py +1039 -0
  331. package/skills/path/BUILD.md +700 -0
  332. package/skills/path/DECIDE.md +115 -0
  333. package/skills/path/DEFINE.md +297 -0
  334. package/skills/path/DOCS-AUDIT.md +190 -0
  335. package/skills/path/INSPECT.md +167 -0
  336. package/skills/path/PLAN.md +411 -0
  337. package/skills/path/RESEARCH.md +159 -0
  338. package/skills/path/ROADMAP.md +215 -0
  339. package/skills/path/SHIP.md +412 -0
  340. package/skills/path/SKILL.md +471 -0
  341. package/skills/path/agents/openai.yaml +6 -0
  342. package/skills/path/references/codebase-mapper.md +52 -0
  343. package/skills/path/references/coder.md +90 -0
  344. package/skills/path/references/decider.md +36 -0
  345. package/skills/path/references/dispatch.md +131 -0
  346. package/skills/path/references/docs-auditor.md +60 -0
  347. package/skills/path/references/planner.md +104 -0
  348. package/skills/path/references/researcher.md +42 -0
  349. package/skills/path/references/reviewer.md +188 -0
  350. package/skills/path/references/roadmapper.md +48 -0
  351. package/skills/path/scripts/_common.py +180 -0
  352. package/skills/path/scripts/archive_milestone.py +2265 -0
  353. package/skills/path/scripts/bootstrap_repository.py +710 -0
  354. package/skills/path/scripts/build_state.py +854 -0
  355. package/skills/path/scripts/check_docs_audit.py +463 -0
  356. package/skills/path/scripts/check_handoffs.py +1855 -0
  357. package/skills/path/scripts/check_task_briefs.py +376 -0
  358. package/skills/path/scripts/check_update.py +93 -0
  359. package/skills/path/scripts/detect_project.py +1517 -0
  360. package/skills/path/scripts/discussion_records.py +619 -0
  361. package/skills/path/scripts/discussion_validate.py +1321 -0
  362. package/skills/path/scripts/dispatch_driver.py +1812 -0
  363. package/skills/path/scripts/integration.py +1433 -0
  364. package/skills/path/scripts/isolation.py +3538 -0
  365. package/skills/path/scripts/lean_verification.py +256 -0
  366. package/skills/path/scripts/loop_run.py +837 -0
  367. package/skills/path/scripts/pipeline_git.py +940 -0
  368. package/skills/path/scripts/pipeline_state.py +2291 -0
  369. package/skills/path/scripts/promote_lookahead.py +415 -0
  370. package/skills/path/scripts/review_findings.py +714 -0
  371. package/skills/path/scripts/review_panel.py +553 -0
  372. package/skills/path/scripts/state_checkpoint.py +1039 -0
  373. package/skills/path/scripts/state_promote.py +743 -0
  374. package/skills/path/scripts/token_budget.py +146 -0
  375. package/skills/path/scripts/workflow_run.py +117 -0
  376. package/skills/path/templates/answers.md +29 -0
  377. package/skills/path/templates/archive-manifest.md +37 -0
  378. package/skills/path/templates/charter.md +50 -0
  379. package/skills/path/templates/codebase.md +46 -0
  380. package/skills/path/templates/dialogue.md +21 -0
  381. package/skills/path/templates/docs-audit.md +69 -0
  382. package/skills/path/templates/evidence.md +24 -0
  383. package/skills/path/templates/final-review.md +27 -0
  384. package/skills/path/templates/gap-review.md +20 -0
  385. package/skills/path/templates/intent.md +98 -0
  386. package/skills/path/templates/loop.md +59 -0
  387. package/skills/path/templates/patch-findings.md +16 -0
  388. package/skills/path/templates/plan-panel.md +25 -0
  389. package/skills/path/templates/plan.md +89 -0
  390. package/skills/path/templates/repository.md +16 -0
  391. package/skills/path/templates/research-handoff.md +25 -0
  392. package/skills/path/templates/roadmap.md +38 -0
  393. package/skills/path/templates/skeptic.md +36 -0
  394. package/skills/path/templates/state.md +27 -0
  395. package/skills/path/templates/synthesis.md +36 -0
  396. package/skills/path/templates/task.md +74 -0
  397. package/skills/path/templates/wave-panel.md +25 -0
  398. package/skills/path/templates/wave-review.md +64 -0
@@ -0,0 +1,1039 @@
1
+ #!/usr/bin/env python3
2
+ # gsd-path project runtime
3
+ """Checkpoint plan and roadmap approvals and classify plan drift for pipeline_state."""
4
+
5
+ from __future__ import annotations
6
+
7
+ import hashlib
8
+ import json
9
+ import os
10
+ import re
11
+ from datetime import date
12
+ from pathlib import Path, PurePosixPath
13
+ from typing import Mapping, Optional, Sequence
14
+
15
+ try:
16
+ from isolation import IsolationError, checkpoint as isolation_checkpoint, require_commit, require_full_sha, PROJECT_ENTRIES
17
+ except ModuleNotFoundError as error: # pragma: no cover - package imports used by tests
18
+ if error.name != "isolation":
19
+ raise
20
+ from scripts.isolation import IsolationError, checkpoint as isolation_checkpoint, require_commit, require_full_sha, PROJECT_ENTRIES
21
+
22
+
23
+ def _sha256(content: str) -> str:
24
+ return hashlib.sha256(content.encode("utf-8")).hexdigest()
25
+
26
+
27
+ def _tree_digest(path: Path, excluded: Sequence[str] = ()) -> str:
28
+ if path.is_symlink() or not path.is_dir():
29
+ raise PipelineStateError(f"promotion path must be a real directory: {path}")
30
+ digest = hashlib.sha256()
31
+ for child in sorted(path.rglob("*")):
32
+ if child.is_symlink():
33
+ raise PipelineStateError(f"promotion path contains a symlink: {child}")
34
+ relative = child.relative_to(path).as_posix()
35
+ if PurePosixPath(relative).parts[0] in excluded:
36
+ continue
37
+ if child.is_dir():
38
+ digest.update(f"d\0{relative}\0".encode())
39
+ elif child.is_file():
40
+ digest.update(f"f\0{relative}\0".encode())
41
+ digest.update(child.read_bytes())
42
+ else:
43
+ raise PipelineStateError(f"promotion path is not a file or directory: {child}")
44
+ return digest.hexdigest()
45
+
46
+
47
+ def _checkpoint_artifact_digest(project: Path, mutable_paths: set[str]) -> str:
48
+ """Hash every approval artifact except the helper-owned metadata files."""
49
+ digest = hashlib.sha256()
50
+ for child in sorted(project.rglob("*")):
51
+ if child.is_symlink():
52
+ raise PipelineStateError(f"checkpoint path contains a symlink: {child}")
53
+ relative = child.relative_to(project).as_posix()
54
+ # Unsupported entries (OS junk) are refused by the checkpoint itself; leaving them
55
+ # out lets a refused approval resume after the owner removes them.
56
+ if relative in mutable_paths or relative.split("/", 1)[0] not in PROJECT_ENTRIES:
57
+ continue
58
+ if child.is_dir():
59
+ digest.update(f"d\0{relative}\0".encode())
60
+ elif child.is_file():
61
+ digest.update(f"f\0{relative}\0".encode())
62
+ digest.update(child.read_bytes())
63
+ else:
64
+ raise PipelineStateError(f"checkpoint path is not a file or directory: {child}")
65
+ return digest.hexdigest()
66
+
67
+
68
+ def _roadmap_status(lines: list[str], start: int, end: int) -> str:
69
+ values = []
70
+ for line in lines[start:end]:
71
+ match = re.fullmatch(r"Status:\s*(\S+)\s*", line.rstrip("\r\n"))
72
+ if match:
73
+ values.append(match.group(1))
74
+ if len(values) != 1:
75
+ raise PipelineStateError("ROADMAP.md milestone must have one Status field")
76
+ return values[0]
77
+
78
+
79
+ def _activate_roadmap_milestone(text: str, milestone: str) -> str:
80
+ sections = pipeline_state._roadmap_sections(text)
81
+ if milestone not in sections:
82
+ raise PipelineStateError(f"ROADMAP.md is missing selected milestone: {milestone}")
83
+ lines = text.splitlines(keepends=True)
84
+ active = [
85
+ slug
86
+ for slug, (_, start, end) in sections.items()
87
+ if _roadmap_status(lines, start, end) == "active"
88
+ ]
89
+ if active:
90
+ raise PipelineStateError(
91
+ "ROADMAP.md already has an active milestone: " + ", ".join(active)
92
+ )
93
+ _, start, end = sections[milestone]
94
+ pipeline_state._replace_roadmap_field(lines, start, end, "Status", {"pending"}, "active")
95
+ return "".join(lines)
96
+
97
+
98
+ def _approval_details(
99
+ kind: str,
100
+ state: PipelineState,
101
+ selected_milestone: Optional[str],
102
+ ) -> tuple[dict[str, Optional[str]], str, str, str]:
103
+ if kind == "plan":
104
+ if selected_milestone is not None:
105
+ raise PipelineStateError("plan approval does not accept a selected milestone")
106
+ if (state.phase, state.status) != ("plan", "active"):
107
+ raise PipelineStateError("plan approval requires STATE plan/active")
108
+ return (
109
+ {"status": "done"},
110
+ "plan approved",
111
+ "plan: build plan approved",
112
+ "Why: approved plan checkpoint\n"
113
+ f"Milestone: {state.milestone or 'none'}",
114
+ )
115
+ if kind == "roadmap":
116
+ if (state.phase, state.status) != ("roadmap", "active"):
117
+ raise PipelineStateError("roadmap approval requires STATE roadmap/active")
118
+ if selected_milestone is None or not SLUG_RE.fullmatch(selected_milestone):
119
+ raise PipelineStateError("roadmap approval requires a selected milestone slug")
120
+ return (
121
+ {"status": "done", "milestone": selected_milestone},
122
+ "program roadmap approved",
123
+ "roadmap: program roadmap approved",
124
+ "Why: approved roadmap checkpoint",
125
+ )
126
+ raise PipelineStateError(f"unsupported approval kind: {kind}")
127
+
128
+
129
+ def _checkpoint_request_fields(
130
+ kind: str,
131
+ project_dir: str,
132
+ selected_milestone: Optional[str],
133
+ expected_head: str,
134
+ ) -> dict[str, object]:
135
+ return {
136
+ "kind": kind,
137
+ "project_dir": project_dir,
138
+ "selected_milestone": selected_milestone,
139
+ "expected_head": expected_head,
140
+ }
141
+
142
+
143
+ def _checkpoint_file(repo: Path, relative: str, label: str) -> Path:
144
+ path_value = PurePosixPath(relative)
145
+ if (
146
+ path_value.is_absolute()
147
+ or ".." in path_value.parts
148
+ or not path_value.parts
149
+ or path_value.parts[0] != ".project"
150
+ ):
151
+ raise PipelineStateError(f"checkpoint {label} path is invalid: {relative}")
152
+ path = repo.joinpath(*path_value.parts)
153
+ if path.is_symlink() or not path.is_file():
154
+ raise PipelineStateError(f"checkpoint {label} must be a real file: {path}")
155
+ return path
156
+
157
+
158
+ def _approval_journal(repo: Path, journal: Mapping[str, object]) -> dict[str, object]:
159
+ expected_keys = {
160
+ "schema",
161
+ "repo",
162
+ "kind",
163
+ "project_dir",
164
+ "selected_milestone",
165
+ "expected_head",
166
+ "event",
167
+ "event_date",
168
+ "subject",
169
+ "body",
170
+ "artifact_digest",
171
+ "state_path",
172
+ "state_before",
173
+ "state_after",
174
+ "roadmap_path",
175
+ "roadmap_before",
176
+ "roadmap_after",
177
+ }
178
+ if set(journal) != expected_keys:
179
+ raise PipelineStateError("checkpoint journal has invalid fields")
180
+ if journal.get("schema") != CHECKPOINT_SCHEMA or journal.get("repo") != str(repo):
181
+ raise PipelineStateError("checkpoint journal does not belong to this worktree")
182
+ string_fields = (
183
+ "kind",
184
+ "project_dir",
185
+ "expected_head",
186
+ "event",
187
+ "event_date",
188
+ "subject",
189
+ "body",
190
+ "artifact_digest",
191
+ "state_path",
192
+ "state_before",
193
+ "state_after",
194
+ )
195
+ if any(not isinstance(journal.get(field), str) for field in string_fields):
196
+ raise PipelineStateError("checkpoint journal has invalid string fields")
197
+ if journal["kind"] not in CHECKPOINT_KINDS:
198
+ raise PipelineStateError("checkpoint journal has invalid approval kind")
199
+ if journal["project_dir"] not in {".project", ".project/next"}:
200
+ raise PipelineStateError("checkpoint journal has invalid project directory")
201
+ selected = journal["selected_milestone"]
202
+ if selected is not None and not isinstance(selected, str):
203
+ raise PipelineStateError("checkpoint journal has invalid selected milestone")
204
+ roadmap_values = (
205
+ journal["roadmap_path"],
206
+ journal["roadmap_before"],
207
+ journal["roadmap_after"],
208
+ )
209
+ if any(value is not None and not isinstance(value, str) for value in roadmap_values):
210
+ raise PipelineStateError("checkpoint journal has invalid roadmap fields")
211
+
212
+ expected_head = str(journal["expected_head"])
213
+ resolved_head = pipeline_state._run_git(
214
+ repo,
215
+ "rev-parse",
216
+ "--verify",
217
+ f"{expected_head}^{{commit}}",
218
+ ).stdout.strip()
219
+ if resolved_head != expected_head:
220
+ raise PipelineStateError("checkpoint journal expected-head is not an exact commit SHA")
221
+
222
+ project_dir = str(journal["project_dir"])
223
+ expected_state_path = f"{project_dir}/STATE.md"
224
+ if journal["state_path"] != expected_state_path:
225
+ raise PipelineStateError("checkpoint journal has an invalid STATE path")
226
+ before = pipeline_state._state_from_text(str(journal["state_before"]), "checkpoint before STATE.md")
227
+ changes, event, subject, body = _approval_details(
228
+ str(journal["kind"]),
229
+ before,
230
+ selected if isinstance(selected, str) else None,
231
+ )
232
+ expected = {
233
+ "phase": before.phase,
234
+ "status": before.status,
235
+ "milestone": before.milestone,
236
+ "branch": before.branch,
237
+ "archive": before.archive,
238
+ }
239
+ _, after, rendered = pipeline_state._render_transition(
240
+ before,
241
+ str(journal["state_before"]),
242
+ expected,
243
+ changes,
244
+ event,
245
+ project_dir,
246
+ str(journal["event_date"]),
247
+ str(journal["kind"]),
248
+ )
249
+ if (
250
+ journal["event"] != event
251
+ or journal["subject"] != subject
252
+ or journal["body"] != body
253
+ or journal["state_after"] != rendered
254
+ ):
255
+ raise PipelineStateError("checkpoint journal approval contract is invalid")
256
+ if str(journal["kind"]) == "roadmap":
257
+ if project_dir != ".project":
258
+ raise PipelineStateError("roadmap approval is legal only on the active track")
259
+ if journal["roadmap_path"] != ".project/ROADMAP.md":
260
+ raise PipelineStateError("checkpoint journal has an invalid ROADMAP path")
261
+ expected_roadmap = _activate_roadmap_milestone(
262
+ str(journal["roadmap_before"]),
263
+ str(selected),
264
+ )
265
+ if journal["roadmap_after"] != expected_roadmap:
266
+ raise PipelineStateError("checkpoint journal has an invalid ROADMAP result")
267
+ elif any(value is not None for value in roadmap_values):
268
+ raise PipelineStateError("plan checkpoint journal unexpectedly owns ROADMAP.md")
269
+ pipeline_state._validate_state_context(after, project_dir, "checkpoint after STATE.md")
270
+ return dict(journal)
271
+
272
+
273
+ def _remove_checkpoint_temporary(path: Path) -> None:
274
+ temporary = path.parent / f".{path.name}.gsd-path-tmp"
275
+ if temporary.is_symlink():
276
+ raise PipelineStateError(f"checkpoint temporary path is a symlink: {temporary}")
277
+ if temporary.exists():
278
+ if not temporary.is_file():
279
+ raise PipelineStateError(f"checkpoint temporary path is invalid: {temporary}")
280
+ temporary.unlink()
281
+
282
+
283
+ def _converge_checkpoint_file(path: Path, before: str, after: str, label: str) -> None:
284
+ _remove_checkpoint_temporary(path)
285
+ current = pipeline_state._read_real_file(path, label)
286
+ if current not in {before, after}:
287
+ raise PipelineStateError(f"{label} drifted during checkpoint recovery")
288
+ if current != after:
289
+ pipeline_state._atomic_write(path, after)
290
+
291
+
292
+ def _unlink_checkpoint_journal(path: Path) -> None:
293
+ path.unlink()
294
+ descriptor = os.open(path.parent, os.O_RDONLY)
295
+ try:
296
+ os.fsync(descriptor)
297
+ finally:
298
+ os.close(descriptor)
299
+
300
+
301
+ def _resume_checkpoint_locked(
302
+ repo: Path,
303
+ project: Path,
304
+ journal_path: Path,
305
+ journal: Mapping[str, object],
306
+ ) -> dict[str, object]:
307
+ transaction = _approval_journal(repo, journal)
308
+ state_path = _checkpoint_file(repo, str(transaction["state_path"]), "STATE.md")
309
+ mutable = {state_path.relative_to(project).as_posix()}
310
+ roadmap_path: Optional[Path] = None
311
+ if transaction["roadmap_path"] is not None:
312
+ roadmap_path = _checkpoint_file(
313
+ repo,
314
+ str(transaction["roadmap_path"]),
315
+ "ROADMAP.md",
316
+ )
317
+ mutable.add(roadmap_path.relative_to(project).as_posix())
318
+ _remove_checkpoint_temporary(state_path)
319
+ if roadmap_path is not None:
320
+ _remove_checkpoint_temporary(roadmap_path)
321
+ if _checkpoint_artifact_digest(project, mutable) != transaction["artifact_digest"]:
322
+ raise PipelineStateError("approval artifacts drifted after checkpoint preparation")
323
+ outside = [path for path in pipeline_state._worktree_changes(repo) if not path.startswith(".project/")]
324
+ if outside:
325
+ raise PipelineStateError(
326
+ "checkpoint found changes outside .project: " + ", ".join(outside)
327
+ )
328
+
329
+ head = pipeline_state._run_git(repo, "rev-parse", "HEAD").stdout.strip()
330
+ expected_head = str(transaction["expected_head"])
331
+ if head == expected_head:
332
+ _converge_checkpoint_file(
333
+ state_path,
334
+ str(transaction["state_before"]),
335
+ str(transaction["state_after"]),
336
+ "STATE.md",
337
+ )
338
+ if roadmap_path is not None:
339
+ _converge_checkpoint_file(
340
+ roadmap_path,
341
+ str(transaction["roadmap_before"]),
342
+ str(transaction["roadmap_after"]),
343
+ "ROADMAP.md",
344
+ )
345
+ else:
346
+ if pipeline_state._worktree_changes(repo):
347
+ raise PipelineStateError("completed checkpoint has worktree drift")
348
+ if pipeline_state._read_real_file(state_path, "STATE.md") != transaction["state_after"]:
349
+ raise PipelineStateError("completed checkpoint has the wrong STATE.md")
350
+ if roadmap_path is not None and (
351
+ pipeline_state._read_real_file(roadmap_path, "ROADMAP.md")
352
+ != transaction["roadmap_after"]
353
+ ):
354
+ raise PipelineStateError("completed checkpoint has the wrong ROADMAP.md")
355
+
356
+ try:
357
+ committed = isolation_checkpoint(
358
+ repo,
359
+ expected_head,
360
+ str(transaction["subject"]),
361
+ str(transaction["body"]),
362
+ [".project"],
363
+ )
364
+ except IsolationError as error:
365
+ raise PipelineStateError(f"approval checkpoint failed: {error}") from error
366
+ if committed.get("commit") != pipeline_state._run_git(repo, "rev-parse", "HEAD").stdout.strip():
367
+ raise PipelineStateError("approval checkpoint returned the wrong commit")
368
+ _unlink_checkpoint_journal(journal_path)
369
+ state = pipeline_state._state_from_text(str(transaction["state_after"]))
370
+ return {
371
+ "schema": CHECKPOINT_SCHEMA,
372
+ "status": "approved",
373
+ "kind": transaction["kind"],
374
+ "project_dir": transaction["project_dir"],
375
+ "commit": committed["commit"],
376
+ "paths": committed.get("paths"),
377
+ "state": state.json(),
378
+ }
379
+
380
+
381
+ def _validate_plan_briefs(repo: Path, kind: str, project_dir: str) -> None:
382
+ if kind != "plan":
383
+ return
384
+ head = pipeline_state._run_git(repo, "rev-parse", "--verify", "HEAD", check=False)
385
+ if head.returncode != 0:
386
+ # Pre-Git approval defers base-dependent checks to build.
387
+ return
388
+ try:
389
+ from check_task_briefs import BriefError, validate_task_briefs
390
+ except ModuleNotFoundError as error: # pragma: no cover - package imports
391
+ if error.name != "check_task_briefs":
392
+ raise
393
+ from scripts.check_task_briefs import BriefError, validate_task_briefs
394
+ try:
395
+ import check_handoffs
396
+ except ModuleNotFoundError as error:
397
+ if error.name != "check_handoffs":
398
+ raise
399
+ from scripts import check_handoffs
400
+ try:
401
+ tasks, dependency_files = check_handoffs.plan_brief_inputs(repo, project_dir)
402
+ landed_bases = {}
403
+ for task_id, text in tasks.items():
404
+ if check_handoffs._task_scalar(text, task_id, "status") != "done":
405
+ continue
406
+ agent = check_handoffs._task_scalar(text, task_id, "agent")
407
+ if agent in {"", "null"}:
408
+ raise BriefError(f"{task_id} landed task has no recorded agent")
409
+ recorded_base = check_handoffs._task_scalar(text, task_id, "base")
410
+ try:
411
+ landed_bases[task_id] = require_commit(repo, require_full_sha(recorded_base))
412
+ except IsolationError as error:
413
+ raise BriefError(f"{task_id} landed task has invalid historical base: {error}") from error
414
+ dependency_files[task_id] = set()
415
+ validate_task_briefs(
416
+ repo, head.stdout.strip(), f"{project_dir}/tasks",
417
+ dependency_files=dependency_files, landed_bases=landed_bases,
418
+ )
419
+ except (BriefError, check_handoffs.HandoffError) as error:
420
+ raise PipelineStateError(f"task brief validation failed: {error}") from error
421
+
422
+
423
+ def checkpoint_approval(
424
+ repo: Path,
425
+ kind: str,
426
+ expected_head: str,
427
+ project_dir: str = ".project",
428
+ selected_milestone: Optional[str] = None,
429
+ ) -> dict[str, object]:
430
+ """Approve plan or roadmap metadata and commit it as one resumable transaction."""
431
+ resolved = pipeline_state._repo_root(repo)
432
+ if project_dir not in {".project", ".project/next"}:
433
+ raise PipelineStateError("approval project-dir must be .project or .project/next")
434
+ project = pipeline_state._track_root(resolved, ".project")
435
+ journal_path = pipeline_state._git_path(resolved, CHECKPOINT_JOURNAL_NAME)
436
+ request = _checkpoint_request_fields(
437
+ kind,
438
+ project_dir,
439
+ selected_milestone,
440
+ expected_head,
441
+ )
442
+ with pipeline_state._state_lock(project):
443
+ if journal_path.exists() or journal_path.is_symlink():
444
+ transaction = _approval_journal(resolved, pipeline_state._read_json(journal_path))
445
+ mismatches = {
446
+ key: {"expected": value, "actual": transaction.get(key)}
447
+ for key, value in request.items()
448
+ if transaction.get(key) != value
449
+ }
450
+ if mismatches:
451
+ raise PipelineStateError(
452
+ "checkpoint journal does not match request: "
453
+ + json.dumps(mismatches, sort_keys=True)
454
+ )
455
+ return _resume_checkpoint_locked(resolved, project, journal_path, transaction)
456
+
457
+ resolved_head = pipeline_state._run_git(
458
+ resolved,
459
+ "rev-parse",
460
+ "--verify",
461
+ f"{expected_head}^{{commit}}",
462
+ ).stdout.strip()
463
+ if resolved_head != expected_head:
464
+ raise PipelineStateError("expected-head must be an exact full commit SHA")
465
+ if pipeline_state._run_git(resolved, "rev-parse", "HEAD").stdout.strip() != expected_head:
466
+ raise PipelineStateError("approval must start at expected-head")
467
+ active_state, _, _ = pipeline_state.load_state(resolved)
468
+ if active_state.branch is None or pipeline_state._current_branch(resolved) != active_state.branch:
469
+ raise PipelineStateError("approval requires the active bound branch")
470
+ outside = [path for path in pipeline_state._worktree_changes(resolved) if not path.startswith(".project/")]
471
+ if outside:
472
+ raise PipelineStateError(
473
+ "checkpoint found changes outside .project: " + ", ".join(outside)
474
+ )
475
+
476
+ state, state_before, state_path = pipeline_state.load_state(resolved, project_dir)
477
+ changes, event, subject, body = _approval_details(kind, state, selected_milestone)
478
+ _validate_plan_briefs(resolved, kind, project_dir)
479
+ expected = {
480
+ "phase": state.phase,
481
+ "status": state.status,
482
+ "milestone": state.milestone,
483
+ "branch": state.branch,
484
+ "archive": state.archive,
485
+ }
486
+ event_date = date.today().isoformat()
487
+ _, _, state_after = pipeline_state._render_transition(
488
+ state,
489
+ state_before,
490
+ expected,
491
+ changes,
492
+ event,
493
+ project_dir,
494
+ event_date,
495
+ kind,
496
+ )
497
+ roadmap_path: Optional[Path] = None
498
+ roadmap_before: Optional[str] = None
499
+ roadmap_after: Optional[str] = None
500
+ if kind == "roadmap":
501
+ if project_dir != ".project":
502
+ raise PipelineStateError("roadmap approval is legal only on the active track")
503
+ assert selected_milestone is not None
504
+ roadmap_path = project / "ROADMAP.md"
505
+ roadmap_before = pipeline_state._read_real_file(roadmap_path, "ROADMAP.md")
506
+ roadmap_after = _activate_roadmap_milestone(
507
+ roadmap_before,
508
+ selected_milestone,
509
+ )
510
+
511
+ state_relative = state_path.relative_to(project).as_posix()
512
+ mutable = {state_relative}
513
+ if roadmap_path is not None:
514
+ mutable.add(roadmap_path.relative_to(project).as_posix())
515
+ journal: dict[str, object] = {
516
+ "schema": CHECKPOINT_SCHEMA,
517
+ "repo": str(resolved),
518
+ **request,
519
+ "event": event,
520
+ "event_date": event_date,
521
+ "subject": subject,
522
+ "body": body,
523
+ "artifact_digest": _checkpoint_artifact_digest(project, mutable),
524
+ "state_path": f".project/{state_relative}",
525
+ "state_before": state_before,
526
+ "state_after": state_after,
527
+ "roadmap_path": ".project/ROADMAP.md" if roadmap_path is not None else None,
528
+ "roadmap_before": roadmap_before,
529
+ "roadmap_after": roadmap_after,
530
+ }
531
+ pipeline_state._write_json(journal_path, journal)
532
+ return _resume_checkpoint_locked(resolved, project, journal_path, journal)
533
+
534
+
535
+ def _checkpoint_deferral_error(repo: Path, kind: str, patch: bool) -> Optional[str]:
536
+ """Return why a deferred approval is illegal, or None when it is allowed."""
537
+ if patch:
538
+ return None if kind == "plan" else "--patch applies only to --kind plan"
539
+ if pipeline_state._run_git(repo, "rev-parse", "--verify", "HEAD", check=False).returncode != 0:
540
+ return None
541
+ binding = repo / ".project" / "REPOSITORY.md"
542
+ if binding.is_file() and not binding.is_symlink():
543
+ kinds = [
544
+ line.removeprefix("Kind:").strip()
545
+ for line in binding.read_text(encoding="utf-8").splitlines()
546
+ if line.startswith("Kind:")
547
+ ]
548
+ if kinds == ["new-github"]:
549
+ return None
550
+ return (
551
+ "checkpoint deferral requires no Git HEAD, a Kind: new-github "
552
+ "REPOSITORY.md, or --patch; run: pipeline_state.py approve "
553
+ f"--kind {kind} --expected-head <full HEAD>"
554
+ )
555
+
556
+
557
+ def defer_approval(
558
+ repo: Path,
559
+ kind: str,
560
+ project_dir: str = ".project",
561
+ selected_milestone: Optional[str] = None,
562
+ patch: bool = False,
563
+ ) -> dict[str, object]:
564
+ """Approve plan or roadmap metadata without a checkpoint commit.
565
+
566
+ Legal only before Git exists, during a new-repository transaction, or for
567
+ a patch plan; the next build transition commit owns the artifacts.
568
+ """
569
+ resolved = pipeline_state._repo_root(repo)
570
+ if project_dir not in {".project", ".project/next"}:
571
+ raise PipelineStateError("approval project-dir must be .project or .project/next")
572
+ error = _checkpoint_deferral_error(resolved, kind, patch)
573
+ if error:
574
+ raise PipelineStateError(error)
575
+ project = pipeline_state._track_root(resolved, ".project")
576
+ with pipeline_state._state_lock(project):
577
+ state, state_before, state_path = pipeline_state.load_state(resolved, project_dir)
578
+ changes, event, _, _ = _approval_details(kind, state, selected_milestone)
579
+ _validate_plan_briefs(resolved, kind, project_dir)
580
+ if patch:
581
+ event = "patch plan approved"
582
+ expected = {
583
+ "phase": state.phase,
584
+ "status": state.status,
585
+ "milestone": state.milestone,
586
+ "branch": state.branch,
587
+ "archive": state.archive,
588
+ }
589
+ _, after, state_after = pipeline_state._render_transition(
590
+ state,
591
+ state_before,
592
+ expected,
593
+ changes,
594
+ event,
595
+ project_dir,
596
+ approval_kind=kind,
597
+ )
598
+ if kind == "roadmap":
599
+ if project_dir != ".project":
600
+ raise PipelineStateError("roadmap approval is legal only on the active track")
601
+ assert selected_milestone is not None
602
+ roadmap_path = project / "ROADMAP.md"
603
+ roadmap_after = _activate_roadmap_milestone(
604
+ pipeline_state._read_real_file(roadmap_path, "ROADMAP.md"),
605
+ selected_milestone,
606
+ )
607
+ pipeline_state._atomic_write(roadmap_path, roadmap_after)
608
+ pipeline_state._atomic_write(state_path, state_after)
609
+ return {
610
+ "schema": CHECKPOINT_SCHEMA,
611
+ "status": "approved",
612
+ "kind": kind,
613
+ "project_dir": project_dir,
614
+ "commit": None,
615
+ "deferred": True,
616
+ "state": after.json(),
617
+ }
618
+
619
+
620
+ def resume_checkpoint(repo: Path) -> dict[str, object]:
621
+ """Resume the single journaled plan or roadmap approval transaction."""
622
+ resolved = pipeline_state._repo_root(repo)
623
+ project = pipeline_state._track_root(resolved, ".project")
624
+ journal_path = pipeline_state._git_path(resolved, CHECKPOINT_JOURNAL_NAME)
625
+ with pipeline_state._state_lock(project):
626
+ if not journal_path.exists() and not journal_path.is_symlink():
627
+ raise PipelineStateError("no approval checkpoint journal exists")
628
+ return _resume_checkpoint_locked(
629
+ resolved,
630
+ project,
631
+ journal_path,
632
+ pipeline_state._read_json(journal_path),
633
+ )
634
+
635
+
636
+ def _task_files(text: str, label: str) -> tuple[str, list[str]]:
637
+ lines = text.splitlines()
638
+ if not lines or lines[0] != "---":
639
+ raise PipelineStateError(f"{label} is missing YAML frontmatter")
640
+ task_id = ""
641
+ files: list[str] = []
642
+ closing: Optional[int] = None
643
+ for index, line in enumerate(lines[1:], start=1):
644
+ if line == "---":
645
+ closing = index
646
+ break
647
+ id_match = re.fullmatch(r"id:\s*([^#\s]+)(?:\s+#.*)?", line)
648
+ if id_match:
649
+ if task_id:
650
+ raise PipelineStateError(f"{label} repeats task id")
651
+ task_id = id_match.group(1).strip("\"'")
652
+ files_match = re.fullmatch(r"files:\s*(.*?)(?:\s+#.*)?", line)
653
+ if files_match is None:
654
+ continue
655
+ inline = re.fullmatch(r"\[(.*)\]", files_match.group(1).strip())
656
+ if inline:
657
+ files = [
658
+ value.strip().strip("\"'")
659
+ for value in inline.group(1).split(",")
660
+ if value.strip()
661
+ ]
662
+ continue
663
+ cursor = index + 1
664
+ while cursor < len(lines):
665
+ item = re.match(r"^\s+-\s+(.+?)(?:\s+#.*)?$", lines[cursor])
666
+ if item is None:
667
+ break
668
+ files.append(item.group(1).strip().strip("\"'"))
669
+ cursor += 1
670
+ if closing is None:
671
+ raise PipelineStateError(f"{label} frontmatter is not closed")
672
+ if not TASK_ID_RE.fullmatch(task_id):
673
+ raise PipelineStateError(f"{label} has invalid task id: {task_id}")
674
+ if not files:
675
+ raise PipelineStateError(f"{label} has no declared files")
676
+ return task_id, files
677
+
678
+
679
+ def _approval_checkpoint(
680
+ repo: Path,
681
+ state: PipelineState,
682
+ revision: str = "HEAD",
683
+ ) -> Optional[str]:
684
+ parents = pipeline_state._run_git(repo, "show", "-s", "--format=%P", revision).stdout.split()
685
+ if len(parents) != 2:
686
+ return None
687
+ base, ship = parents
688
+ if pipeline_state._run_git(
689
+ repo,
690
+ "merge-base",
691
+ "--is-ancestor",
692
+ base,
693
+ ship,
694
+ check=False,
695
+ ).returncode != 0:
696
+ return None
697
+ commits = pipeline_state._run_git(
698
+ repo,
699
+ "rev-list",
700
+ "--first-parent",
701
+ ship,
702
+ f"^{base}",
703
+ ).stdout.splitlines()
704
+ candidates = []
705
+ for commit in commits:
706
+ raw = pipeline_state._run_git(repo, "cat-file", "commit", commit).stdout
707
+ if "\n\n" not in raw:
708
+ continue
709
+ message = raw.split("\n\n", 1)[1]
710
+ expected_message = pipeline_state._commit_message(
711
+ PLAN_APPROVAL_SUBJECT,
712
+ {
713
+ "Why": "approved plan checkpoint",
714
+ "Milestone": state.milestone or "none",
715
+ },
716
+ )
717
+ if message != expected_message:
718
+ continue
719
+ shown = pipeline_state._run_git(
720
+ repo,
721
+ "show",
722
+ f"{commit}:.project/next/STATE.md",
723
+ check=False,
724
+ )
725
+ if shown.returncode != 0:
726
+ continue
727
+ try:
728
+ candidate = pipeline_state._state_from_text(shown.stdout, "checkpoint next/STATE.md")
729
+ except PipelineStateError:
730
+ continue
731
+ if (
732
+ candidate.project == state.project
733
+ and candidate.milestone == state.milestone
734
+ and candidate.phase == "plan"
735
+ and candidate.status == "done"
736
+ and candidate.branch is None
737
+ and candidate.archive is None
738
+ ):
739
+ pipeline_state._validate_state_context(
740
+ candidate,
741
+ ".project/next",
742
+ "checkpoint next/STATE.md",
743
+ )
744
+ _validate_approval_track(repo, commit)
745
+ candidates.append(commit)
746
+ if len(candidates) > 1:
747
+ raise PipelineStateError(
748
+ "current milestone attempt has multiple matching plan approval checkpoints"
749
+ )
750
+ return candidates[0] if candidates else None
751
+
752
+
753
+ def _validate_approval_track(repo: Path, checkpoint: str) -> None:
754
+ prefix = ".project/next"
755
+ entries = [
756
+ line
757
+ for line in pipeline_state._run_git(
758
+ repo,
759
+ "ls-tree",
760
+ "-r",
761
+ checkpoint,
762
+ "--",
763
+ prefix,
764
+ ).stdout.splitlines()
765
+ if line
766
+ ]
767
+ paths = []
768
+ for entry in entries:
769
+ metadata, path = entry.split("\t", 1)
770
+ mode, object_type, _ = metadata.split()
771
+ if object_type != "blob" or mode not in {"100644", "100755"}:
772
+ raise PipelineStateError(
773
+ f"approval checkpoint has unsafe lookahead artifact: {path}"
774
+ )
775
+ relative = PurePosixPath(path).relative_to(prefix)
776
+ if not relative.parts or (
777
+ relative.as_posix() != "STATE.md"
778
+ and relative.parts[0] not in PROMOTION_TRACKS
779
+ ):
780
+ raise PipelineStateError(
781
+ f"approval checkpoint has unowned lookahead artifact: {path}"
782
+ )
783
+ paths.append(path)
784
+ required = {
785
+ ".project/next/STATE.md",
786
+ ".project/next/plan/PLAN.md",
787
+ }
788
+ if not required.issubset(paths):
789
+ raise PipelineStateError("approval checkpoint has an incomplete lookahead track")
790
+ tasks = _approved_task_contracts(repo, checkpoint)
791
+ changed = {
792
+ value
793
+ for value in pipeline_state._run_git(
794
+ repo,
795
+ "diff-tree",
796
+ "--no-commit-id",
797
+ "--name-only",
798
+ "-r",
799
+ checkpoint,
800
+ ).stdout.splitlines()
801
+ if value
802
+ }
803
+ required_changes = required | {contract.path for contract in tasks.values()}
804
+ if not required_changes.issubset(changed):
805
+ raise PipelineStateError(
806
+ "plan approval commit did not checkpoint its complete plan and task set"
807
+ )
808
+
809
+
810
+ def _safe_declared_paths(task_id: str, paths: Sequence[str]) -> tuple[str, ...]:
811
+ safe: list[str] = []
812
+ for value in paths:
813
+ relative = PurePosixPath(value)
814
+ if relative.is_absolute() or not relative.parts or ".." in relative.parts:
815
+ raise PipelineStateError(f"{task_id} declares unsafe path: {value}")
816
+ safe.append(relative.as_posix())
817
+ return tuple(safe)
818
+
819
+
820
+ def _git_text_at(repo: Path, commit: str, path: str) -> Optional[str]:
821
+ shown = pipeline_state._run_git(repo, "show", f"{commit}:{path}", check=False)
822
+ return shown.stdout if shown.returncode == 0 else None
823
+
824
+
825
+ def _approved_task_contracts(
826
+ repo: Path,
827
+ checkpoint: str,
828
+ ) -> dict[str, ApprovedTaskContract]:
829
+ prefix = PurePosixPath(".project/next/tasks")
830
+ paths = [
831
+ value
832
+ for value in pipeline_state._run_git(
833
+ repo,
834
+ "ls-tree",
835
+ "-r",
836
+ "--name-only",
837
+ "-z",
838
+ checkpoint,
839
+ "--",
840
+ prefix.as_posix(),
841
+ ).stdout.split("\0")
842
+ if value
843
+ ]
844
+ contracts: dict[str, ApprovedTaskContract] = {}
845
+ for value in paths:
846
+ relative = PurePosixPath(value)
847
+ match = TASK_FILE_RE.fullmatch(relative.name)
848
+ if relative.parent != prefix or match is None:
849
+ raise PipelineStateError(
850
+ f"approval checkpoint has noncanonical task artifact: {value}"
851
+ )
852
+ text = _git_text_at(repo, checkpoint, value)
853
+ if text is None:
854
+ raise PipelineStateError(f"approval checkpoint lost task artifact: {value}")
855
+ task_id, declared = _task_files(text, value)
856
+ if task_id != match.group(1):
857
+ raise PipelineStateError(f"approval checkpoint task id does not match {value}")
858
+ if task_id in contracts:
859
+ raise PipelineStateError(f"approval checkpoint repeats task id: {task_id}")
860
+ contracts[task_id] = ApprovedTaskContract(
861
+ value,
862
+ text,
863
+ _safe_declared_paths(task_id, declared),
864
+ )
865
+ if not contracts:
866
+ raise PipelineStateError("approval checkpoint has no task contracts")
867
+ return contracts
868
+
869
+
870
+ def _task_contracts_at(
871
+ repo: Path,
872
+ revision: str,
873
+ prefix: str = ".project/next/tasks",
874
+ ) -> dict[str, tuple[str, str]]:
875
+ root = PurePosixPath(prefix)
876
+ paths = [
877
+ value
878
+ for value in pipeline_state._run_git(
879
+ repo,
880
+ "ls-tree",
881
+ "-r",
882
+ "--name-only",
883
+ "-z",
884
+ revision,
885
+ "--",
886
+ prefix,
887
+ ).stdout.split("\0")
888
+ if value
889
+ ]
890
+ contracts: dict[str, tuple[str, str]] = {}
891
+ for value in paths:
892
+ path = PurePosixPath(value)
893
+ match = TASK_FILE_RE.fullmatch(path.name)
894
+ if path.parent != root or match is None:
895
+ raise PipelineStateError(f"lookahead has noncanonical task artifact: {value}")
896
+ task_id = match.group(1)
897
+ if task_id in contracts:
898
+ raise PipelineStateError(f"lookahead tasks repeat id: {task_id}")
899
+ text = _git_text_at(repo, revision, value)
900
+ if text is None:
901
+ raise PipelineStateError(f"lookahead lost task artifact: {value}")
902
+ contracts[task_id] = (
903
+ value,
904
+ text,
905
+ )
906
+ return contracts
907
+
908
+
909
+ def _classify_plan_drift(
910
+ repo: Path,
911
+ state: PipelineState,
912
+ revision: str,
913
+ landing: Optional[str] = None,
914
+ ) -> dict[str, object]:
915
+ """Compare the approved plan at the landing's checkpoint with ``revision``."""
916
+ if state.phase != "plan" or state.status != "done":
917
+ return {"class": "not-applicable", "reason": "lookahead track is not plan/done"}
918
+ assert state.milestone is not None
919
+ checkpoint = _approval_checkpoint(repo, state, landing or revision)
920
+ if checkpoint is None:
921
+ return {
922
+ "class": "unverifiable",
923
+ "checkpoint": None,
924
+ "task_ids": [],
925
+ "changed_paths": [],
926
+ "reason": "matching plan approval checkpoint was not found",
927
+ }
928
+ changed = [
929
+ item
930
+ for item in pipeline_state._run_git(
931
+ repo,
932
+ "diff",
933
+ "--name-only",
934
+ "-z",
935
+ checkpoint,
936
+ revision,
937
+ ).stdout.split("\0")
938
+ if item
939
+ ]
940
+ approved_plan_path = ".project/next/plan/PLAN.md"
941
+ approved_plan = _git_text_at(repo, checkpoint, approved_plan_path)
942
+ if approved_plan is None:
943
+ return {
944
+ "class": "unverifiable",
945
+ "checkpoint": checkpoint,
946
+ "task_ids": [],
947
+ "changed_paths": [],
948
+ "contract_paths": [],
949
+ "reason": "plan approval checkpoint has no PLAN.md",
950
+ }
951
+ approved = _approved_task_contracts(repo, checkpoint)
952
+ current = _task_contracts_at(repo, revision)
953
+
954
+ contract_paths: set[str] = set()
955
+ flagged_ids: set[str] = set()
956
+ current_plan = _git_text_at(repo, revision, approved_plan_path)
957
+ if current_plan != approved_plan:
958
+ contract_paths.add(approved_plan_path)
959
+ for task_id in set(approved) | set(current):
960
+ approved_contract = approved.get(task_id)
961
+ current_contract = current.get(task_id)
962
+ if (
963
+ approved_contract is None
964
+ or current_contract is None
965
+ or approved_contract.path != current_contract[0]
966
+ or approved_contract.text != current_contract[1]
967
+ ):
968
+ flagged_ids.add(task_id)
969
+ if approved_contract is not None:
970
+ contract_paths.add(approved_contract.path)
971
+ if current_contract is not None:
972
+ contract_paths.add(current_contract[0])
973
+
974
+ product_paths: set[str] = set()
975
+ for task_id, contract in approved.items():
976
+ matches = sorted(
977
+ changed_path
978
+ for changed_path in changed
979
+ if any(
980
+ changed_path == path
981
+ or changed_path.startswith(f"{path.rstrip('/')}/")
982
+ for path in contract.files
983
+ )
984
+ )
985
+ if matches:
986
+ flagged_ids.add(task_id)
987
+ product_paths.update(matches)
988
+
989
+ changed_paths = sorted(contract_paths | product_paths)
990
+ changed_contract = bool(contract_paths)
991
+ changed_product = bool(product_paths)
992
+ reasons = []
993
+ if changed_contract:
994
+ reasons.append("plan or task contracts changed after approval")
995
+ if changed_product:
996
+ reasons.append("approved declared task paths changed after approval")
997
+ return {
998
+ "class": "changed" if changed_paths else "clean",
999
+ "checkpoint": checkpoint,
1000
+ "task_ids": sorted(flagged_ids),
1001
+ "changed_paths": changed_paths,
1002
+ "contract_paths": sorted(contract_paths),
1003
+ "reason": "; ".join(reasons) if reasons else "approval contracts and declared paths are unchanged",
1004
+ }
1005
+
1006
+
1007
+ # pipeline_state imports this module, so the parent is resolved after the
1008
+ # definitions above. Parent functions are looked up on the module at call time
1009
+ # so patches applied to pipeline_state stay visible here.
1010
+ if __package__: # imported as scripts.state_checkpoint
1011
+ from . import pipeline_state
1012
+ from .pipeline_state import (
1013
+ CHECKPOINT_JOURNAL_NAME,
1014
+ CHECKPOINT_KINDS,
1015
+ CHECKPOINT_SCHEMA,
1016
+ PLAN_APPROVAL_SUBJECT,
1017
+ PROMOTION_TRACKS,
1018
+ SLUG_RE,
1019
+ TASK_FILE_RE,
1020
+ TASK_ID_RE,
1021
+ ApprovedTaskContract,
1022
+ PipelineState,
1023
+ PipelineStateError,
1024
+ )
1025
+ else: # standalone script or sibling import
1026
+ import pipeline_state
1027
+ from pipeline_state import (
1028
+ CHECKPOINT_JOURNAL_NAME,
1029
+ CHECKPOINT_KINDS,
1030
+ CHECKPOINT_SCHEMA,
1031
+ PLAN_APPROVAL_SUBJECT,
1032
+ PROMOTION_TRACKS,
1033
+ SLUG_RE,
1034
+ TASK_FILE_RE,
1035
+ TASK_ID_RE,
1036
+ ApprovedTaskContract,
1037
+ PipelineState,
1038
+ PipelineStateError,
1039
+ )