@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,854 @@
1
+ #!/usr/bin/env python3
2
+ """Build task readiness, landing reconciliation, and the verify ledger."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import argparse
7
+ import json
8
+ import re
9
+ import sys
10
+ from dataclasses import dataclass
11
+ from datetime import datetime, timezone
12
+ from pathlib import Path, PurePosixPath
13
+ from typing import Dict, List, Mapping, Optional, Sequence, Tuple
14
+
15
+ try:
16
+ from check_task_briefs import _frontmatter, _sections
17
+ from isolation import (
18
+ VERIFY_LEDGER_PATH,
19
+ IsolationError,
20
+ recover as recover_isolation,
21
+ verify_landed_task_files,
22
+ )
23
+ from pipeline_git import task_commit_subject
24
+ from pipeline_state import PipelineStateError, load_state
25
+ import _common
26
+ except ImportError: # pragma: no cover - package imports used by tests
27
+ from scripts.check_task_briefs import _frontmatter, _sections
28
+ from scripts.isolation import (
29
+ VERIFY_LEDGER_PATH,
30
+ IsolationError,
31
+ recover as recover_isolation,
32
+ verify_landed_task_files,
33
+ )
34
+ from scripts.pipeline_git import task_commit_subject
35
+ from scripts.pipeline_state import PipelineStateError, load_state
36
+ from scripts import _common
37
+
38
+
39
+ DEFAULT_PROJECT_DIR = ".project"
40
+ TASK_ID_RE = re.compile(r"^T\d{3}$")
41
+ TASK_FILE_RE = re.compile(r"^(?P<id>T\d{3})-[a-z0-9][a-z0-9-]*\.md$")
42
+ FULL_SHA_RE = re.compile(r"^[0-9a-f]{40}$")
43
+ # recover verdicts that count as landed, mapped to their evidence classification.
44
+ LANDED_VERDICTS = {"recovered": "proven-landed", "attested": "attested"}
45
+ WAVE_HEADING_RE = re.compile(r"(?m)^## Wave (?P<wave>\d+)\b.*$")
46
+ VERIFY_HEAVY_RE = re.compile(r"(?m)^Heavy:\s*(?P<value>yes|no)\s*(?:<!--.*-->)?\s*$")
47
+ VALID_TASK_STATUSES = {"pending", "in-progress", "done", "failed", "blocked"}
48
+ VERIFY_RESULTS = _common.VERIFY_RESULTS
49
+
50
+
51
+ class BuildStateError(RuntimeError):
52
+ """A typed, user-actionable build state error."""
53
+
54
+ def __init__(
55
+ self, code: str, message: str, details: Optional[Mapping[str, object]] = None
56
+ ) -> None:
57
+ super().__init__(message)
58
+ self.code = code
59
+ self.details = dict(details or {})
60
+
61
+
62
+ @dataclass(frozen=True)
63
+ class Task:
64
+ task_id: str
65
+ title: str
66
+ wave: int
67
+ deps: Tuple[str, ...]
68
+ files: Tuple[str, ...]
69
+ status: str
70
+ agent: Optional[str]
71
+ base: Optional[str]
72
+ worktree: Optional[str]
73
+ task_branch: Optional[str]
74
+ task_file: str
75
+ verify_heavy: bool
76
+
77
+
78
+ @dataclass(frozen=True)
79
+ class Project:
80
+ repo: Path
81
+ branch: str
82
+ head: str
83
+ tasks: Tuple[Task, ...]
84
+ tasks_dir: Path
85
+
86
+
87
+ _run_git = _common.run_git
88
+
89
+
90
+ def _git_output(repo: Path, *arguments: str) -> str:
91
+ result = _run_git(repo, *arguments)
92
+ if result.returncode != 0:
93
+ detail = (result.stderr or result.stdout).strip() or "git command failed"
94
+ raise BuildStateError("git-error", detail)
95
+ return result.stdout.strip()
96
+
97
+
98
+ def _repo_root(value: str) -> Path:
99
+ candidate = Path(value).resolve()
100
+ if not candidate.is_dir():
101
+ raise BuildStateError("invalid-repo", f"repository is not a directory: {candidate}")
102
+ result = _run_git(candidate, "rev-parse", "--show-toplevel")
103
+ if result.returncode != 0:
104
+ raise BuildStateError("invalid-repo", f"not a Git repository: {candidate}")
105
+ root = Path(result.stdout.strip()).resolve()
106
+ if root != candidate:
107
+ raise BuildStateError("invalid-repo", f"--repo must be the Git root: {root}")
108
+ return root
109
+
110
+
111
+ def _project_path(repo: Path, value: str) -> Tuple[str, Path]:
112
+ relative = PurePosixPath(value)
113
+ if relative.is_absolute() or not relative.parts or ".." in relative.parts:
114
+ raise BuildStateError(
115
+ "invalid-project-dir", "--project-dir must be a repo-relative path"
116
+ )
117
+ normalized = str(relative)
118
+ path = repo.joinpath(*relative.parts)
119
+ if not path.is_dir() or path.is_symlink():
120
+ raise BuildStateError(
121
+ "invalid-project-dir", f"project directory is missing or unsafe: {normalized}"
122
+ )
123
+ return normalized, path
124
+
125
+
126
+ def _read_file(path: Path, label: str) -> str:
127
+ if not path.is_file() or path.is_symlink():
128
+ raise BuildStateError("missing-input", f"{label} is missing or unsafe: {path}")
129
+ return path.read_text(encoding="utf-8")
130
+
131
+
132
+ def _parse_frontmatter(text: str, label: str) -> Dict[str, object]:
133
+ fields, error = _frontmatter(text)
134
+ if fields is None:
135
+ raise BuildStateError("invalid-frontmatter", f"{label}: {error}")
136
+ return fields
137
+
138
+
139
+ def _verify_heavy(text: str, label: str) -> bool:
140
+ """Read the optional `Heavy: yes|no` line of the task's ## Verify section."""
141
+
142
+ matches = VERIFY_HEAVY_RE.findall(_sections(text).get("Verify", ""))
143
+ if len(matches) > 1:
144
+ raise BuildStateError("invalid-task-file", f"{label} repeats Verify Heavy")
145
+ return bool(matches) and matches[0] == "yes"
146
+
147
+
148
+ def _required_string(fields: Mapping[str, object], field: str, label: str) -> str:
149
+ value = fields.get(field)
150
+ if not isinstance(value, str) or not value.strip():
151
+ raise BuildStateError("invalid-frontmatter", f"{label} has invalid {field}")
152
+ return value.strip()
153
+
154
+
155
+ def _nullable_string(fields: Mapping[str, object], field: str, label: str) -> Optional[str]:
156
+ value = fields.get(field)
157
+ if not isinstance(value, str):
158
+ raise BuildStateError("invalid-frontmatter", f"{label} has invalid {field}")
159
+ cleaned = value.strip()
160
+ return None if cleaned in {"", "null"} else cleaned
161
+
162
+
163
+ def _normalize_list(
164
+ value: object, label: str, *, task_ids: bool = False, paths: bool = False
165
+ ) -> Tuple[str, ...]:
166
+ if not isinstance(value, list) or any(not isinstance(item, str) for item in value):
167
+ raise BuildStateError("invalid-frontmatter", f"{label} must be a list")
168
+ normalized: List[str] = []
169
+ for raw in value:
170
+ item = raw.strip()
171
+ if task_ids and not TASK_ID_RE.fullmatch(item):
172
+ raise BuildStateError("invalid-frontmatter", f"{label} has invalid task id: {item}")
173
+ if paths:
174
+ candidate = PurePosixPath(item)
175
+ if candidate.is_absolute() or not candidate.parts or ".." in candidate.parts:
176
+ raise BuildStateError("invalid-frontmatter", f"{label} has invalid path: {item}")
177
+ item = str(candidate)
178
+ if not item:
179
+ raise BuildStateError("invalid-frontmatter", f"{label} has an empty value")
180
+ normalized.append(item)
181
+ if len(normalized) != len(set(normalized)):
182
+ raise BuildStateError("invalid-frontmatter", f"{label} repeats a value")
183
+ return tuple(normalized)
184
+
185
+
186
+ def _parse_plan(text: str) -> Tuple[int, ...]:
187
+ headings = list(WAVE_HEADING_RE.finditer(text))
188
+ if not headings:
189
+ raise BuildStateError("invalid-plan", "PLAN.md has no waves")
190
+ wave_numbers = tuple(int(match.group("wave")) for match in headings)
191
+ if wave_numbers != tuple(range(1, len(wave_numbers) + 1)):
192
+ raise BuildStateError(
193
+ "invalid-plan", "PLAN.md wave numbers must be unique, ordered, and contiguous"
194
+ )
195
+ return wave_numbers
196
+
197
+
198
+ def _parse_task(path: Path, relative_path: str, task_file: Optional[str] = None) -> Task:
199
+ label = relative_path
200
+ text = _read_file(path, label)
201
+ fields = _parse_frontmatter(text, label)
202
+ task_id = _required_string(fields, "id", label)
203
+ title = _required_string(fields, "title", label)
204
+ wave_text = _required_string(fields, "wave", label)
205
+ try:
206
+ wave = int(wave_text)
207
+ except ValueError as error:
208
+ raise BuildStateError("invalid-frontmatter", f"{label} has invalid wave") from error
209
+ deps = _normalize_list(fields.get("deps"), f"{label} deps", task_ids=True)
210
+ files = _normalize_list(fields.get("files"), f"{label} files", paths=True)
211
+ status = _required_string(fields, "status", label)
212
+ if status not in VALID_TASK_STATUSES:
213
+ raise BuildStateError("invalid-frontmatter", f"{label} has invalid status: {status}")
214
+
215
+ return Task(
216
+ task_id=task_id,
217
+ title=title,
218
+ wave=wave,
219
+ deps=deps,
220
+ files=files,
221
+ status=status,
222
+ agent=_nullable_string(fields, "agent", label),
223
+ base=_nullable_string(fields, "base", label),
224
+ worktree=_nullable_string(fields, "worktree", label),
225
+ task_branch=_nullable_string(fields, "task_branch", label),
226
+ task_file=task_file or relative_path,
227
+ verify_heavy=_verify_heavy(text, label),
228
+ )
229
+
230
+
231
+ def _validate_graph(tasks: Sequence[Task]) -> None:
232
+ by_id = {task.task_id: task for task in tasks}
233
+ for task in tasks:
234
+ for dependency in task.deps:
235
+ if dependency not in by_id:
236
+ raise BuildStateError(
237
+ "missing-dependency",
238
+ f"{task.task_id} names missing dependency {dependency}",
239
+ {"task": task.task_id, "dependency": dependency},
240
+ )
241
+ if by_id[dependency].wave > task.wave:
242
+ raise BuildStateError(
243
+ "dependency-wave-order",
244
+ f"{task.task_id} depends on later-wave {dependency}",
245
+ {"task": task.task_id, "dependency": dependency},
246
+ )
247
+
248
+ visiting: List[str] = []
249
+ visited = set()
250
+
251
+ def visit(task_id: str) -> None:
252
+ if task_id in visiting:
253
+ start = visiting.index(task_id)
254
+ cycle = visiting[start:] + [task_id]
255
+ raise BuildStateError(
256
+ "dependency-cycle",
257
+ "dependency cycle: " + " -> ".join(cycle),
258
+ {"cycle": cycle},
259
+ )
260
+ if task_id in visited:
261
+ return
262
+ visiting.append(task_id)
263
+ for dependency in by_id[task_id].deps:
264
+ visit(dependency)
265
+ visiting.pop()
266
+ visited.add(task_id)
267
+
268
+ for task in tasks:
269
+ visit(task.task_id)
270
+
271
+
272
+ def _load_tasks(
273
+ repo: Path,
274
+ project_dir: str,
275
+ project_path: Path,
276
+ canonical_project_dir: Optional[str] = None,
277
+ ) -> Tuple[Task, ...]:
278
+ plan_path = project_path / "plan" / "PLAN.md"
279
+ plan_waves = _parse_plan(_read_file(plan_path, f"{project_dir}/plan/PLAN.md"))
280
+ tasks_dir = project_path / "tasks"
281
+ if not tasks_dir.is_dir() or tasks_dir.is_symlink():
282
+ raise BuildStateError("missing-input", f"task directory is missing or unsafe: {tasks_dir}")
283
+ task_paths: Dict[str, Path] = {}
284
+ for path in sorted(tasks_dir.iterdir()):
285
+ match = TASK_FILE_RE.fullmatch(path.name)
286
+ if match is None or not path.is_file() or path.is_symlink():
287
+ raise BuildStateError(
288
+ "invalid-task-file",
289
+ f"task artifacts must be real T###-slug.md files: {path.name}",
290
+ )
291
+ task_id = match.group("id")
292
+ if task_id in task_paths:
293
+ raise BuildStateError(
294
+ "duplicate-task-file",
295
+ f"multiple canonical task files map to {task_id}",
296
+ {"task": task_id, "files": [task_paths[task_id].name, path.name]},
297
+ )
298
+ task_paths[task_id] = path
299
+ parsed: List[Task] = []
300
+ for task_id, path in sorted(task_paths.items()):
301
+ relative = path.relative_to(repo).as_posix()
302
+ task_file = relative
303
+ if canonical_project_dir is not None:
304
+ task_file = str(PurePosixPath(canonical_project_dir) / "tasks" / path.name)
305
+ task = _parse_task(path, relative, task_file)
306
+ if task.task_id != task_id:
307
+ raise BuildStateError(
308
+ "invalid-task-file",
309
+ f"{relative} frontmatter id does not match its filename",
310
+ )
311
+ if task.wave not in plan_waves:
312
+ raise BuildStateError(
313
+ "plan-task-mismatch",
314
+ f"{task.task_id} names missing PLAN.md Wave {task.wave}",
315
+ )
316
+ parsed.append(task)
317
+ missing_waves = sorted(set(plan_waves) - {task.wave for task in parsed})
318
+ if missing_waves:
319
+ raise BuildStateError(
320
+ "invalid-plan",
321
+ "PLAN.md waves without task files: "
322
+ + ", ".join(str(wave) for wave in missing_waves),
323
+ )
324
+ _validate_graph(parsed)
325
+ return tuple(parsed)
326
+
327
+
328
+ def _load_project(repo_value: str, project_value: str, statuses: Sequence[str]) -> Project:
329
+ repo = _repo_root(repo_value)
330
+ project_dir, project_path = _project_path(repo, project_value)
331
+ if project_dir == ".project/next":
332
+ raise BuildStateError(
333
+ "invalid-project-dir",
334
+ "build task state cannot use the lookahead .project/next track",
335
+ )
336
+ try:
337
+ state, _, _ = load_state(repo, project_dir)
338
+ except PipelineStateError as error:
339
+ raise BuildStateError("invalid-state", str(error)) from error
340
+ if state.phase != "build" or state.status not in statuses:
341
+ expected = ", ".join(statuses)
342
+ raise BuildStateError(
343
+ "invalid-build-state", f"STATE.md must be build with status in: {expected}"
344
+ )
345
+ if state.branch is None:
346
+ raise BuildStateError("invalid-build-state", "build STATE.md must name a branch")
347
+ branch = state.branch
348
+ current_branch = _git_output(repo, "branch", "--show-current")
349
+ if not current_branch or current_branch != branch:
350
+ raise BuildStateError(
351
+ "branch-mismatch",
352
+ f"primary branch is {current_branch or '(detached)'}, expected {branch}",
353
+ )
354
+ head = _git_output(repo, "rev-parse", "HEAD")
355
+ parsed = _load_tasks(repo, project_dir, project_path)
356
+ return Project(repo, branch, head, tuple(parsed), project_path / "tasks")
357
+
358
+
359
+ def _commit_resolves(repo: Path, value: str) -> bool:
360
+ result = _run_git(repo, "rev-parse", "--verify", "--quiet", f"{value}^{{commit}}")
361
+ return result.returncode == 0
362
+
363
+
364
+ def _is_ancestor(repo: Path, ancestor: str, descendant: str) -> bool:
365
+ return _run_git(repo, "merge-base", "--is-ancestor", ancestor, descendant).returncode == 0
366
+
367
+
368
+ def _invalid_state(task: Task, message: str) -> None:
369
+ raise BuildStateError(
370
+ "invalid-task-state", f"{task.task_id}: {message}", {"task": task.task_id}
371
+ )
372
+
373
+
374
+ def _recovery_reports(project: Project) -> Dict[str, Mapping[str, object]]:
375
+ try:
376
+ recovery = recover_isolation(project.repo, project.tasks_dir)
377
+ except IsolationError as error:
378
+ raise BuildStateError("invalid-task-state", str(error)) from error
379
+ if recovery.get("verdict") == "block" and not recovery.get("tasks"):
380
+ raise BuildStateError(
381
+ "invalid-task-state",
382
+ str(recovery.get("reason", "task recovery failed")),
383
+ )
384
+ reports: Dict[str, Mapping[str, object]] = {}
385
+ for report in recovery.get("tasks", []):
386
+ if not isinstance(report, Mapping):
387
+ raise BuildStateError("invalid-task-state", "task recovery returned invalid evidence")
388
+ task_id = report.get("task_id")
389
+ if not isinstance(task_id, str) or task_id in reports:
390
+ raise BuildStateError("invalid-task-state", "task recovery returned invalid ownership")
391
+ reports[task_id] = report
392
+ expected = {task.task_id for task in project.tasks}
393
+ if set(reports) != expected:
394
+ raise BuildStateError("invalid-task-state", "task recovery inventory is incomplete")
395
+ return reports
396
+
397
+
398
+ def _validate_ready_metadata(project: Project) -> None:
399
+ by_id = {task.task_id: task for task in project.tasks}
400
+ recovery = _recovery_reports(project)
401
+ seen_worktrees: Dict[str, str] = {}
402
+ seen_branches: Dict[str, str] = {}
403
+ for task in project.tasks:
404
+ if task.status == "pending":
405
+ if any(
406
+ value is not None
407
+ for value in (
408
+ task.agent,
409
+ task.base,
410
+ task.worktree,
411
+ task.task_branch,
412
+ )
413
+ ):
414
+ _invalid_state(task, "pending task retains dispatch metadata")
415
+ continue
416
+ if task.status in {"in-progress", "failed", "blocked"}:
417
+ if task.agent is None or task.base is None or task.worktree is None:
418
+ _invalid_state(
419
+ task,
420
+ f"{task.status} task is missing agent, base, or worktree",
421
+ )
422
+ if not FULL_SHA_RE.fullmatch(task.base) or not _commit_resolves(
423
+ project.repo, task.base
424
+ ):
425
+ _invalid_state(
426
+ task,
427
+ f"{task.status} task base is not a full, existing commit",
428
+ )
429
+ if not _is_ancestor(project.repo, task.base, project.head):
430
+ _invalid_state(
431
+ task,
432
+ f"{task.status} task base is not an ancestor of HEAD",
433
+ )
434
+ if any(by_id[dependency].status != "done" for dependency in task.deps):
435
+ _invalid_state(task, f"{task.status} task has an incomplete dependency")
436
+ if task.task_branch is None:
437
+ if Path(task.worktree).resolve() != project.repo:
438
+ _invalid_state(task, "serial task does not name the primary worktree")
439
+ else:
440
+ expected = f"gsd-path-task/{task.task_id}"
441
+ if task.task_branch != expected or Path(task.worktree).resolve() == project.repo:
442
+ _invalid_state(task, "parallel task has invalid branch or worktree metadata")
443
+ previous = seen_branches.setdefault(task.task_branch, task.task_id)
444
+ if previous != task.task_id:
445
+ _invalid_state(task, f"task branch is also owned by {previous}")
446
+ worktree_key = str(Path(task.worktree).resolve())
447
+ previous = seen_worktrees.setdefault(worktree_key, task.task_id)
448
+ if previous != task.task_id:
449
+ _invalid_state(task, f"worktree is also owned by {previous}")
450
+ continue
451
+ if task.status == "done":
452
+ if any(by_id[dependency].status != "done" for dependency in task.deps):
453
+ _invalid_state(task, "done task has an incomplete dependency")
454
+ if task.agent is None or task.base is None:
455
+ _invalid_state(task, "done task is missing agent or base")
456
+ if task.worktree is not None or task.task_branch is not None:
457
+ _invalid_state(task, "done task retains active isolation metadata")
458
+ report = recovery[task.task_id]
459
+ if report.get("verdict") not in LANDED_VERDICTS:
460
+ reason = report.get("reason", "landing commit is not proven")
461
+ _invalid_state(task, str(reason))
462
+
463
+
464
+ def _overlap(left: Task, right: Task) -> List[str]:
465
+ return sorted(set(left.files) & set(right.files))
466
+
467
+
468
+ def ready(repo: str, project_dir: str = DEFAULT_PROJECT_DIR, *,
469
+ allow_done: bool = False) -> Dict[str, object]:
470
+ """Return task readiness, optionally accepting build/done for completion checks."""
471
+
472
+ project = _load_project(repo, project_dir, ("active", "done") if allow_done else ("active",))
473
+ _validate_ready_metadata(project)
474
+ by_id = {task.task_id: task for task in project.tasks}
475
+ unfinished_waves = sorted({task.wave for task in project.tasks if task.status != "done"})
476
+ current_wave = unfinished_waves[0] if unfinished_waves else None
477
+ active = [task for task in project.tasks if task.status == "in-progress"]
478
+ if current_wave is not None and any(task.wave != current_wave for task in active):
479
+ task = next(task for task in active if task.wave != current_wave)
480
+ _invalid_state(task, f"active outside current Wave {current_wave}")
481
+
482
+ recovery = [
483
+ task
484
+ for task in project.tasks
485
+ if task.wave == current_wave and task.status in {"failed", "blocked"}
486
+ ]
487
+ if recovery:
488
+ raise BuildStateError(
489
+ "task-recovery-required",
490
+ f"Wave {current_wave} has failed or blocked tasks requiring recovery",
491
+ {
492
+ "wave": current_wave,
493
+ "tasks": [
494
+ {"id": task.task_id, "status": task.status}
495
+ for task in recovery
496
+ ],
497
+ },
498
+ )
499
+
500
+ selectable = [
501
+ task
502
+ for task in project.tasks
503
+ if task.wave == current_wave
504
+ and task.status == "pending"
505
+ and all(by_id[dependency].status == "done" for dependency in task.deps)
506
+ ]
507
+ concurrent = selectable + active
508
+ for index, left in enumerate(concurrent):
509
+ for right in concurrent[index + 1 :]:
510
+ paths = _overlap(left, right)
511
+ if paths:
512
+ raise BuildStateError(
513
+ "ready-file-overlap",
514
+ f"concurrent tasks {left.task_id} and {right.task_id} overlap",
515
+ {"tasks": [left.task_id, right.task_id], "files": paths},
516
+ )
517
+
518
+ wave_active = any(task.wave == current_wave for task in active)
519
+ if current_wave is not None and not selectable and not wave_active:
520
+ stalled = [
521
+ {
522
+ "id": task.task_id,
523
+ "status": task.status,
524
+ "waiting_on": [
525
+ dependency
526
+ for dependency in task.deps
527
+ if by_id[dependency].status != "done"
528
+ ],
529
+ }
530
+ for task in project.tasks
531
+ if task.wave == current_wave and task.status != "done"
532
+ ]
533
+ raise BuildStateError(
534
+ "dependency-deadlock",
535
+ f"Wave {current_wave} has unfinished tasks but no ready or in-progress task",
536
+ {"wave": current_wave, "tasks": stalled},
537
+ )
538
+
539
+ return {
540
+ "command": "ready",
541
+ "branch": project.branch,
542
+ "head": project.head,
543
+ "current_wave": current_wave,
544
+ "ready": [
545
+ {
546
+ "id": task.task_id,
547
+ "title": task.title,
548
+ "wave": task.wave,
549
+ "status": task.status,
550
+ "deps": list(task.deps),
551
+ "files": list(task.files),
552
+ "task_file": task.task_file,
553
+ "verify_heavy": task.verify_heavy,
554
+ }
555
+ for task in selectable
556
+ ],
557
+ }
558
+
559
+
560
+ def _reconcile_result(
561
+ project: Project,
562
+ task: Task,
563
+ classification: str,
564
+ candidates: Sequence[Mapping[str, object]],
565
+ reasons: Sequence[str],
566
+ landed_commit: Optional[str] = None,
567
+ ) -> Dict[str, object]:
568
+ return {
569
+ "command": "reconcile",
570
+ "classification": classification,
571
+ "task": {
572
+ "id": task.task_id,
573
+ "status": task.status,
574
+ "task_file": task.task_file,
575
+ "base": task.base,
576
+ },
577
+ "history": {
578
+ "branch": project.branch,
579
+ "head": project.head,
580
+ "subject": task_commit_subject(task.task_id, task.title),
581
+ "candidates": list(candidates),
582
+ },
583
+ "landed_commit": landed_commit,
584
+ "reasons": list(reasons),
585
+ }
586
+
587
+
588
+ def _reconcile_task(project: Project, task: Task) -> Dict[str, object]:
589
+ report = _recovery_reports(project)[task.task_id]
590
+ verdict = str(report.get("verdict", "block"))
591
+ rejected = report.get("rejected")
592
+ candidates: List[Mapping[str, object]] = []
593
+ if isinstance(rejected, list):
594
+ candidates.extend(item for item in rejected if isinstance(item, Mapping))
595
+ commit = report.get("commit")
596
+ if isinstance(commit, str):
597
+ candidates.append(
598
+ {
599
+ "commit": commit,
600
+ "base": report.get("base"),
601
+ "valid": verdict in LANDED_VERDICTS,
602
+ }
603
+ )
604
+ if verdict in LANDED_VERDICTS:
605
+ return _reconcile_result(
606
+ project,
607
+ task,
608
+ LANDED_VERDICTS[verdict],
609
+ candidates,
610
+ (),
611
+ landed_commit=commit if isinstance(commit, str) else None,
612
+ )
613
+ reason = str(report.get("reason", verdict))
614
+ classification = {
615
+ "none": "resumable",
616
+ "resume": "resumable",
617
+ "reconcile": "reconcile",
618
+ "block": "blocked",
619
+ }.get(verdict, "blocked")
620
+ return _reconcile_result(project, task, classification, candidates, (reason,))
621
+
622
+
623
+ def reconcile(
624
+ repo: str, task_id: str, project_dir: str = DEFAULT_PROJECT_DIR
625
+ ) -> Dict[str, object]:
626
+ """Classify recorded task state against canonical first-parent commits."""
627
+
628
+ if not TASK_ID_RE.fullmatch(task_id):
629
+ raise BuildStateError("invalid-task-id", f"invalid task id: {task_id}")
630
+ project = _load_project(repo, project_dir, ("active", "blocked", "done"))
631
+ task = next((item for item in project.tasks if item.task_id == task_id), None)
632
+ if task is None:
633
+ raise BuildStateError("unknown-task", f"task file is missing: {task_id}")
634
+ return _reconcile_task(project, task)
635
+
636
+
637
+ def verify_landed_tasks(
638
+ repo: str,
639
+ project_dir: str,
640
+ head: str,
641
+ canonical_project_dir: str = DEFAULT_PROJECT_DIR,
642
+ ) -> Dict[str, object]:
643
+ """Verify archived task records against canonical first-parent landings."""
644
+
645
+ repository = _repo_root(repo)
646
+ artifact_dir, artifact_path = _project_path(repository, project_dir)
647
+ canonical_dir, _ = _project_path(repository, canonical_project_dir)
648
+ if not FULL_SHA_RE.fullmatch(head) or not _commit_resolves(repository, head):
649
+ raise BuildStateError(
650
+ "invalid-head", "--head must be a full, existing commit SHA"
651
+ )
652
+ if _git_output(repository, "rev-parse", f"{head}^{{commit}}") != head:
653
+ raise BuildStateError("invalid-head", "--head must resolve to itself")
654
+
655
+ tasks = _load_tasks(
656
+ repository,
657
+ artifact_dir,
658
+ artifact_path,
659
+ canonical_project_dir=canonical_dir,
660
+ )
661
+ for task in tasks:
662
+ if task.status != "done":
663
+ _invalid_state(task, "task must have status: done before shipping")
664
+ try:
665
+ proven = verify_landed_task_files(
666
+ repository,
667
+ [artifact_path / "tasks" / Path(task.task_file).name for task in tasks],
668
+ str(PurePosixPath(canonical_dir) / "tasks"),
669
+ head,
670
+ )
671
+ except IsolationError as error:
672
+ raise BuildStateError("invalid-task-state", str(error)) from error
673
+
674
+ reports = {
675
+ str(report["task_id"]): report
676
+ for report in proven["tasks"]
677
+ if isinstance(report, Mapping) and isinstance(report.get("task_id"), str)
678
+ }
679
+ evidence = []
680
+ for task in tasks:
681
+ report = reports.get(task.task_id)
682
+ if report is None or report.get("verdict") not in LANDED_VERDICTS:
683
+ _invalid_state(task, "task landing is not canonical")
684
+ commit = report.get("commit")
685
+ evidence.append(
686
+ {
687
+ "command": "reconcile",
688
+ "classification": LANDED_VERDICTS[str(report["verdict"])],
689
+ "task": {
690
+ "id": task.task_id,
691
+ "status": task.status,
692
+ "task_file": task.task_file,
693
+ "base": task.base,
694
+ },
695
+ "history": {
696
+ "branch": proven["bound_branch"],
697
+ "head": head,
698
+ "subject": task_commit_subject(task.task_id, task.title),
699
+ "candidates": [
700
+ {
701
+ "commit": commit,
702
+ "base": report.get("base"),
703
+ "valid": True,
704
+ }
705
+ ],
706
+ },
707
+ "landed_commit": commit,
708
+ "reasons": [],
709
+ }
710
+ )
711
+ return {
712
+ "command": "verify-landed",
713
+ "project_dir": artifact_dir,
714
+ "canonical_project_dir": canonical_dir,
715
+ "head": head,
716
+ "tasks": evidence,
717
+ }
718
+
719
+
720
+ def _ledger_path(repo: Path) -> Path:
721
+ path = repo.joinpath(*PurePosixPath(VERIFY_LEDGER_PATH).parts)
722
+ if path.is_symlink() or (path.exists() and not path.is_file()):
723
+ raise BuildStateError("invalid-ledger", f"verify ledger is not a regular file: {path}")
724
+ return path
725
+
726
+
727
+ def _ledger_key(repo: Path, command: str, commit: str) -> Tuple[str, str]:
728
+ if not command.strip():
729
+ raise BuildStateError("invalid-command", "--command must not be empty")
730
+ if not FULL_SHA_RE.fullmatch(commit) or not _commit_resolves(repo, commit):
731
+ raise BuildStateError("invalid-commit", "--commit must be a full, existing commit SHA")
732
+ return command, commit
733
+
734
+
735
+ def _ledger_entries(path: Path) -> List[Dict[str, object]]:
736
+ try:
737
+ return _common.verify_ledger_entries(path)
738
+ except ValueError as error:
739
+ raise BuildStateError("invalid-ledger", str(error)) from error
740
+
741
+
742
+ def verify_record(repo: str, command: str, commit: str, result: str,
743
+ execution: Optional[Dict[str, object]] = None) -> Dict[str, object]:
744
+ """Append one verify run (command, commit, result, timestamp) to the ledger."""
745
+
746
+ repository = _repo_root(repo)
747
+ if result not in VERIFY_RESULTS:
748
+ raise BuildStateError("invalid-result", "--result must be pass or fail")
749
+ normalized, commit = _ledger_key(repository, command, commit)
750
+ path = _ledger_path(repository)
751
+ _ledger_entries(path)
752
+ entry = {
753
+ "schema": _common.VERIFY_LEDGER_SCHEMA,
754
+ "command": normalized,
755
+ "commit": commit,
756
+ "result": result,
757
+ "recorded_at": datetime.now(timezone.utc).isoformat(timespec="seconds"),
758
+ }
759
+ if execution is not None:
760
+ if (not isinstance(execution.get("stdout"), str)
761
+ or not isinstance(execution.get("stderr"), str)
762
+ or type(execution.get("exit_code")) is not int
763
+ or (execution["exit_code"] == 0) != (result == "pass")):
764
+ raise BuildStateError("invalid-execution", "verification output and result disagree")
765
+ entry["execution"] = execution
766
+ path.parent.mkdir(parents=True, exist_ok=True)
767
+ previous = path.read_text(encoding="utf-8") if path.exists() else ""
768
+ _common.atomic_write(path, previous + json.dumps(entry, sort_keys=True) + "\n")
769
+ return {"command": "verify-record", "ledger": VERIFY_LEDGER_PATH, "entry": entry}
770
+
771
+
772
+ def verify_lookup(repo: str, command: str, commit: str) -> Dict[str, object]:
773
+ """Return the latest recorded run of this command at this commit, or a miss."""
774
+
775
+ repository = _repo_root(repo)
776
+ normalized, commit = _ledger_key(repository, command, commit)
777
+ entry = _common.latest_verify_entry(_ledger_entries(_ledger_path(repository)), normalized, commit)
778
+ return {
779
+ "command": "verify-lookup",
780
+ "ledger": VERIFY_LEDGER_PATH,
781
+ "hit": entry is not None,
782
+ "reuse": entry is not None and entry["result"] == "pass",
783
+ "entry": entry,
784
+ }
785
+
786
+
787
+ def _parser() -> argparse.ArgumentParser:
788
+ parser = argparse.ArgumentParser(description=__doc__)
789
+ subcommands = parser.add_subparsers(dest="command", required=True)
790
+ ready_parser = subcommands.add_parser("ready", help="list dependency-ready tasks")
791
+ ready_parser.add_argument("--repo", required=True)
792
+ ready_parser.add_argument("--project-dir", default=DEFAULT_PROJECT_DIR)
793
+ reconcile_parser = subcommands.add_parser(
794
+ "reconcile", help="classify one task against first-parent landing evidence"
795
+ )
796
+ reconcile_parser.add_argument("--repo", required=True)
797
+ reconcile_parser.add_argument("--task-id", required=True)
798
+ reconcile_parser.add_argument("--project-dir", default=DEFAULT_PROJECT_DIR)
799
+ verify_parser = subcommands.add_parser(
800
+ "verify-landed", help="verify archived tasks against canonical landings"
801
+ )
802
+ verify_parser.add_argument("--repo", required=True)
803
+ verify_parser.add_argument("--project-dir", required=True)
804
+ verify_parser.add_argument("--head", required=True)
805
+ verify_parser.add_argument("--canonical-project-dir", default=DEFAULT_PROJECT_DIR)
806
+ record_parser = subcommands.add_parser(
807
+ "verify-record", help="append one verify run to the ledger"
808
+ )
809
+ record_parser.add_argument("--repo", required=True)
810
+ record_parser.add_argument("--command", required=True, dest="verify_command")
811
+ record_parser.add_argument("--commit", required=True)
812
+ record_parser.add_argument("--result", required=True, choices=VERIFY_RESULTS)
813
+ lookup_parser = subcommands.add_parser(
814
+ "verify-lookup", help="find the recorded run of a command at a commit"
815
+ )
816
+ lookup_parser.add_argument("--repo", required=True)
817
+ lookup_parser.add_argument("--command", required=True, dest="verify_command")
818
+ lookup_parser.add_argument("--commit", required=True)
819
+ return parser
820
+
821
+
822
+ def main(argv: Optional[Sequence[str]] = None) -> int:
823
+ arguments = _parser().parse_args(argv)
824
+ try:
825
+ if arguments.command == "ready":
826
+ result = ready(arguments.repo, arguments.project_dir)
827
+ elif arguments.command == "reconcile":
828
+ result = reconcile(arguments.repo, arguments.task_id, arguments.project_dir)
829
+ elif arguments.command == "verify-record":
830
+ result = verify_record(
831
+ arguments.repo, arguments.verify_command, arguments.commit, arguments.result
832
+ )
833
+ elif arguments.command == "verify-lookup":
834
+ result = verify_lookup(arguments.repo, arguments.verify_command, arguments.commit)
835
+ else:
836
+ result = verify_landed_tasks(
837
+ arguments.repo,
838
+ arguments.project_dir,
839
+ arguments.head,
840
+ arguments.canonical_project_dir,
841
+ )
842
+ except BuildStateError as error:
843
+ result = {
844
+ "command": arguments.command,
845
+ "error": {"code": error.code, "message": str(error), "details": error.details},
846
+ }
847
+ print(json.dumps(result, indent=2, sort_keys=True))
848
+ return 1
849
+ print(json.dumps(result, indent=2, sort_keys=True))
850
+ return 0
851
+
852
+
853
+ if __name__ == "__main__":
854
+ sys.exit(main())