@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,940 @@
1
+ #!/usr/bin/env python3
2
+ # gsd-path project runtime
3
+ """Deterministic GSD Path branch names and pipeline commit messages.
4
+
5
+ Bound work lives on `gsd-path/M00N` for that milestone. The required remote
6
+ default `main` stays a separate trunk. Ship merges the bound branch onto main;
7
+ the next milestone binds a new unused `gsd-path/M00N` and retires the
8
+ integrated previous branch, locally and on origin.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import argparse
14
+ import json
15
+ import os
16
+ import re
17
+ import stat
18
+ import subprocess
19
+ import sys
20
+ from pathlib import Path
21
+ from typing import Optional, Sequence
22
+
23
+ try:
24
+ import _common
25
+ except ImportError: # pragma: no cover - package import used by tests
26
+ from scripts import _common
27
+
28
+
29
+ ARCHIVE_NAME_RE = re.compile(r"^(\d{3,})-([a-z0-9][a-z0-9-]*)$")
30
+ BOUND_BRANCH_RE = _common.BOUND_BRANCH_RE
31
+ MILESTONE_ID_RE = re.compile(r"^M(\d{3,})$")
32
+
33
+ LEGACY_SHIP_PREFIX = "ship: "
34
+ LEGACY_INTEGRATE_PREFIX = "integrate: "
35
+ LEGACY_BIND_NEXT_JOURNAL_SCHEMA = "gsd-path/bind-next-journal/v1"
36
+ BIND_NEXT_JOURNAL_SCHEMA = "gsd-path/bind-next-journal/v2"
37
+ BIND_NEXT_JOURNAL_DIR = "gsd-path-bind-next"
38
+
39
+
40
+ class PipelineGitError(ValueError):
41
+ """Raised when a milestone id, archive name, or branch cannot be derived."""
42
+
43
+
44
+ def milestone_id(number: int) -> str:
45
+ if number < 1:
46
+ raise PipelineGitError(f"milestone number must be >= 1, got {number}")
47
+ return f"M{number:03d}"
48
+
49
+
50
+ def milestone_number(token: str) -> int:
51
+ bound = BOUND_BRANCH_RE.fullmatch(token)
52
+ if bound:
53
+ number = int(bound.group(1))
54
+ if number < 1:
55
+ raise PipelineGitError(f"milestone number must be >= 1, got {number}")
56
+ return number
57
+ milestone = MILESTONE_ID_RE.fullmatch(token)
58
+ if milestone:
59
+ number = int(milestone.group(1))
60
+ if number < 1:
61
+ raise PipelineGitError(f"milestone number must be >= 1, got {number}")
62
+ return number
63
+ archive = ARCHIVE_NAME_RE.fullmatch(token)
64
+ if archive:
65
+ number = int(archive.group(1))
66
+ if number < 1:
67
+ raise PipelineGitError(f"milestone number must be >= 1, got {number}")
68
+ return number
69
+ raise PipelineGitError(f"not a milestone id, bound branch, or archive name: {token}")
70
+
71
+
72
+ def bound_branch_name(number: int) -> str:
73
+ return f"gsd-path/{milestone_id(number)}"
74
+
75
+
76
+ def is_bound_branch(name: str) -> bool:
77
+ match = BOUND_BRANCH_RE.fullmatch(name)
78
+ return match is not None and int(match.group(1)) >= 1
79
+
80
+
81
+ def archive_slug(archive_name: str) -> str:
82
+ match = ARCHIVE_NAME_RE.fullmatch(archive_name)
83
+ if not match:
84
+ raise PipelineGitError(f"invalid archive name: {archive_name}")
85
+ milestone_number(archive_name)
86
+ return match.group(2)
87
+
88
+
89
+ def ship_subject(archive_name: str) -> str:
90
+ number = milestone_number(archive_name)
91
+ return f"ship: {milestone_id(number)} — {archive_slug(archive_name)}"
92
+
93
+
94
+ def integrate_subject(archive_name: str, default_branch: str) -> str:
95
+ if not default_branch or default_branch.startswith("origin/"):
96
+ raise PipelineGitError(f"default branch is invalid: {default_branch}")
97
+ number = milestone_number(archive_name)
98
+ return (
99
+ f"integrate: {milestone_id(number)} — "
100
+ f"merge {bound_branch_name(number)} into {default_branch}"
101
+ )
102
+
103
+
104
+ def legacy_ship_subject(archive_name: str) -> str:
105
+ return f"{LEGACY_SHIP_PREFIX}{archive_name}"
106
+
107
+
108
+ def legacy_integrate_subject(archive_name: str) -> str:
109
+ return f"{LEGACY_INTEGRATE_PREFIX}{archive_name}"
110
+
111
+
112
+ def is_ship_subject(subject: str, archive_name: str) -> bool:
113
+ return subject in {ship_subject(archive_name), legacy_ship_subject(archive_name)}
114
+
115
+
116
+ def is_integrate_subject(subject: str, archive_name: str, default_branch: str) -> bool:
117
+ return subject in {
118
+ integrate_subject(archive_name, default_branch),
119
+ legacy_integrate_subject(archive_name),
120
+ }
121
+
122
+
123
+ def ship_commit_body(archive_path: str, reviewed_head: str) -> str:
124
+ return f"Archive: {archive_path}\nReviewed-HEAD: {reviewed_head}\n"
125
+
126
+
127
+ def integrate_commit_body(
128
+ archive_path: str,
129
+ ship_commit: str,
130
+ default_branch: str,
131
+ bound_branch: str,
132
+ ) -> str:
133
+ return (
134
+ f"Archive: {archive_path}\n"
135
+ f"Ship: {ship_commit}\n"
136
+ f"Default: {default_branch}\n"
137
+ f"Branch: {bound_branch}\n"
138
+ )
139
+
140
+
141
+ def task_commit_subject(task_id: str, title: str) -> str:
142
+ cleaned = title.strip()
143
+ if not cleaned:
144
+ raise PipelineGitError("title is empty")
145
+ return f"{task_id}: {cleaned}"
146
+
147
+
148
+ def attest_commit_subject(task_id: str, title: str) -> str:
149
+ cleaned = title.strip()
150
+ if not cleaned:
151
+ raise PipelineGitError("title is empty")
152
+ return f"attest: {task_id} — {cleaned}"
153
+
154
+
155
+ def attest_commit_body(
156
+ task_file: str,
157
+ base: str,
158
+ head: str,
159
+ paths: Sequence[str],
160
+ verify_command: str,
161
+ ruling: str,
162
+ *,
163
+ legacy: bool = False,
164
+ ) -> str:
165
+ lines = [f"Task: {task_file}", f"Base: {base}", f"Head: {head}", "Files:"]
166
+ lines.extend(f"- {path}" for path in sorted(paths))
167
+ if legacy:
168
+ lines.append(f"Verify: {' '.join(verify_command.split())}")
169
+ else:
170
+ lines.append("Attestation: gsd-path/attestation/v2")
171
+ lines.append(f"Verify-JSON: {json.dumps(verify_command)}")
172
+ lines.append(f"Ruling: {' '.join(ruling.split())}")
173
+ return "\n".join(lines) + "\n"
174
+
175
+
176
+ def task_commit_body(task_file: str, paths: Sequence[str], base: str) -> str:
177
+ ordered = sorted(paths)
178
+ lines = [f"Task: {task_file}", f"Base: {base}", "Files:"]
179
+ if ordered:
180
+ lines.extend(f"- {path}" for path in ordered)
181
+ else:
182
+ lines.append("- (none)")
183
+ return "\n".join(lines) + "\n"
184
+
185
+
186
+ def default_branch_name(remote_default: str) -> str:
187
+ if remote_default.startswith("origin/"):
188
+ return remote_default.removeprefix("origin/")
189
+ return remote_default
190
+
191
+
192
+ def _run_git(
193
+ repo: Path,
194
+ *args: str,
195
+ check: bool = True,
196
+ ) -> subprocess.CompletedProcess[str]:
197
+ result = subprocess.run(
198
+ ["git", "-C", str(repo), *args],
199
+ capture_output=True,
200
+ text=True,
201
+ )
202
+ if check and result.returncode != 0:
203
+ detail = result.stderr.strip() or result.stdout.strip()
204
+ raise PipelineGitError(f"git {' '.join(args)} failed: {detail}")
205
+ return result
206
+
207
+
208
+ def _ref_exists(repo: Path, ref: str) -> bool:
209
+ result = _run_git(
210
+ repo,
211
+ "show-ref",
212
+ "--verify",
213
+ "--quiet",
214
+ ref,
215
+ check=False,
216
+ )
217
+ if result.returncode not in {0, 1}:
218
+ raise PipelineGitError(f"could not inspect bound branch: {ref}")
219
+ return result.returncode == 0
220
+
221
+
222
+ def _origin_remote_exists(repo: Path) -> bool:
223
+ result = _run_git(repo, "remote", "get-url", "origin", check=False)
224
+ return result.returncode == 0
225
+
226
+
227
+ def _require_worktree_root(repo: Path) -> Path:
228
+ if repo.is_symlink() or not repo.is_dir():
229
+ raise PipelineGitError(f"repo must be a real worktree directory: {repo}")
230
+ resolved = repo.resolve()
231
+ top = _run_git(resolved, "rev-parse", "--show-toplevel").stdout.strip()
232
+ if Path(top).resolve() != resolved:
233
+ raise PipelineGitError(f"repo must be the worktree root: {resolved}")
234
+ return resolved
235
+
236
+
237
+ def _symbolic_branch(repo: Path) -> str:
238
+ result = _run_git(
239
+ repo,
240
+ "symbolic-ref",
241
+ "--quiet",
242
+ "--short",
243
+ "HEAD",
244
+ check=False,
245
+ )
246
+ if result.returncode != 0:
247
+ raise PipelineGitError("primary worktree must be on a named branch")
248
+ return result.stdout.strip()
249
+
250
+
251
+ def _branch_worktrees(repo: Path) -> dict[str, Path]:
252
+ """Return every checked-out local branch from Git's worktree registry."""
253
+ output = _run_git(repo, "worktree", "list", "--porcelain", "-z").stdout
254
+ worktrees: dict[str, Path] = {}
255
+ current_path: Optional[Path] = None
256
+ for field in output.split("\0"):
257
+ if field.startswith("worktree "):
258
+ current_path = Path(field.removeprefix("worktree ")).resolve()
259
+ elif field.startswith("branch ") and current_path is not None:
260
+ ref = field.removeprefix("branch ")
261
+ prefix = "refs/heads/"
262
+ if ref.startswith(prefix):
263
+ worktrees[ref.removeprefix(prefix)] = current_path
264
+ return worktrees
265
+
266
+
267
+ def _remote_ref_sha(repo: Path, ref: str) -> Optional[str]:
268
+ if not _origin_remote_exists(repo):
269
+ raise PipelineGitError("origin remote is not configured")
270
+ result = _run_git(
271
+ repo,
272
+ "ls-remote",
273
+ "--exit-code",
274
+ "--heads",
275
+ "origin",
276
+ ref,
277
+ check=False,
278
+ )
279
+ if result.returncode == 0:
280
+ rows = [line.split("\t", 1) for line in result.stdout.splitlines() if line]
281
+ if len(rows) != 1 or len(rows[0]) != 2 or rows[0][1] != ref:
282
+ raise PipelineGitError(f"origin returned ambiguous ref data for {ref}")
283
+ return rows[0][0]
284
+ if result.returncode == 2:
285
+ return None
286
+ raise PipelineGitError(f"could not inspect {ref} on origin: git ls-remote failed")
287
+
288
+
289
+ def _remote_branch_exists(repo: Path, branch: str) -> bool:
290
+ return _remote_ref_sha(repo, f"refs/heads/{branch}") is not None
291
+
292
+
293
+ def _pull_request_tag_fields(repo: Path, ref: str) -> dict[str, str]:
294
+ tag_type = _run_git(repo, "cat-file", "-t", ref, check=False)
295
+ if tag_type.returncode != 0 or tag_type.stdout.strip() != "tag":
296
+ return {}
297
+ contents = _run_git(repo, "for-each-ref", "--format=%(contents)", ref).stdout
298
+ fields: dict[str, str] = {}
299
+ for line in contents.splitlines():
300
+ match = re.fullmatch(r"(Mode|Pull-Request|Ship|Landing): (.+)", line)
301
+ if match:
302
+ if match.group(1) in fields:
303
+ raise PipelineGitError("pull-request milestone tag repeats metadata")
304
+ fields[match.group(1)] = match.group(2)
305
+ return fields
306
+
307
+
308
+ def _validated_shipped_state(repo: Path) -> dict[str, object]:
309
+ validator = Path(__file__).with_name("pipeline_state.py")
310
+ result = subprocess.run(
311
+ [
312
+ sys.executable,
313
+ str(validator),
314
+ "validate",
315
+ "--repo",
316
+ str(repo),
317
+ ],
318
+ capture_output=True,
319
+ text=True,
320
+ )
321
+ if result.returncode != 0:
322
+ detail = result.stderr.strip() or result.stdout.strip()
323
+ raise PipelineGitError(f"could not validate shipped STATE.md: {detail}")
324
+ try:
325
+ payload = json.loads(result.stdout)
326
+ except json.JSONDecodeError as error:
327
+ raise PipelineGitError("pipeline state validator returned invalid JSON") from error
328
+ state = payload.get("state") if isinstance(payload, dict) else None
329
+ if not isinstance(state, dict):
330
+ raise PipelineGitError("pipeline state validator omitted state")
331
+ return state
332
+
333
+
334
+ def _require_pull_request_integration_proof(
335
+ repo: Path,
336
+ ship: str,
337
+ base: str,
338
+ previous_branch: str,
339
+ ) -> str:
340
+ state = _validated_shipped_state(repo)
341
+ if state.get("phase") != "shipped" or state.get("status") != "done":
342
+ raise PipelineGitError("bind-next requires shipped/done state")
343
+ if state.get("branch") != previous_branch:
344
+ raise PipelineGitError("shipped state does not name the previous branch")
345
+ if state.get("integration") != "pull-request":
346
+ raise PipelineGitError("shipped integration mode is not pull-request")
347
+ archive = state.get("archive")
348
+ if not isinstance(archive, str):
349
+ raise PipelineGitError("shipped state does not name an archive")
350
+ archive_pattern = r"\.project/archive/(\d{3,}-[a-z0-9][a-z0-9-]*)/?"
351
+ match = re.fullmatch(archive_pattern, archive)
352
+ if match is None or milestone_number(match.group(1)) != milestone_number(
353
+ previous_branch
354
+ ):
355
+ raise PipelineGitError("shipped archive does not match the previous branch")
356
+ ref = f"refs/tags/milestone/{match.group(1)}"
357
+ fields = _pull_request_tag_fields(repo, ref)
358
+ if set(fields) != {"Mode", "Pull-Request", "Ship", "Landing"}:
359
+ raise PipelineGitError("pull-request integration proof is incomplete")
360
+ if fields["Mode"] != "pull-request" or fields["Ship"] != ship:
361
+ raise PipelineGitError("pull-request integration proof does not match shipped state")
362
+ pull_request = fields["Pull-Request"]
363
+ if re.fullmatch(
364
+ r"https://github\.com/[^/]+/[^/]+/pull/[1-9][0-9]*",
365
+ pull_request,
366
+ ) is None:
367
+ raise PipelineGitError("pull-request integration proof has an invalid PR URL")
368
+ landing = fields["Landing"]
369
+ if re.fullmatch(r"[0-9a-f]{40}", landing) is None:
370
+ raise PipelineGitError("pull-request integration proof has an invalid landing")
371
+ local_object = _run_git(repo, "rev-parse", ref).stdout.strip()
372
+ local_target = _run_git(repo, "rev-parse", f"{ref}^{{commit}}").stdout.strip()
373
+ if local_target != landing:
374
+ raise PipelineGitError("pull-request integration tag does not point at landing")
375
+ remote = _run_git(
376
+ repo,
377
+ "ls-remote",
378
+ "--exit-code",
379
+ "--tags",
380
+ "origin",
381
+ ref,
382
+ f"{ref}^{{}}",
383
+ check=False,
384
+ )
385
+ if remote.returncode != 0:
386
+ raise PipelineGitError("pull-request integration tag is not published")
387
+ rows = dict(
388
+ line.split("\t", 1)[::-1]
389
+ for line in remote.stdout.splitlines()
390
+ if "\t" in line
391
+ )
392
+ if rows != {ref: local_object, f"{ref}^{{}}": landing}:
393
+ raise PipelineGitError("published pull-request integration tag differs")
394
+ parents = _run_git(
395
+ repo,
396
+ "rev-list",
397
+ "--parents",
398
+ "-n",
399
+ "1",
400
+ landing,
401
+ ).stdout.split()
402
+ if len(parents) != 3 or parents[2] != ship:
403
+ raise PipelineGitError("pull-request integration proof has invalid merge topology")
404
+ first_parent = _run_git(repo, "rev-list", "--first-parent", base).stdout.splitlines()
405
+ if landing not in first_parent:
406
+ raise PipelineGitError(
407
+ "pull-request integration landing is not on main first-parent history"
408
+ )
409
+ return landing
410
+
411
+
412
+ def _git_path(repo: Path, name: str) -> Path:
413
+ value = _run_git(repo, "rev-parse", "--git-path", name).stdout.strip()
414
+ path = Path(value)
415
+ if not path.is_absolute():
416
+ path = repo / path
417
+ return path.resolve()
418
+
419
+
420
+ def bind_next_journal_path(repo: Path, branch: str) -> Path:
421
+ """Return the durable ownership journal path for one target branch."""
422
+ if not is_bound_branch(branch):
423
+ raise PipelineGitError(f"invalid bound branch: {branch}")
424
+ return _git_path(repo, BIND_NEXT_JOURNAL_DIR) / f"M{milestone_number(branch):03d}.json"
425
+
426
+
427
+ def _read_bind_next_journal(path: Path) -> dict[str, object]:
428
+ if path.is_symlink() or not path.is_file():
429
+ raise PipelineGitError(f"bind-next journal must be a real file: {path}")
430
+ try:
431
+ value = json.loads(path.read_text(encoding="utf-8"))
432
+ except (OSError, json.JSONDecodeError) as error:
433
+ raise PipelineGitError(f"bind-next journal is unreadable: {path}: {error}") from error
434
+ if not isinstance(value, dict):
435
+ raise PipelineGitError(f"bind-next journal must contain an object: {path}")
436
+ return value
437
+
438
+
439
+ def _write_bind_next_journal(path: Path, value: dict[str, object]) -> None:
440
+ path.parent.mkdir(parents=True, exist_ok=True)
441
+ temporary = path.parent / f".{path.name}.tmp"
442
+ if temporary.exists() or temporary.is_symlink():
443
+ raise PipelineGitError(f"bind-next journal temporary path exists: {temporary}")
444
+ descriptor: Optional[int] = None
445
+ try:
446
+ descriptor = os.open(temporary, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600)
447
+ with os.fdopen(descriptor, "w", encoding="utf-8") as handle:
448
+ descriptor = None
449
+ json.dump(value, handle, indent=2, sort_keys=True)
450
+ handle.write("\n")
451
+ handle.flush()
452
+ os.fsync(handle.fileno())
453
+ os.replace(temporary, path)
454
+ directory = os.open(path.parent, os.O_RDONLY)
455
+ try:
456
+ os.fsync(directory)
457
+ finally:
458
+ os.close(directory)
459
+ finally:
460
+ if descriptor is not None:
461
+ os.close(descriptor)
462
+ if temporary.exists() or temporary.is_symlink():
463
+ temporary.unlink()
464
+
465
+
466
+ def _initial_binding_snapshot(repo: Path) -> Optional[tuple]:
467
+ """Admit only clean work or the validated, untracked initializer state."""
468
+ dirty = _run_git(repo, "status", "--porcelain=v1", "-z", "--untracked-files=all").stdout
469
+ if not dirty:
470
+ return None
471
+ if dirty != "?? .project/STATE.md\0":
472
+ raise PipelineGitError("primary worktree is not clean")
473
+ try:
474
+ import pipeline_state
475
+ except ImportError: # pragma: no cover - package import used by tests
476
+ from scripts import pipeline_state
477
+
478
+ project = repo / ".project"
479
+ path = project / "STATE.md"
480
+ try:
481
+ directory = project.lstat()
482
+ state_file = path.lstat()
483
+ if (not stat.S_ISDIR(directory.st_mode)
484
+ or not stat.S_ISREG(state_file.st_mode)
485
+ or state_file.st_nlink != 1
486
+ or set(project.iterdir()) != {path}):
487
+ raise PipelineGitError("initial state must be the sole regular, unlinked .project file")
488
+ state = pipeline_state.validate_state(repo)["state"]
489
+ if (state["phase"] not in {"inspect", "define"}
490
+ or state["status"] != "active"
491
+ or any(state[key] is not None for key in ("branch", "milestone", "archive"))):
492
+ raise PipelineGitError("untracked state is not an unbound initial state")
493
+ return (directory.st_dev, directory.st_ino, state_file.st_dev,
494
+ state_file.st_ino, path.read_bytes())
495
+ except (OSError, pipeline_state.PipelineStateError) as error:
496
+ raise PipelineGitError(f"invalid initial state: {error}") from error
497
+
498
+
499
+ def bind_initial_milestone_branch(
500
+ repo: Path,
501
+ branch: str,
502
+ remote_default: str,
503
+ base: str,
504
+ ) -> dict[str, str]:
505
+ """Bind the first milestone at an exact fetched remote-default SHA."""
506
+ resolved = _require_worktree_root(repo)
507
+ if not is_bound_branch(branch):
508
+ raise PipelineGitError(f"invalid bound branch: {branch}")
509
+ if remote_default != "origin/main":
510
+ raise PipelineGitError(f"remote default must be origin/main, got {remote_default}")
511
+
512
+ default_sha = _run_git(
513
+ resolved,
514
+ "rev-parse",
515
+ "--verify",
516
+ f"{remote_default}^{{commit}}",
517
+ ).stdout.strip()
518
+ base_sha = _run_git(
519
+ resolved,
520
+ "rev-parse",
521
+ "--verify",
522
+ f"{base}^{{commit}}",
523
+ ).stdout.strip()
524
+ if base != base_sha:
525
+ raise PipelineGitError(f"base must be the full commit SHA: {base}")
526
+ if base_sha != default_sha:
527
+ raise PipelineGitError(
528
+ f"validated base is stale: {base_sha} != {remote_default} {default_sha}"
529
+ )
530
+ remote_sha = _remote_ref_sha(resolved, "refs/heads/main")
531
+ if remote_sha != default_sha:
532
+ raise PipelineGitError(
533
+ f"fetched {remote_default} is stale relative to origin"
534
+ )
535
+ initial_state = _initial_binding_snapshot(resolved)
536
+
537
+ current = _symbolic_branch(resolved)
538
+ head = _run_git(resolved, "rev-parse", "HEAD").stdout.strip()
539
+ if head != base_sha:
540
+ raise PipelineGitError(
541
+ f"primary worktree is not at {remote_default}: {head} != {default_sha}"
542
+ )
543
+ worktrees = _branch_worktrees(resolved)
544
+ if current not in worktrees or worktrees[current] != resolved:
545
+ raise PipelineGitError("repo is not the registered path for its current worktree")
546
+ owner = worktrees.get(branch)
547
+ if owner is not None and owner != resolved:
548
+ raise PipelineGitError(f"bound branch is checked out by another worktree: {owner}")
549
+ if _remote_branch_exists(resolved, branch):
550
+ raise PipelineGitError(f"bound branch already exists: refs/heads/{branch} on origin")
551
+
552
+ if current == branch:
553
+ status = "already-bound"
554
+ else:
555
+ if is_bound_branch(current):
556
+ raise PipelineGitError(
557
+ f"current branch is already bound to another milestone: {current}"
558
+ )
559
+ local_ref = f"refs/heads/{branch}"
560
+ if _ref_exists(resolved, local_ref):
561
+ raise PipelineGitError(f"bound branch already exists: {local_ref}")
562
+ if _initial_binding_snapshot(resolved) != initial_state:
563
+ raise PipelineGitError("initial state changed before branch binding")
564
+ _run_git(resolved, "switch", "--no-track", "-c", branch, base_sha)
565
+ status = "bound"
566
+
567
+ if _initial_binding_snapshot(resolved) != initial_state:
568
+ raise PipelineGitError("initial state changed during branch binding")
569
+
570
+ return {
571
+ "schema": "gsd-path/bind-initial/v1",
572
+ "status": status,
573
+ "base": default_sha,
574
+ "branch": branch,
575
+ "previous_branch": current,
576
+ }
577
+
578
+
579
+ def retire_previous_branch(
580
+ repo: Path,
581
+ previous_branch: str,
582
+ expected_sha: str,
583
+ *,
584
+ allow_remote_absent: bool = False,
585
+ ) -> None:
586
+ """Delete an integrated previous bound branch, locally and on origin.
587
+
588
+ Retirement is hygiene, never reachability: callers prove the branch tip is
589
+ an ancestor of the validated remote default first, and the milestone tag
590
+ preserves the integration merge. The live remote branch is deleted first
591
+ with an expected-SHA lease; local cleanup follows only after that proof.
592
+ """
593
+ local_ref = f"refs/heads/{previous_branch}"
594
+ if _ref_exists(repo, local_ref):
595
+ local_sha = _run_git(repo, "rev-parse", f"{local_ref}^{{commit}}").stdout.strip()
596
+ if local_sha != expected_sha:
597
+ raise PipelineGitError(
598
+ f"previous branch moved after ship: {local_sha} != {expected_sha}"
599
+ )
600
+
601
+ remote_ref = f"refs/heads/{previous_branch}"
602
+ remote_sha = _remote_ref_sha(repo, remote_ref)
603
+ if remote_sha is None:
604
+ if not allow_remote_absent:
605
+ raise PipelineGitError(f"origin/{previous_branch} is missing before retirement")
606
+ else:
607
+ if remote_sha != expected_sha:
608
+ raise PipelineGitError(
609
+ f"origin/{previous_branch} moved after ship: {remote_sha} != {expected_sha}"
610
+ )
611
+ push = _run_git(
612
+ repo,
613
+ "push",
614
+ f"--force-with-lease={remote_ref}:{expected_sha}",
615
+ "origin",
616
+ "--delete",
617
+ previous_branch,
618
+ check=False,
619
+ )
620
+ if push.returncode != 0:
621
+ detail = (push.stderr or push.stdout).strip()
622
+ raise PipelineGitError(
623
+ f"could not delete origin/{previous_branch} with expected-SHA lease: {detail}"
624
+ )
625
+ if _remote_ref_sha(repo, remote_ref) is not None:
626
+ raise PipelineGitError(f"origin/{previous_branch} still exists after retirement")
627
+
628
+ tracking_ref = f"refs/remotes/origin/{previous_branch}"
629
+ if _ref_exists(repo, tracking_ref):
630
+ _run_git(repo, "update-ref", "-d", tracking_ref)
631
+ if _ref_exists(repo, local_ref):
632
+ _run_git(repo, "branch", "-d", previous_branch)
633
+
634
+
635
+ def bind_next_milestone_branch(
636
+ repo: Path,
637
+ branch: str,
638
+ previous_branch: str,
639
+ ship: str,
640
+ remote_default: str,
641
+ base: str,
642
+ landing: str,
643
+ allow_remote_absent: bool = False,
644
+ ) -> dict[str, str]:
645
+ """Move a clean primary worktree onto a new bound branch after integration."""
646
+ repo = _require_worktree_root(repo)
647
+ if not is_bound_branch(branch):
648
+ raise PipelineGitError(f"invalid bound branch: {branch}")
649
+ if not is_bound_branch(previous_branch):
650
+ raise PipelineGitError(f"invalid previous bound branch: {previous_branch}")
651
+ if milestone_number(branch) <= milestone_number(previous_branch):
652
+ raise PipelineGitError(f"next branch {branch} must follow {previous_branch}")
653
+ if remote_default != "origin/main":
654
+ raise PipelineGitError(f"remote default must be origin/main, got {remote_default}")
655
+
656
+ default_sha = _run_git(
657
+ repo,
658
+ "rev-parse",
659
+ "--verify",
660
+ f"{remote_default}^{{commit}}",
661
+ ).stdout.strip()
662
+ base_sha = _run_git(
663
+ repo,
664
+ "rev-parse",
665
+ "--verify",
666
+ f"{base}^{{commit}}",
667
+ ).stdout.strip()
668
+ if base != base_sha:
669
+ raise PipelineGitError(f"base must be the full commit SHA: {base}")
670
+ if base_sha != default_sha:
671
+ raise PipelineGitError(
672
+ f"validated base is stale: {base_sha} != {remote_default} {default_sha}"
673
+ )
674
+ live_default_sha = _remote_ref_sha(repo, "refs/heads/main")
675
+ if live_default_sha != default_sha:
676
+ raise PipelineGitError(f"fetched {remote_default} is stale relative to origin")
677
+ ship_sha = _run_git(
678
+ repo,
679
+ "rev-parse",
680
+ "--verify",
681
+ f"{ship}^{{commit}}",
682
+ ).stdout.strip()
683
+ if ship != ship_sha:
684
+ raise PipelineGitError(f"ship must be the full commit SHA: {ship}")
685
+ landing_sha = _run_git(
686
+ repo,
687
+ "rev-parse",
688
+ "--verify",
689
+ f"{landing}^{{commit}}",
690
+ ).stdout.strip()
691
+ if landing != landing_sha:
692
+ raise PipelineGitError(f"landing must be the full commit SHA: {landing}")
693
+ previous_ref = f"refs/heads/{previous_branch}"
694
+ if _ref_exists(repo, previous_ref):
695
+ previous_sha = _run_git(
696
+ repo,
697
+ "rev-parse",
698
+ "--verify",
699
+ f"{previous_ref}^{{commit}}",
700
+ ).stdout.strip()
701
+ if previous_sha != ship_sha:
702
+ raise PipelineGitError(
703
+ f"previous branch moved after ship: {previous_sha} != {ship_sha}"
704
+ )
705
+ integrated = _run_git(
706
+ repo,
707
+ "merge-base",
708
+ "--is-ancestor",
709
+ ship_sha,
710
+ landing_sha,
711
+ check=False,
712
+ )
713
+ if integrated.returncode == 1:
714
+ raise PipelineGitError("milestone landing does not contain the ship commit")
715
+ if integrated.returncode != 0:
716
+ detail = integrated.stderr.strip() or integrated.stdout.strip()
717
+ raise PipelineGitError(f"could not verify milestone landing: {detail}")
718
+ landing_integrated = _run_git(
719
+ repo,
720
+ "merge-base",
721
+ "--is-ancestor",
722
+ landing_sha,
723
+ base_sha,
724
+ check=False,
725
+ )
726
+ if landing_integrated.returncode == 1:
727
+ raise PipelineGitError("milestone landing is not integrated into the validated base")
728
+ if landing_integrated.returncode != 0:
729
+ detail = landing_integrated.stderr.strip() or landing_integrated.stdout.strip()
730
+ raise PipelineGitError(f"could not verify integrated branch: {detail}")
731
+ if allow_remote_absent:
732
+ proven_landing = _require_pull_request_integration_proof(
733
+ repo,
734
+ ship_sha,
735
+ base_sha,
736
+ previous_branch,
737
+ )
738
+ if proven_landing != landing_sha:
739
+ raise PipelineGitError(
740
+ "validated landing does not match pull-request integration proof"
741
+ )
742
+
743
+ symbolic_branch = _run_git(
744
+ repo,
745
+ "symbolic-ref",
746
+ "--quiet",
747
+ "--short",
748
+ "HEAD",
749
+ check=False,
750
+ )
751
+ if symbolic_branch.returncode != 0:
752
+ raise PipelineGitError("primary worktree must be on a named branch")
753
+ current = symbolic_branch.stdout.strip()
754
+ status = "already-bound" if current == branch else "bound"
755
+ if _run_git(repo, "status", "--porcelain", "--untracked-files=all").stdout:
756
+ raise PipelineGitError("primary worktree is not clean")
757
+ if _remote_branch_exists(repo, branch):
758
+ raise PipelineGitError(f"bound branch already exists: refs/heads/{branch} on origin")
759
+
760
+ request: dict[str, object] = {
761
+ "schema": BIND_NEXT_JOURNAL_SCHEMA,
762
+ "repo": str(repo),
763
+ "branch": branch,
764
+ "previous_branch": previous_branch,
765
+ "ship": ship_sha,
766
+ "remote_default": remote_default,
767
+ "base": base_sha,
768
+ "landing": landing_sha,
769
+ }
770
+ if allow_remote_absent:
771
+ request["allow_remote_absent"] = True
772
+ journal_path = bind_next_journal_path(repo, branch)
773
+ new_journal = False
774
+ if journal_path.exists() or journal_path.is_symlink():
775
+ journal = _read_bind_next_journal(journal_path)
776
+ legacy = journal.get("schema") == LEGACY_BIND_NEXT_JOURNAL_SCHEMA
777
+ expected_fields = set(request) | {"stage"}
778
+ if legacy:
779
+ legacy_fields = expected_fields - {"landing"}
780
+ if set(journal) != legacy_fields and set(journal) != expected_fields:
781
+ raise PipelineGitError("bind-next journal has unsupported fields")
782
+ elif set(journal) != expected_fields:
783
+ raise PipelineGitError("bind-next journal has unsupported fields")
784
+ mismatches = {
785
+ key: {"expected": value, "actual": journal.get(key)}
786
+ for key, value in request.items()
787
+ if key != "schema"
788
+ and (key != "landing" or "landing" in journal)
789
+ and journal.get(key) != value
790
+ }
791
+ if not legacy and journal.get("schema") != BIND_NEXT_JOURNAL_SCHEMA:
792
+ mismatches["schema"] = {
793
+ "expected": BIND_NEXT_JOURNAL_SCHEMA,
794
+ "actual": journal.get("schema"),
795
+ }
796
+ if mismatches:
797
+ raise PipelineGitError(
798
+ f"bind-next journal does not match request: {json.dumps(mismatches, sort_keys=True)}"
799
+ )
800
+ if journal.get("stage") not in {"prepared", "switched", "retired"}:
801
+ raise PipelineGitError("bind-next journal has invalid stage")
802
+ if legacy:
803
+ journal["schema"] = BIND_NEXT_JOURNAL_SCHEMA
804
+ journal["landing"] = landing_sha
805
+ _write_bind_next_journal(journal_path, journal)
806
+ else:
807
+ if current == branch:
808
+ raise PipelineGitError(
809
+ f"current target branch {branch} has no matching bind-next journal"
810
+ )
811
+ if current != previous_branch:
812
+ raise PipelineGitError(
813
+ f"current branch {current} does not match previous branch {previous_branch}"
814
+ )
815
+ local_branch_ref = f"refs/heads/{branch}"
816
+ if _ref_exists(repo, local_branch_ref):
817
+ raise PipelineGitError(f"bound branch already exists: {local_branch_ref}")
818
+ journal = {**request, "stage": "prepared"}
819
+ new_journal = True
820
+
821
+ stage = journal["stage"]
822
+ if current not in {previous_branch, branch}:
823
+ raise PipelineGitError(
824
+ f"current branch {current} does not match bind-next journal ownership"
825
+ )
826
+ if current == previous_branch and stage != "prepared":
827
+ raise PipelineGitError(
828
+ f"bind-next journal stage {stage} conflicts with current {previous_branch}"
829
+ )
830
+ if current == branch:
831
+ head = _run_git(repo, "rev-parse", "HEAD").stdout.strip()
832
+ if head != base_sha:
833
+ raise PipelineGitError(
834
+ f"existing {branch} is not at {remote_default}: {head} != {default_sha}"
835
+ )
836
+ if stage == "prepared":
837
+ journal["stage"] = "switched"
838
+ _write_bind_next_journal(journal_path, journal)
839
+
840
+ remote_previous = _remote_ref_sha(repo, f"refs/heads/{previous_branch}")
841
+ if remote_previous is None:
842
+ if not allow_remote_absent and (
843
+ current != branch or journal["stage"] not in {"switched", "retired"}
844
+ ):
845
+ raise PipelineGitError(
846
+ f"origin/{previous_branch} is missing before retirement"
847
+ )
848
+ elif remote_previous != ship_sha:
849
+ raise PipelineGitError(
850
+ f"origin/{previous_branch} moved after ship: {remote_previous} != {ship_sha}"
851
+ )
852
+
853
+ if current == previous_branch:
854
+ if new_journal:
855
+ _write_bind_next_journal(journal_path, journal)
856
+ _run_git(repo, "switch", "--no-track", "-c", branch, base_sha)
857
+ journal["stage"] = "switched"
858
+ _write_bind_next_journal(journal_path, journal)
859
+
860
+ retire_previous_branch(
861
+ repo,
862
+ previous_branch,
863
+ ship_sha,
864
+ allow_remote_absent=(
865
+ allow_remote_absent or journal["stage"] in {"switched", "retired"}
866
+ ),
867
+ )
868
+ journal["stage"] = "retired"
869
+ _write_bind_next_journal(journal_path, journal)
870
+ return {
871
+ "schema": "gsd-path/bind-next/v1",
872
+ "status": status,
873
+ "base": default_sha,
874
+ "landing": landing_sha,
875
+ "branch": branch,
876
+ "previous_branch": previous_branch,
877
+ }
878
+
879
+
880
+ def parse_args(argv: Optional[Sequence[str]] = None) -> argparse.Namespace:
881
+ parser = argparse.ArgumentParser(description=__doc__)
882
+ subparsers = parser.add_subparsers(dest="command", required=True)
883
+ bind_next = subparsers.add_parser(
884
+ "bind-next",
885
+ help=(
886
+ "bind the next milestone branch at the integrated remote default "
887
+ "and retire the previous bound branch"
888
+ ),
889
+ )
890
+ bind_next.add_argument("--repo", required=True, type=Path)
891
+ bind_next.add_argument("--branch", required=True)
892
+ bind_next.add_argument("--previous-branch", required=True)
893
+ bind_next.add_argument("--ship", required=True)
894
+ bind_next.add_argument("--remote-default", required=True)
895
+ bind_next.add_argument("--base", required=True)
896
+ bind_next.add_argument("--landing", required=True)
897
+ bind_next.add_argument("--allow-missing-previous", action="store_true")
898
+ bind_initial = subparsers.add_parser(
899
+ "bind-initial",
900
+ help="bind the first milestone branch at the exact fetched remote default",
901
+ )
902
+ bind_initial.add_argument("--repo", required=True, type=Path)
903
+ bind_initial.add_argument("--branch", required=True)
904
+ bind_initial.add_argument("--remote-default", required=True)
905
+ bind_initial.add_argument("--base", required=True)
906
+ return parser.parse_args(argv)
907
+
908
+
909
+ def main(argv: Optional[Sequence[str]] = None) -> int:
910
+ args = parse_args(argv)
911
+ try:
912
+ if args.command == "bind-next":
913
+ result = bind_next_milestone_branch(
914
+ args.repo,
915
+ args.branch,
916
+ args.previous_branch,
917
+ args.ship,
918
+ args.remote_default,
919
+ args.base,
920
+ args.landing,
921
+ args.allow_missing_previous,
922
+ )
923
+ elif args.command == "bind-initial":
924
+ result = bind_initial_milestone_branch(
925
+ args.repo,
926
+ args.branch,
927
+ args.remote_default,
928
+ args.base,
929
+ )
930
+ else: # pragma: no cover - argparse rejects unknown commands
931
+ raise PipelineGitError(f"unknown command: {args.command}")
932
+ except PipelineGitError as exc:
933
+ print(f"error: {exc}", file=sys.stderr)
934
+ return 1
935
+ print(json.dumps(result, sort_keys=True))
936
+ return 0
937
+
938
+
939
+ if __name__ == "__main__":
940
+ raise SystemExit(main())