@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,743 @@
1
+ #!/usr/bin/env python3
2
+ # gsd-path project runtime
3
+ """Promote the lookahead track as a resumable transaction for pipeline_state."""
4
+
5
+ from __future__ import annotations
6
+
7
+ import json
8
+ import re
9
+ import shutil
10
+ from pathlib import Path, PurePosixPath
11
+ from typing import Mapping, Optional
12
+
13
+
14
+ def _render_roadmap(
15
+ text: str,
16
+ previous_milestone: str,
17
+ milestone: str,
18
+ branch: str,
19
+ integrate: str,
20
+ ) -> str:
21
+ sections = pipeline_state._roadmap_sections(text)
22
+ if previous_milestone not in sections:
23
+ raise PipelineStateError(f"ROADMAP.md is missing previous milestone: {previous_milestone}")
24
+ if milestone not in sections:
25
+ raise PipelineStateError(f"ROADMAP.md is missing promoted milestone: {milestone}")
26
+ branch_number = pipeline_state._bound_branch_number(branch)
27
+ if branch_number is None:
28
+ raise PipelineStateError(f"invalid promoted branch: {branch}")
29
+ promoted_id, new_start, new_end = sections[milestone]
30
+ if branch_number != int(promoted_id.removeprefix("M")):
31
+ raise PipelineStateError(f"branch {branch} does not match roadmap milestone {promoted_id}")
32
+ _, old_start, old_end = sections[previous_milestone]
33
+ lines = text.splitlines(keepends=True)
34
+ pipeline_state._replace_roadmap_field(lines, old_start, old_end, "Status", {"shipped"}, "shipped")
35
+ pipeline_state._replace_roadmap_field(lines, old_start, old_end, "Integrated", {NULL}, integrate)
36
+ pipeline_state._replace_roadmap_field(lines, new_start, new_end, "Status", {"pending"}, "active")
37
+ return "".join(lines)
38
+
39
+
40
+ def _promotion_event(
41
+ drift: Mapping[str, object],
42
+ branch: str,
43
+ landing: str,
44
+ base: Optional[str] = None,
45
+ ) -> str:
46
+ prefix = f"lookahead promoted on {branch} from integrate {landing}"
47
+ if base is not None and base != landing:
48
+ prefix = f"{prefix} at main base {base}"
49
+ drift_class = drift["class"]
50
+ if drift_class == "changed":
51
+ task_ids = drift.get("task_ids")
52
+ if isinstance(task_ids, list) and task_ids:
53
+ return f"{prefix}; plan drift flagged tasks {', '.join(task_ids)}"
54
+ return f"{prefix}; plan contract changed after approval"
55
+ if drift_class == "unverifiable":
56
+ return f"{prefix}; plan drift unverifiable because approval checkpoint is missing"
57
+ return prefix
58
+
59
+
60
+ def _promotion_state(
61
+ active_text: str,
62
+ next_state: PipelineState,
63
+ branch: str,
64
+ landing: str,
65
+ drift: Mapping[str, object],
66
+ event_date: Optional[str] = None,
67
+ base: Optional[str] = None,
68
+ ) -> str:
69
+ status = next_state.status
70
+ if next_state.phase == "plan" and next_state.status == "done" and drift["class"] != "clean":
71
+ status = "active"
72
+ rendered = pipeline_state._set_frontmatter(
73
+ active_text,
74
+ {
75
+ "milestone": next_state.milestone or NULL,
76
+ "phase": next_state.phase,
77
+ "status": status,
78
+ "branch": branch,
79
+ "archive": NULL,
80
+ "integration_default": next_state.integration_default,
81
+ "integration": next_state.integration,
82
+ "integration_source": next_state.integration_source,
83
+ },
84
+ )
85
+ rendered = pipeline_state._append_event(
86
+ rendered,
87
+ next_state.phase,
88
+ _promotion_event(drift, branch, landing, base),
89
+ event_date,
90
+ )
91
+ pipeline_state._state_from_text(rendered)
92
+ return rendered
93
+
94
+
95
+ def _head_with_exact_message(
96
+ repo: Path,
97
+ subject: str,
98
+ body_fields: Mapping[str, str],
99
+ ) -> Optional[str]:
100
+ head = pipeline_state._run_git(repo, "rev-parse", "HEAD").stdout.strip()
101
+ raw = pipeline_state._run_git(repo, "cat-file", "commit", head).stdout
102
+ try:
103
+ message = raw.split("\n\n", 1)[1]
104
+ except IndexError as error:
105
+ raise PipelineStateError(f"commit {head} has no message body") from error
106
+ return head if message == pipeline_state._commit_message(subject, body_fields) else None
107
+
108
+
109
+ def _tree_entries(
110
+ repo: Path,
111
+ revision: str,
112
+ prefix: str,
113
+ ) -> dict[str, tuple[str, str]]:
114
+ records = pipeline_state._run_git(
115
+ repo,
116
+ "ls-tree",
117
+ "-r",
118
+ "-z",
119
+ revision,
120
+ "--",
121
+ prefix,
122
+ ).stdout.split("\0")
123
+ entries: dict[str, tuple[str, str]] = {}
124
+ for record in records:
125
+ if not record:
126
+ continue
127
+ metadata, separator, path = record.partition("\t")
128
+ fields = metadata.split()
129
+ if not separator or len(fields) != 3:
130
+ raise PipelineStateError(f"could not parse Git tree entry: {record}")
131
+ mode, object_type, object_id = fields
132
+ if object_type != "blob" or mode not in {"100644", "100755"}:
133
+ raise PipelineStateError(f"promotion requires a regular tracked file: {path}")
134
+ entries[path] = (mode, object_id)
135
+ return entries
136
+
137
+
138
+ def _promotion_tree_mapping(
139
+ repo: Path,
140
+ parent: str,
141
+ head: str,
142
+ ) -> set[str]:
143
+ next_prefix = ".project/next"
144
+ next_entries = _tree_entries(repo, parent, next_prefix)
145
+ state_path = f"{next_prefix}/STATE.md"
146
+ if state_path not in next_entries:
147
+ raise PipelineStateError("promotion parent has no lookahead STATE.md")
148
+
149
+ destinations: dict[str, tuple[str, str]] = {}
150
+ used_tracks: set[str] = set()
151
+ for source, entry in next_entries.items():
152
+ if source == state_path:
153
+ continue
154
+ parts = PurePosixPath(source).parts
155
+ if len(parts) < 4 or parts[:2] != (".project", "next"):
156
+ raise PipelineStateError(f"promotion parent has invalid lookahead path: {source}")
157
+ track = parts[2]
158
+ if track not in PROMOTION_TRACKS:
159
+ raise PipelineStateError(f"promotion parent has unowned lookahead path: {source}")
160
+ destination = PurePosixPath(".project", *parts[2:]).as_posix()
161
+ destinations[destination] = entry
162
+ used_tracks.add(track)
163
+ if not destinations:
164
+ raise PipelineStateError("promotion parent has no promotable artifacts")
165
+
166
+ if _tree_entries(repo, head, next_prefix):
167
+ raise PipelineStateError("completed promotion retained tracked lookahead artifacts")
168
+ for track in used_tracks:
169
+ prefix = f".project/{track}"
170
+ if _tree_entries(repo, parent, prefix):
171
+ raise PipelineStateError(f"promotion parent already had destination track: {track}")
172
+ actual = _tree_entries(repo, head, prefix)
173
+ expected = {
174
+ path: entry
175
+ for path, entry in destinations.items()
176
+ if path == prefix or path.startswith(f"{prefix}/")
177
+ }
178
+ if actual != expected:
179
+ raise PipelineStateError(f"completed promotion did not preserve track: {track}")
180
+
181
+ changed = {
182
+ path
183
+ for path in pipeline_state._run_git(
184
+ repo,
185
+ "diff",
186
+ "--name-only",
187
+ "--no-renames",
188
+ "-z",
189
+ parent,
190
+ head,
191
+ ).stdout.split("\0")
192
+ if path
193
+ }
194
+ expected_changes = {
195
+ ".project/STATE.md",
196
+ ".project/ROADMAP.md",
197
+ *next_entries,
198
+ *destinations,
199
+ }
200
+ if changed != expected_changes:
201
+ missing = sorted(expected_changes - changed)
202
+ extra = sorted(changed - expected_changes)
203
+ raise PipelineStateError(
204
+ "completed promotion has wrong path set: "
205
+ f"missing={missing}, extra={extra}"
206
+ )
207
+ return expected_changes
208
+
209
+
210
+ def _completed_promotion(
211
+ repo: Path,
212
+ project: Path,
213
+ milestone: str,
214
+ branch: str,
215
+ base: str,
216
+ landing: str,
217
+ ) -> Optional[dict[str, object]]:
218
+ next_root = project / "next"
219
+ if next_root.exists() or next_root.is_symlink():
220
+ return None
221
+ state, _, _ = pipeline_state.load_state(repo)
222
+ if state.milestone != milestone or state.branch != branch or state.archive is not None:
223
+ return None
224
+ if pipeline_state._current_branch(repo) != branch:
225
+ raise PipelineStateError("completed promotion is not on its recorded branch")
226
+ if pipeline_state._worktree_changes(repo):
227
+ raise PipelineStateError("completed promotion worktree is not clean")
228
+
229
+ head = pipeline_state._run_git(repo, "rev-parse", "HEAD").stdout.strip()
230
+ parents = pipeline_state._run_git(repo, "show", "-s", "--format=%P", head).stdout.split()
231
+ if parents != [base]:
232
+ raise PipelineStateError(
233
+ "completed promotion must be current HEAD with base as its single parent"
234
+ )
235
+ subject = f"router: promote lookahead milestone {milestone}"
236
+ fields = {
237
+ "Why": "promote lookahead track",
238
+ "Milestone": milestone,
239
+ "Integrate": landing,
240
+ }
241
+ if base != landing:
242
+ fields["Base"] = base
243
+ if _head_with_exact_message(repo, subject, fields) != head:
244
+ raise PipelineStateError("completed promotion has the wrong commit message")
245
+
246
+ active_text = pipeline_state._git_text_at(repo, base, ".project/STATE.md")
247
+ next_text = pipeline_state._git_text_at(repo, base, ".project/next/STATE.md")
248
+ roadmap_text = pipeline_state._git_text_at(repo, base, ".project/ROADMAP.md")
249
+ if active_text is None or next_text is None or roadmap_text is None:
250
+ raise PipelineStateError("promotion parent is missing required metadata")
251
+ active_state = pipeline_state._state_from_text(active_text, "promotion parent STATE.md")
252
+ next_state = pipeline_state._state_from_text(next_text, "promotion parent next/STATE.md")
253
+ pipeline_state._validate_state_context(next_state, ".project/next", "promotion parent next/STATE.md")
254
+ if active_state.phase != "shipped" or active_state.status != "done":
255
+ raise PipelineStateError("promotion parent STATE is not shipped/done")
256
+ if active_state.milestone is None:
257
+ raise PipelineStateError("promotion parent STATE has no milestone")
258
+ if next_state.project != active_state.project:
259
+ raise PipelineStateError("promotion parent lookahead belongs to another project")
260
+ if next_state.milestone != milestone:
261
+ raise PipelineStateError("promotion parent lookahead milestone does not match request")
262
+ if next_state.phase not in {"define", "research", "decide", "plan"}:
263
+ raise PipelineStateError(
264
+ f"promotion parent lookahead cannot be in phase {next_state.phase}"
265
+ )
266
+
267
+ _promotion_tree_mapping(repo, base, head)
268
+ drift = pipeline_state._classify_plan_drift(repo, next_state, base, landing)
269
+ expected_roadmap = _render_roadmap(
270
+ roadmap_text,
271
+ active_state.milestone,
272
+ milestone,
273
+ branch,
274
+ landing,
275
+ )
276
+ current_roadmap = pipeline_state._git_text_at(repo, head, ".project/ROADMAP.md")
277
+ if current_roadmap != expected_roadmap:
278
+ raise PipelineStateError("completed promotion has invalid ROADMAP.md")
279
+
280
+ current_state = pipeline_state._git_text_at(repo, head, ".project/STATE.md")
281
+ if current_state is None:
282
+ raise PipelineStateError("completed promotion has no STATE.md")
283
+ event = re.escape(_promotion_event(drift, branch, landing, base))
284
+ suffix = re.search(
285
+ rf"(?m)^- (\d{{4}}-\d{{2}}-\d{{2}}) — {re.escape(next_state.phase)} — {event}\n\Z",
286
+ current_state,
287
+ )
288
+ if suffix is None:
289
+ raise PipelineStateError("completed promotion has invalid STATE log event")
290
+ expected_state = _promotion_state(
291
+ active_text,
292
+ next_state,
293
+ branch,
294
+ landing,
295
+ drift,
296
+ suffix.group(1),
297
+ base,
298
+ )
299
+ if current_state != expected_state:
300
+ raise PipelineStateError("completed promotion has invalid STATE.md")
301
+ return {
302
+ "schema": PROMOTION_SCHEMA,
303
+ "status": "already-complete",
304
+ "milestone": milestone,
305
+ "branch": branch,
306
+ "integrate": landing,
307
+ "landing": landing,
308
+ "base": base,
309
+ "commit": head,
310
+ "drift": drift,
311
+ }
312
+
313
+
314
+ def _prepare_promotion(
315
+ repo: Path,
316
+ project: Path,
317
+ milestone: str,
318
+ branch: str,
319
+ base: str,
320
+ landing: str,
321
+ ) -> dict[str, object]:
322
+ if pipeline_state._bound_branch_number(branch) is None:
323
+ raise PipelineStateError(f"invalid promoted branch: {branch}")
324
+ current = pipeline_state._current_branch(repo)
325
+ if current != branch:
326
+ raise PipelineStateError(
327
+ f"current branch {current or '<detached>'} != promoted branch {branch}"
328
+ )
329
+ resolved_base = pipeline_state._run_git(
330
+ repo,
331
+ "rev-parse",
332
+ "--verify",
333
+ f"{base}^{{commit}}",
334
+ ).stdout.strip()
335
+ if base != resolved_base:
336
+ raise PipelineStateError("base must be an exact full commit SHA")
337
+ resolved_landing = pipeline_state._run_git(
338
+ repo,
339
+ "rev-parse",
340
+ "--verify",
341
+ f"{landing}^{{commit}}",
342
+ ).stdout.strip()
343
+ if landing != resolved_landing:
344
+ raise PipelineStateError("landing must be an exact full commit SHA")
345
+ head = pipeline_state._run_git(repo, "rev-parse", "HEAD").stdout.strip()
346
+ if head != base:
347
+ raise PipelineStateError(f"promotion must start at base SHA: {head} != {base}")
348
+ remote = pipeline_state._run_git(repo, "rev-parse", "--verify", "origin/main^{commit}").stdout.strip()
349
+ if remote != base:
350
+ raise PipelineStateError(
351
+ f"base SHA is not current origin/main: {base} != {remote}"
352
+ )
353
+ if not pipeline_state._is_ancestor(repo, landing, base):
354
+ raise PipelineStateError("landing is not an ancestor of the current main base")
355
+ if pipeline_state._run_git(repo, "status", "--porcelain", "--untracked-files=all").stdout:
356
+ raise PipelineStateError("promotion must start from a clean worktree")
357
+
358
+ active_state, active_text, _ = pipeline_state.load_state(repo)
359
+ if active_state.phase != "shipped" or active_state.status != "done":
360
+ raise PipelineStateError("promotion requires active STATE shipped/done")
361
+ if active_state.milestone is None:
362
+ raise PipelineStateError("active STATE does not name the shipped milestone")
363
+ archive_name = PurePosixPath(active_state.archive or "").name
364
+ tag_ref = f"refs/tags/milestone/{archive_name}"
365
+ published_tag_ref = f"refs/remotes/origin/tags/milestone/{archive_name}"
366
+ tag_type = pipeline_state._run_git(repo, "cat-file", "-t", tag_ref, check=False)
367
+ if tag_type.returncode != 0 or tag_type.stdout.strip() != "tag":
368
+ raise PipelineStateError("shipped milestone tag is missing or not annotated")
369
+ published_tag_type = pipeline_state._run_git(
370
+ repo,
371
+ "cat-file",
372
+ "-t",
373
+ published_tag_ref,
374
+ check=False,
375
+ )
376
+ if published_tag_type.returncode != 0 or published_tag_type.stdout.strip() != "tag":
377
+ raise PipelineStateError("published milestone tag is missing or not annotated")
378
+ tag_object = pipeline_state._run_git(repo, "rev-parse", "--verify", tag_ref).stdout.strip()
379
+ published_tag_object = pipeline_state._run_git(
380
+ repo,
381
+ "rev-parse",
382
+ "--verify",
383
+ published_tag_ref,
384
+ ).stdout.strip()
385
+ if tag_object != published_tag_object:
386
+ raise PipelineStateError("local milestone tag does not match published milestone tag")
387
+ tag_landing = pipeline_state._run_git(
388
+ repo,
389
+ "rev-parse",
390
+ "--verify",
391
+ f"{tag_ref}^{{commit}}",
392
+ ).stdout.strip()
393
+ if tag_landing != landing:
394
+ raise PipelineStateError("milestone tag does not point at landing")
395
+ published_tag_landing = pipeline_state._run_git(
396
+ repo,
397
+ "rev-parse",
398
+ "--verify",
399
+ f"{published_tag_ref}^{{commit}}",
400
+ ).stdout.strip()
401
+ if published_tag_landing != landing:
402
+ raise PipelineStateError("published milestone tag does not point at landing")
403
+ if active_state.integration == "pull-request":
404
+ remote_tag_ref = f"refs/tags/milestone/{archive_name}"
405
+ live_tag_object, live_tag_landing = pipeline_state._live_annotated_tag(
406
+ repo,
407
+ remote_tag_ref,
408
+ )
409
+ if live_tag_object != published_tag_object:
410
+ raise PipelineStateError(
411
+ "published milestone tag does not match the live origin tag"
412
+ )
413
+ if live_tag_landing != landing:
414
+ raise PipelineStateError(
415
+ "live origin milestone tag does not point at landing"
416
+ )
417
+ next_root = project / "next"
418
+ next_state, next_text, _ = pipeline_state.load_state(repo, ".project/next")
419
+ if next_state.project != active_state.project:
420
+ raise PipelineStateError(
421
+ "lookahead STATE project does not match active STATE project"
422
+ )
423
+ if next_state.integration_default != active_state.integration_default:
424
+ raise PipelineStateError(
425
+ "lookahead integration_default does not match the active project"
426
+ )
427
+ if next_state.milestone != milestone:
428
+ raise PipelineStateError(
429
+ f"requested milestone {milestone} != next STATE milestone {next_state.milestone}"
430
+ )
431
+ if next_state.branch is not None or next_state.archive is not None:
432
+ raise PipelineStateError("lookahead STATE branch and archive must be null")
433
+
434
+ tracks: dict[str, str] = {}
435
+ for name in PROMOTION_TRACKS:
436
+ source = next_root / name
437
+ destination = project / name
438
+ if source.exists() or source.is_symlink():
439
+ if destination.exists() or destination.is_symlink():
440
+ raise PipelineStateError(f"promotion destination already exists: {destination}")
441
+ tracks[name] = pipeline_state._tree_digest(source)
442
+ if not tracks:
443
+ raise PipelineStateError("lookahead track has no promotable artifacts")
444
+ drift = pipeline_state._classify_plan_drift(repo, next_state, base, landing)
445
+ roadmap_path = project / "ROADMAP.md"
446
+ roadmap_text = pipeline_state._read_real_file(roadmap_path, "ROADMAP.md")
447
+ target_state = _promotion_state(
448
+ active_text, next_state, branch, landing, drift, base=base
449
+ )
450
+ target_roadmap = _render_roadmap(
451
+ roadmap_text,
452
+ active_state.milestone,
453
+ milestone,
454
+ branch,
455
+ landing,
456
+ )
457
+ return {
458
+ "schema": PROMOTION_SCHEMA,
459
+ "repo": str(repo),
460
+ "milestone": milestone,
461
+ "previous_milestone": active_state.milestone,
462
+ "branch": branch,
463
+ "integrate": landing,
464
+ "landing": landing,
465
+ "base": base,
466
+ "tracks": tracks,
467
+ "residual_sha256": pipeline_state._tree_digest(next_root, tuple(tracks)),
468
+ "next_state_sha256": pipeline_state._sha256(next_text),
469
+ "active_state_sha256": pipeline_state._sha256(active_text),
470
+ "roadmap_sha256": pipeline_state._sha256(roadmap_text),
471
+ "target_state": target_state,
472
+ "target_roadmap": target_roadmap,
473
+ "drift": drift,
474
+ "stage": "prepared",
475
+ }
476
+
477
+
478
+ def _require_journal_request(
479
+ journal: Mapping[str, object],
480
+ repo: Path,
481
+ milestone: str,
482
+ branch: str,
483
+ base: str,
484
+ landing: str,
485
+ ) -> None:
486
+ expected = {
487
+ "schema": PROMOTION_SCHEMA,
488
+ "repo": str(repo),
489
+ "milestone": milestone,
490
+ "branch": branch,
491
+ "integrate": landing,
492
+ }
493
+ mismatches = {
494
+ key: {"expected": value, "actual": journal.get(key)}
495
+ for key, value in expected.items()
496
+ if journal.get(key) != value
497
+ }
498
+ if mismatches:
499
+ raise PipelineStateError(
500
+ f"promotion journal does not match request: {json.dumps(mismatches, sort_keys=True)}"
501
+ )
502
+ journal_base = journal.get("base", journal.get("integrate"))
503
+ if journal_base != base:
504
+ raise PipelineStateError("promotion journal does not match the requested base")
505
+
506
+
507
+ def _resume_track_moves(project: Path, journal: dict[str, object], path: Path) -> None:
508
+ tracks = journal.get("tracks")
509
+ if not isinstance(tracks, dict):
510
+ raise PipelineStateError("promotion journal has invalid tracks")
511
+ next_root = project / "next"
512
+ for name, expected_digest in tracks.items():
513
+ if name not in PROMOTION_TRACKS or not isinstance(expected_digest, str):
514
+ raise PipelineStateError("promotion journal has invalid track entry")
515
+ source = next_root / name
516
+ destination = project / name
517
+ source_exists = source.exists() or source.is_symlink()
518
+ destination_exists = destination.exists() or destination.is_symlink()
519
+ if source_exists and destination_exists:
520
+ raise PipelineStateError(f"promotion has both source and destination: {name}")
521
+ if not source_exists and not destination_exists:
522
+ raise PipelineStateError(f"promotion lost both source and destination: {name}")
523
+ candidate = source if source_exists else destination
524
+ if pipeline_state._tree_digest(candidate) != expected_digest:
525
+ raise PipelineStateError(f"promotion track drifted: {name}")
526
+ if source_exists:
527
+ source.rename(destination)
528
+ journal["stage"] = f"moved:{name}"
529
+ pipeline_state._write_json(path, journal)
530
+
531
+
532
+ def _resume_metadata(project: Path, journal: dict[str, object], path: Path) -> None:
533
+ targets = (
534
+ (project / "STATE.md", "active_state_sha256", "target_state"),
535
+ (project / "ROADMAP.md", "roadmap_sha256", "target_roadmap"),
536
+ )
537
+ for target, original_key, target_key in targets:
538
+ current = pipeline_state._read_real_file(target, target.name)
539
+ expected_original = journal.get(original_key)
540
+ desired = journal.get(target_key)
541
+ if not isinstance(expected_original, str) or not isinstance(desired, str):
542
+ raise PipelineStateError("promotion journal has invalid metadata")
543
+ if current == desired:
544
+ continue
545
+ if pipeline_state._sha256(current) != expected_original:
546
+ raise PipelineStateError(f"promotion metadata drifted: {target}")
547
+ pipeline_state._atomic_write(target, desired)
548
+ journal["stage"] = f"updated:{target.name}"
549
+ pipeline_state._write_json(path, journal)
550
+
551
+
552
+ def _resume_next_removal(
553
+ project: Path,
554
+ journal: dict[str, object],
555
+ journal_path: Path,
556
+ residual: Path,
557
+ ) -> None:
558
+ next_root = project / "next"
559
+ if next_root.exists() or next_root.is_symlink():
560
+ if residual.exists() or residual.is_symlink():
561
+ raise PipelineStateError("promotion has both next track and residual staging")
562
+ next_text = pipeline_state._read_real_file(next_root / "STATE.md", "next/STATE.md")
563
+ if pipeline_state._sha256(next_text) != journal.get("next_state_sha256"):
564
+ raise PipelineStateError("lookahead STATE drifted during promotion")
565
+ if pipeline_state._tree_digest(next_root) != journal.get("residual_sha256"):
566
+ raise PipelineStateError("lookahead residual drifted during promotion")
567
+ next_root.rename(residual)
568
+ journal["stage"] = "next-staged"
569
+ pipeline_state._write_json(journal_path, journal)
570
+ elif not residual.exists():
571
+ # Cleanup removes the residual only after recording the commit.
572
+ if journal.get("stage") == "committed" and isinstance(journal.get("commit"), str):
573
+ return
574
+ raise PipelineStateError("promotion lost next track and residual staging")
575
+ if residual.is_symlink() or not residual.is_dir():
576
+ raise PipelineStateError(f"promotion residual must be a real directory: {residual}")
577
+ if pipeline_state._tree_digest(residual) != journal.get("residual_sha256"):
578
+ raise PipelineStateError("staged lookahead residual drifted during promotion")
579
+
580
+
581
+ def _commit_promotion(
582
+ repo: Path,
583
+ milestone: str,
584
+ landing: str,
585
+ base: str,
586
+ journal: dict[str, object],
587
+ journal_path: Path,
588
+ residual: Path,
589
+ ) -> str:
590
+ subject = f"router: promote lookahead milestone {milestone}"
591
+ fields = {
592
+ "Why": "promote lookahead track",
593
+ "Milestone": milestone,
594
+ "Integrate": landing,
595
+ }
596
+ if base != landing:
597
+ fields["Base"] = base
598
+ existing = _head_with_exact_message(repo, subject, fields)
599
+ residual_name = residual.relative_to(repo).as_posix()
600
+
601
+ def is_residual(path: str) -> bool:
602
+ return path == residual_name or path.startswith(f"{residual_name}/")
603
+
604
+ if existing is not None:
605
+ if pipeline_state._run_git(repo, "rev-parse", "HEAD").stdout.strip() != existing:
606
+ raise PipelineStateError("promotion commit exists but is not current HEAD")
607
+ if [path for path in pipeline_state._worktree_changes(repo) if not is_residual(path)]:
608
+ raise PipelineStateError("promotion commit exists with worktree drift")
609
+ return existing
610
+ outside = [
611
+ path
612
+ for path in pipeline_state._worktree_changes(repo)
613
+ if not path.startswith(".project/") and not is_residual(path)
614
+ ]
615
+ if outside:
616
+ raise PipelineStateError(f"promotion found changes outside .project: {outside}")
617
+ pipeline_state._run_git(repo, "add", "-A", "--", ".project")
618
+ staged = [
619
+ path
620
+ for path in pipeline_state._run_git(
621
+ repo,
622
+ "diff",
623
+ "--cached",
624
+ "--name-only",
625
+ "-z",
626
+ ).stdout.split("\0")
627
+ if path
628
+ ]
629
+ if not staged or any(not name.startswith(".project/") for name in staged):
630
+ raise PipelineStateError("promotion commit must contain only .project changes")
631
+ body = "\n".join(f"{key}: {value}" for key, value in fields.items())
632
+ pipeline_state._run_git(repo, "commit", "-m", subject, "-m", body)
633
+ commit = pipeline_state._run_git(repo, "rev-parse", "HEAD").stdout.strip()
634
+ journal["stage"] = "committed"
635
+ journal["commit"] = commit
636
+ pipeline_state._write_json(journal_path, journal)
637
+ return commit
638
+
639
+
640
+ def promote_next(
641
+ repo: Path,
642
+ milestone: str,
643
+ branch: str,
644
+ base: str,
645
+ landing: Optional[str] = None,
646
+ ) -> dict[str, object]:
647
+ """Promote `.project/next` through a journaled, idempotent transaction."""
648
+ resolved = pipeline_state._repo_root(repo)
649
+ if not SLUG_RE.fullmatch(milestone):
650
+ raise PipelineStateError(f"invalid promoted milestone: {milestone}")
651
+ if pipeline_state._bound_branch_number(branch) is None:
652
+ raise PipelineStateError(f"invalid promoted branch: {branch}")
653
+ landing = landing or base
654
+ resolved_base = pipeline_state._run_git(
655
+ resolved,
656
+ "rev-parse",
657
+ "--verify",
658
+ f"{base}^{{commit}}",
659
+ ).stdout.strip()
660
+ if base != resolved_base:
661
+ raise PipelineStateError("base must be an exact full commit SHA")
662
+ project = pipeline_state._track_root(resolved, ".project")
663
+ journal_path = pipeline_state._git_path(resolved, "gsd-path-promote-next.json")
664
+ residual = resolved / PROMOTION_RESIDUAL
665
+ with pipeline_state._state_lock(project):
666
+ if not journal_path.exists():
667
+ completed = _completed_promotion(
668
+ resolved,
669
+ project,
670
+ milestone,
671
+ branch,
672
+ base,
673
+ landing,
674
+ )
675
+ if completed is not None:
676
+ return completed
677
+ if residual.exists() or residual.is_symlink():
678
+ raise PipelineStateError(f"orphaned promotion residual exists: {residual}")
679
+ journal = _prepare_promotion(
680
+ resolved,
681
+ project,
682
+ milestone,
683
+ branch,
684
+ base,
685
+ landing,
686
+ )
687
+ pipeline_state._write_json(journal_path, journal)
688
+ else:
689
+ journal = pipeline_state._read_json(journal_path)
690
+ _require_journal_request(journal, resolved, milestone, branch, base, landing)
691
+ _resume_track_moves(project, journal, journal_path)
692
+ _resume_metadata(project, journal, journal_path)
693
+ _resume_next_removal(project, journal, journal_path, residual)
694
+ commit = _commit_promotion(
695
+ resolved,
696
+ milestone,
697
+ landing,
698
+ base,
699
+ journal,
700
+ journal_path,
701
+ residual,
702
+ )
703
+ if residual.exists():
704
+ shutil.rmtree(residual)
705
+ journal_path.unlink()
706
+ return {
707
+ "schema": PROMOTION_SCHEMA,
708
+ "status": "promoted",
709
+ "milestone": milestone,
710
+ "branch": branch,
711
+ "integrate": landing,
712
+ "landing": landing,
713
+ "base": base,
714
+ "commit": commit,
715
+ "drift": journal["drift"],
716
+ }
717
+
718
+
719
+ # pipeline_state imports this module, so the parent is resolved after the
720
+ # definitions above. Parent functions are looked up on the module at call time
721
+ # so patches applied to pipeline_state stay visible here.
722
+ if __package__: # imported as scripts.state_promote
723
+ from . import pipeline_state
724
+ from .pipeline_state import (
725
+ NULL,
726
+ PROMOTION_RESIDUAL,
727
+ PROMOTION_SCHEMA,
728
+ PROMOTION_TRACKS,
729
+ SLUG_RE,
730
+ PipelineState,
731
+ PipelineStateError,
732
+ )
733
+ else: # standalone script or sibling import
734
+ import pipeline_state
735
+ from pipeline_state import (
736
+ NULL,
737
+ PROMOTION_RESIDUAL,
738
+ PROMOTION_SCHEMA,
739
+ PROMOTION_TRACKS,
740
+ SLUG_RE,
741
+ PipelineState,
742
+ PipelineStateError,
743
+ )