@opengsd/gsd-path 1.4.0 → 1.5.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 (347) hide show
  1. package/CHANGELOG.md +325 -344
  2. package/DOCS.md +12 -2
  3. package/FULL.md +3 -2
  4. package/HOOKS.md +95 -8
  5. package/QUICK.md +1 -1
  6. package/README.md +78 -110
  7. package/RUNTIME.md +1 -1
  8. package/UPDATE.md +4 -3
  9. package/WORKFLOW.md +22 -1
  10. package/package.json +5 -2
  11. package/platforms/shared-agents/dispatch.md +13 -0
  12. package/scripts/_common.py +149 -1
  13. package/scripts/archive_milestone.py +14 -13
  14. package/scripts/build_recovery.py +96 -31
  15. package/scripts/build_state.py +79 -3
  16. package/scripts/check_docs_audit.py +77 -4
  17. package/scripts/check_handoffs.py +546 -30
  18. package/scripts/check_task_briefs.py +279 -23
  19. package/scripts/detect_project.py +243 -18
  20. package/scripts/discussion_validate.py +86 -29
  21. package/scripts/dispatch_driver.py +392 -22
  22. package/scripts/git_guard.py +3 -3
  23. package/scripts/guard_hook.py +1416 -165
  24. package/scripts/install.mjs +23 -8
  25. package/scripts/install.py +49 -9
  26. package/scripts/integration.py +146 -1
  27. package/scripts/isolation.py +392 -60
  28. package/scripts/loop_run.py +16 -4
  29. package/scripts/members.py +11 -9
  30. package/scripts/path_config.py +3 -3
  31. package/scripts/pipeline_git.py +63 -12
  32. package/scripts/pipeline_state.py +111 -33
  33. package/scripts/pipeline_undo.py +101 -8
  34. package/scripts/runtime_store.py +20 -2
  35. package/scripts/skill-resources.json +9 -2
  36. package/scripts/state_checkpoint.py +9 -27
  37. package/scripts/status_runtime.py +22 -4
  38. package/scripts/task_context.py +12 -3
  39. package/scripts/wizard.mjs +1 -0
  40. package/scripts/workflow_run.py +5 -2
  41. package/scripts/worktree_paths.py +54 -2
  42. package/skills/gsd-path/DEFINE.md +28 -3
  43. package/skills/gsd-path/PLAN.md +11 -3
  44. package/skills/gsd-path/RESEARCH.md +11 -0
  45. package/skills/gsd-path/SHIP.md +6 -1
  46. package/skills/gsd-path/references/build-native.md +16 -1
  47. package/skills/gsd-path/references/dispatch.md +13 -0
  48. package/skills/gsd-path/references/docs-auditor.md +5 -0
  49. package/skills/gsd-path/references/planner.md +14 -4
  50. package/skills/gsd-path/references/reviewer-final.md +20 -2
  51. package/skills/gsd-path/references/reviewer-wave.md +15 -4
  52. package/skills/gsd-path/references/reviewer.md +3 -1
  53. package/skills/gsd-path/references/spec-probes.md +150 -0
  54. package/skills/gsd-path/scripts/_common.py +149 -1
  55. package/skills/gsd-path/scripts/archive_milestone.py +14 -13
  56. package/skills/gsd-path/scripts/build_recovery.py +96 -31
  57. package/skills/gsd-path/scripts/build_state.py +79 -3
  58. package/skills/gsd-path/scripts/check_docs_audit.py +77 -4
  59. package/skills/gsd-path/scripts/check_handoffs.py +546 -30
  60. package/skills/gsd-path/scripts/check_task_briefs.py +279 -23
  61. package/skills/gsd-path/scripts/detect_project.py +243 -18
  62. package/skills/gsd-path/scripts/discussion_validate.py +86 -29
  63. package/skills/gsd-path/scripts/dispatch_driver.py +392 -22
  64. package/skills/gsd-path/scripts/git_guard.py +3 -3
  65. package/skills/gsd-path/scripts/integration.py +146 -1
  66. package/skills/gsd-path/scripts/isolation.py +392 -60
  67. package/skills/gsd-path/scripts/loop_run.py +16 -4
  68. package/skills/gsd-path/scripts/members.py +11 -9
  69. package/skills/gsd-path/scripts/path_config.py +3 -3
  70. package/skills/gsd-path/scripts/pipeline_git.py +63 -12
  71. package/skills/gsd-path/scripts/pipeline_state.py +111 -33
  72. package/skills/gsd-path/scripts/state_checkpoint.py +9 -27
  73. package/skills/gsd-path/scripts/status_runtime.py +22 -4
  74. package/skills/gsd-path/scripts/task_context.py +12 -3
  75. package/skills/gsd-path/scripts/workflow_run.py +5 -2
  76. package/skills/gsd-path/scripts/worktree_paths.py +54 -2
  77. package/skills/gsd-path/templates/docs-audit.md +5 -1
  78. package/skills/gsd-path/templates/final-review.md +11 -2
  79. package/skills/gsd-path/templates/intent.md +30 -0
  80. package/skills/gsd-path/templates/patch-findings.md +3 -3
  81. package/skills/gsd-path/templates/plan.md +17 -2
  82. package/skills/gsd-path/templates/research-handoff.md +14 -3
  83. package/skills/gsd-path/templates/state.md +1 -1
  84. package/skills/gsd-path/templates/wave-review.md +16 -6
  85. package/skills/gsd-path-build/references/build-native.md +16 -1
  86. package/skills/gsd-path-build/references/dispatch.md +13 -0
  87. package/skills/gsd-path-build/references/reviewer-final.md +20 -2
  88. package/skills/gsd-path-build/references/reviewer-wave.md +15 -4
  89. package/skills/gsd-path-build/references/reviewer.md +3 -1
  90. package/skills/gsd-path-build/references/spec-probes.md +150 -0
  91. package/skills/gsd-path-build/scripts/_common.py +149 -1
  92. package/skills/gsd-path-build/scripts/archive_milestone.py +14 -13
  93. package/skills/gsd-path-build/scripts/build_recovery.py +96 -31
  94. package/skills/gsd-path-build/scripts/build_state.py +79 -3
  95. package/skills/gsd-path-build/scripts/check_handoffs.py +546 -30
  96. package/skills/gsd-path-build/scripts/check_task_briefs.py +279 -23
  97. package/skills/gsd-path-build/scripts/detect_project.py +243 -18
  98. package/skills/gsd-path-build/scripts/discussion_validate.py +86 -29
  99. package/skills/gsd-path-build/scripts/dispatch_driver.py +392 -22
  100. package/skills/gsd-path-build/scripts/integration.py +146 -1
  101. package/skills/gsd-path-build/scripts/isolation.py +392 -60
  102. package/skills/gsd-path-build/scripts/loop_run.py +16 -4
  103. package/skills/gsd-path-build/scripts/members.py +11 -9
  104. package/skills/gsd-path-build/scripts/path_config.py +3 -3
  105. package/skills/gsd-path-build/scripts/pipeline_git.py +63 -12
  106. package/skills/gsd-path-build/scripts/pipeline_state.py +111 -33
  107. package/skills/gsd-path-build/scripts/state_checkpoint.py +9 -27
  108. package/skills/gsd-path-build/scripts/task_context.py +12 -3
  109. package/skills/gsd-path-build/scripts/workflow_run.py +5 -2
  110. package/skills/gsd-path-build/scripts/worktree_paths.py +54 -2
  111. package/skills/gsd-path-build/templates/wave-review.md +16 -6
  112. package/skills/gsd-path-decide/references/dispatch.md +13 -0
  113. package/skills/gsd-path-decide/scripts/_common.py +149 -1
  114. package/skills/gsd-path-decide/scripts/archive_milestone.py +14 -13
  115. package/skills/gsd-path-decide/scripts/build_recovery.py +96 -31
  116. package/skills/gsd-path-decide/scripts/check_handoffs.py +546 -30
  117. package/skills/gsd-path-decide/scripts/check_task_briefs.py +279 -23
  118. package/skills/gsd-path-decide/scripts/detect_project.py +243 -18
  119. package/skills/gsd-path-decide/scripts/discussion_validate.py +86 -29
  120. package/skills/gsd-path-decide/scripts/integration.py +146 -1
  121. package/skills/gsd-path-decide/scripts/isolation.py +392 -60
  122. package/skills/gsd-path-decide/scripts/loop_run.py +16 -4
  123. package/skills/gsd-path-decide/scripts/members.py +11 -9
  124. package/skills/gsd-path-decide/scripts/path_config.py +3 -3
  125. package/skills/gsd-path-decide/scripts/pipeline_git.py +63 -12
  126. package/skills/gsd-path-decide/scripts/pipeline_state.py +111 -33
  127. package/skills/gsd-path-decide/scripts/state_checkpoint.py +9 -27
  128. package/skills/gsd-path-decide/scripts/worktree_paths.py +54 -2
  129. package/skills/gsd-path-define/SKILL.md +28 -3
  130. package/skills/gsd-path-define/references/spec-probes.md +150 -0
  131. package/skills/gsd-path-define/scripts/_common.py +149 -1
  132. package/skills/gsd-path-define/scripts/archive_milestone.py +14 -13
  133. package/skills/gsd-path-define/scripts/build_recovery.py +96 -31
  134. package/skills/gsd-path-define/scripts/check_handoffs.py +546 -30
  135. package/skills/gsd-path-define/scripts/check_task_briefs.py +279 -23
  136. package/skills/gsd-path-define/scripts/detect_project.py +243 -18
  137. package/skills/gsd-path-define/scripts/discussion_validate.py +86 -29
  138. package/skills/gsd-path-define/scripts/integration.py +146 -1
  139. package/skills/gsd-path-define/scripts/isolation.py +392 -60
  140. package/skills/gsd-path-define/scripts/loop_run.py +16 -4
  141. package/skills/gsd-path-define/scripts/members.py +11 -9
  142. package/skills/gsd-path-define/scripts/path_config.py +3 -3
  143. package/skills/gsd-path-define/scripts/pipeline_git.py +63 -12
  144. package/skills/gsd-path-define/scripts/pipeline_state.py +111 -33
  145. package/skills/gsd-path-define/scripts/state_checkpoint.py +9 -27
  146. package/skills/gsd-path-define/scripts/worktree_paths.py +54 -2
  147. package/skills/gsd-path-define/templates/intent.md +30 -0
  148. package/skills/gsd-path-define/templates/state.md +1 -1
  149. package/skills/gsd-path-discuss/scripts/_common.py +149 -1
  150. package/skills/gsd-path-discuss/scripts/loop_run.py +16 -4
  151. package/skills/gsd-path-discuss/scripts/path_config.py +3 -3
  152. package/skills/gsd-path-discuss/scripts/worktree_paths.py +54 -2
  153. package/skills/gsd-path-docs-audit/references/dispatch.md +13 -0
  154. package/skills/gsd-path-docs-audit/references/docs-auditor.md +5 -0
  155. package/skills/gsd-path-docs-audit/scripts/_common.py +149 -1
  156. package/skills/gsd-path-docs-audit/scripts/archive_milestone.py +14 -13
  157. package/skills/gsd-path-docs-audit/scripts/build_recovery.py +96 -31
  158. package/skills/gsd-path-docs-audit/scripts/check_docs_audit.py +77 -4
  159. package/skills/gsd-path-docs-audit/scripts/check_handoffs.py +546 -30
  160. package/skills/gsd-path-docs-audit/scripts/check_task_briefs.py +279 -23
  161. package/skills/gsd-path-docs-audit/scripts/detect_project.py +243 -18
  162. package/skills/gsd-path-docs-audit/scripts/discussion_validate.py +86 -29
  163. package/skills/gsd-path-docs-audit/scripts/integration.py +146 -1
  164. package/skills/gsd-path-docs-audit/scripts/isolation.py +392 -60
  165. package/skills/gsd-path-docs-audit/scripts/loop_run.py +16 -4
  166. package/skills/gsd-path-docs-audit/scripts/members.py +11 -9
  167. package/skills/gsd-path-docs-audit/scripts/path_config.py +3 -3
  168. package/skills/gsd-path-docs-audit/scripts/pipeline_git.py +63 -12
  169. package/skills/gsd-path-docs-audit/scripts/pipeline_state.py +111 -33
  170. package/skills/gsd-path-docs-audit/scripts/state_checkpoint.py +9 -27
  171. package/skills/gsd-path-docs-audit/scripts/worktree_paths.py +54 -2
  172. package/skills/gsd-path-docs-audit/templates/docs-audit.md +5 -1
  173. package/skills/gsd-path-forensics/scripts/_common.py +149 -1
  174. package/skills/gsd-path-forensics/scripts/loop_run.py +16 -4
  175. package/skills/gsd-path-forensics/scripts/path_config.py +3 -3
  176. package/skills/gsd-path-forensics/scripts/pipeline_undo.py +101 -8
  177. package/skills/gsd-path-forensics/scripts/worktree_paths.py +54 -2
  178. package/skills/gsd-path-inspect/references/dispatch.md +13 -0
  179. package/skills/gsd-path-inspect/references/docs-auditor.md +5 -0
  180. package/skills/gsd-path-inspect/scripts/_common.py +149 -1
  181. package/skills/gsd-path-inspect/scripts/archive_milestone.py +14 -13
  182. package/skills/gsd-path-inspect/scripts/build_recovery.py +96 -31
  183. package/skills/gsd-path-inspect/scripts/check_docs_audit.py +77 -4
  184. package/skills/gsd-path-inspect/scripts/check_handoffs.py +546 -30
  185. package/skills/gsd-path-inspect/scripts/check_task_briefs.py +279 -23
  186. package/skills/gsd-path-inspect/scripts/detect_project.py +243 -18
  187. package/skills/gsd-path-inspect/scripts/discussion_validate.py +86 -29
  188. package/skills/gsd-path-inspect/scripts/integration.py +146 -1
  189. package/skills/gsd-path-inspect/scripts/isolation.py +392 -60
  190. package/skills/gsd-path-inspect/scripts/loop_run.py +16 -4
  191. package/skills/gsd-path-inspect/scripts/members.py +11 -9
  192. package/skills/gsd-path-inspect/scripts/path_config.py +3 -3
  193. package/skills/gsd-path-inspect/scripts/pipeline_git.py +63 -12
  194. package/skills/gsd-path-inspect/scripts/pipeline_state.py +111 -33
  195. package/skills/gsd-path-inspect/scripts/state_checkpoint.py +9 -27
  196. package/skills/gsd-path-inspect/scripts/workflow_run.py +5 -2
  197. package/skills/gsd-path-inspect/scripts/worktree_paths.py +54 -2
  198. package/skills/gsd-path-inspect/templates/docs-audit.md +5 -1
  199. package/skills/gsd-path-inspect/templates/state.md +1 -1
  200. package/skills/gsd-path-loop/scripts/_common.py +149 -1
  201. package/skills/gsd-path-loop/scripts/loop_run.py +16 -4
  202. package/skills/gsd-path-loop/scripts/path_config.py +3 -3
  203. package/skills/gsd-path-loop/scripts/worktree_paths.py +54 -2
  204. package/skills/gsd-path-migrate/scripts/_common.py +149 -1
  205. package/skills/gsd-path-migrate/scripts/loop_run.py +16 -4
  206. package/skills/gsd-path-migrate/scripts/path_config.py +3 -3
  207. package/skills/gsd-path-plan/SKILL.md +11 -3
  208. package/skills/gsd-path-plan/references/dispatch.md +13 -0
  209. package/skills/gsd-path-plan/references/planner.md +14 -4
  210. package/skills/gsd-path-plan/references/spec-probes.md +150 -0
  211. package/skills/gsd-path-plan/scripts/_common.py +149 -1
  212. package/skills/gsd-path-plan/scripts/archive_milestone.py +14 -13
  213. package/skills/gsd-path-plan/scripts/build_recovery.py +96 -31
  214. package/skills/gsd-path-plan/scripts/check_handoffs.py +546 -30
  215. package/skills/gsd-path-plan/scripts/check_task_briefs.py +279 -23
  216. package/skills/gsd-path-plan/scripts/detect_project.py +243 -18
  217. package/skills/gsd-path-plan/scripts/discussion_validate.py +86 -29
  218. package/skills/gsd-path-plan/scripts/integration.py +146 -1
  219. package/skills/gsd-path-plan/scripts/isolation.py +392 -60
  220. package/skills/gsd-path-plan/scripts/loop_run.py +16 -4
  221. package/skills/gsd-path-plan/scripts/members.py +11 -9
  222. package/skills/gsd-path-plan/scripts/path_config.py +3 -3
  223. package/skills/gsd-path-plan/scripts/pipeline_git.py +63 -12
  224. package/skills/gsd-path-plan/scripts/pipeline_state.py +111 -33
  225. package/skills/gsd-path-plan/scripts/state_checkpoint.py +9 -27
  226. package/skills/gsd-path-plan/scripts/workflow_run.py +5 -2
  227. package/skills/gsd-path-plan/scripts/worktree_paths.py +54 -2
  228. package/skills/gsd-path-plan/templates/patch-findings.md +3 -3
  229. package/skills/gsd-path-plan/templates/plan.md +17 -2
  230. package/skills/gsd-path-research/SKILL.md +11 -0
  231. package/skills/gsd-path-research/references/dispatch.md +13 -0
  232. package/skills/gsd-path-research/scripts/_common.py +149 -1
  233. package/skills/gsd-path-research/scripts/archive_milestone.py +14 -13
  234. package/skills/gsd-path-research/scripts/build_recovery.py +96 -31
  235. package/skills/gsd-path-research/scripts/check_handoffs.py +546 -30
  236. package/skills/gsd-path-research/scripts/check_task_briefs.py +279 -23
  237. package/skills/gsd-path-research/scripts/detect_project.py +243 -18
  238. package/skills/gsd-path-research/scripts/discussion_validate.py +86 -29
  239. package/skills/gsd-path-research/scripts/integration.py +146 -1
  240. package/skills/gsd-path-research/scripts/isolation.py +392 -60
  241. package/skills/gsd-path-research/scripts/loop_run.py +16 -4
  242. package/skills/gsd-path-research/scripts/members.py +11 -9
  243. package/skills/gsd-path-research/scripts/path_config.py +3 -3
  244. package/skills/gsd-path-research/scripts/pipeline_git.py +63 -12
  245. package/skills/gsd-path-research/scripts/pipeline_state.py +111 -33
  246. package/skills/gsd-path-research/scripts/state_checkpoint.py +9 -27
  247. package/skills/gsd-path-research/scripts/worktree_paths.py +54 -2
  248. package/skills/gsd-path-research/templates/research-handoff.md +14 -3
  249. package/skills/gsd-path-roadmap/references/dispatch.md +13 -0
  250. package/skills/gsd-path-roadmap/scripts/_common.py +149 -1
  251. package/skills/gsd-path-roadmap/scripts/archive_milestone.py +14 -13
  252. package/skills/gsd-path-roadmap/scripts/build_recovery.py +96 -31
  253. package/skills/gsd-path-roadmap/scripts/check_handoffs.py +546 -30
  254. package/skills/gsd-path-roadmap/scripts/check_task_briefs.py +279 -23
  255. package/skills/gsd-path-roadmap/scripts/detect_project.py +243 -18
  256. package/skills/gsd-path-roadmap/scripts/discussion_validate.py +86 -29
  257. package/skills/gsd-path-roadmap/scripts/integration.py +146 -1
  258. package/skills/gsd-path-roadmap/scripts/isolation.py +392 -60
  259. package/skills/gsd-path-roadmap/scripts/loop_run.py +16 -4
  260. package/skills/gsd-path-roadmap/scripts/members.py +11 -9
  261. package/skills/gsd-path-roadmap/scripts/path_config.py +3 -3
  262. package/skills/gsd-path-roadmap/scripts/pipeline_git.py +63 -12
  263. package/skills/gsd-path-roadmap/scripts/pipeline_state.py +111 -33
  264. package/skills/gsd-path-roadmap/scripts/state_checkpoint.py +9 -27
  265. package/skills/gsd-path-roadmap/scripts/worktree_paths.py +54 -2
  266. package/skills/gsd-path-ship/SKILL.md +6 -1
  267. package/skills/gsd-path-ship/references/dispatch.md +13 -0
  268. package/skills/gsd-path-ship/references/reviewer-final.md +20 -2
  269. package/skills/gsd-path-ship/references/reviewer-wave.md +15 -4
  270. package/skills/gsd-path-ship/references/reviewer.md +3 -1
  271. package/skills/gsd-path-ship/references/spec-probes.md +150 -0
  272. package/skills/gsd-path-ship/scripts/_common.py +149 -1
  273. package/skills/gsd-path-ship/scripts/archive_milestone.py +14 -13
  274. package/skills/gsd-path-ship/scripts/build_recovery.py +96 -31
  275. package/skills/gsd-path-ship/scripts/build_state.py +79 -3
  276. package/skills/gsd-path-ship/scripts/check_handoffs.py +546 -30
  277. package/skills/gsd-path-ship/scripts/check_task_briefs.py +279 -23
  278. package/skills/gsd-path-ship/scripts/detect_project.py +243 -18
  279. package/skills/gsd-path-ship/scripts/discussion_validate.py +86 -29
  280. package/skills/gsd-path-ship/scripts/integration.py +146 -1
  281. package/skills/gsd-path-ship/scripts/isolation.py +392 -60
  282. package/skills/gsd-path-ship/scripts/loop_run.py +16 -4
  283. package/skills/gsd-path-ship/scripts/members.py +11 -9
  284. package/skills/gsd-path-ship/scripts/path_config.py +3 -3
  285. package/skills/gsd-path-ship/scripts/pipeline_git.py +63 -12
  286. package/skills/gsd-path-ship/scripts/pipeline_state.py +111 -33
  287. package/skills/gsd-path-ship/scripts/state_checkpoint.py +9 -27
  288. package/skills/gsd-path-ship/scripts/workflow_run.py +5 -2
  289. package/skills/gsd-path-ship/scripts/worktree_paths.py +54 -2
  290. package/skills/gsd-path-ship/templates/final-review.md +11 -2
  291. package/skills/gsd-path-ship/templates/patch-findings.md +3 -3
  292. package/skills/gsd-path-ship/templates/wave-review.md +16 -6
  293. package/skills/gsd-path-undo/scripts/_common.py +149 -1
  294. package/skills/gsd-path-undo/scripts/loop_run.py +16 -4
  295. package/skills/gsd-path-undo/scripts/path_config.py +3 -3
  296. package/skills/gsd-path-undo/scripts/pipeline_undo.py +101 -8
  297. package/skills/gsd-path-undo/scripts/worktree_paths.py +54 -2
  298. package/skills/path/DEFINE.md +28 -3
  299. package/skills/path/PLAN.md +11 -3
  300. package/skills/path/RESEARCH.md +11 -0
  301. package/skills/path/SHIP.md +6 -1
  302. package/skills/path/references/build-native.md +16 -1
  303. package/skills/path/references/dispatch.md +13 -0
  304. package/skills/path/references/docs-auditor.md +5 -0
  305. package/skills/path/references/planner.md +14 -4
  306. package/skills/path/references/reviewer-final.md +20 -2
  307. package/skills/path/references/reviewer-wave.md +15 -4
  308. package/skills/path/references/reviewer.md +3 -1
  309. package/skills/path/references/spec-probes.md +150 -0
  310. package/skills/path/scripts/_common.py +149 -1
  311. package/skills/path/scripts/archive_milestone.py +14 -13
  312. package/skills/path/scripts/build_recovery.py +96 -31
  313. package/skills/path/scripts/build_state.py +79 -3
  314. package/skills/path/scripts/check_docs_audit.py +77 -4
  315. package/skills/path/scripts/check_handoffs.py +546 -30
  316. package/skills/path/scripts/check_task_briefs.py +279 -23
  317. package/skills/path/scripts/detect_project.py +243 -18
  318. package/skills/path/scripts/discussion_validate.py +86 -29
  319. package/skills/path/scripts/dispatch_driver.py +392 -22
  320. package/skills/path/scripts/git_guard.py +3 -3
  321. package/skills/path/scripts/integration.py +146 -1
  322. package/skills/path/scripts/isolation.py +392 -60
  323. package/skills/path/scripts/loop_run.py +16 -4
  324. package/skills/path/scripts/members.py +11 -9
  325. package/skills/path/scripts/path_config.py +3 -3
  326. package/skills/path/scripts/pipeline_git.py +63 -12
  327. package/skills/path/scripts/pipeline_state.py +111 -33
  328. package/skills/path/scripts/state_checkpoint.py +9 -27
  329. package/skills/path/scripts/status_runtime.py +22 -4
  330. package/skills/path/scripts/task_context.py +12 -3
  331. package/skills/path/scripts/workflow_run.py +5 -2
  332. package/skills/path/scripts/worktree_paths.py +54 -2
  333. package/skills/path/templates/docs-audit.md +5 -1
  334. package/skills/path/templates/final-review.md +11 -2
  335. package/skills/path/templates/intent.md +30 -0
  336. package/skills/path/templates/patch-findings.md +3 -3
  337. package/skills/path/templates/plan.md +17 -2
  338. package/skills/path/templates/research-handoff.md +14 -3
  339. package/skills/path/templates/state.md +1 -1
  340. package/skills/path/templates/wave-review.md +16 -6
  341. package/skills/gsd-path/scripts/__pycache__/_common.cpython-312.pyc +0 -0
  342. package/skills/gsd-path/scripts/__pycache__/check_handoffs.cpython-312.pyc +0 -0
  343. package/skills/gsd-path-build/scripts/__pycache__/_common.cpython-312.pyc +0 -0
  344. package/skills/gsd-path-build/scripts/__pycache__/check_handoffs.cpython-312.pyc +0 -0
  345. package/skills/gsd-path-inspect/scripts/__pycache__/_common.cpython-312.pyc +0 -0
  346. package/skills/gsd-path-plan/scripts/__pycache__/_common.cpython-312.pyc +0 -0
  347. package/skills/gsd-path-ship/scripts/__pycache__/_common.cpython-312.pyc +0 -0
@@ -55,10 +55,22 @@ try:
55
55
  fcntl.flock(fd, fcntl.LOCK_EX)
56
56
 
57
57
  except ImportError: # Windows does not provide POSIX file locks.
58
+ import errno
58
59
  import msvcrt
60
+ import time
59
61
 
60
62
  def lock_exclusive(fd: int) -> None:
61
- msvcrt.locking(fd, msvcrt.LK_LOCK, 1)
63
+ delay = 0.01
64
+ while True:
65
+ try:
66
+ os.lseek(fd, 0, os.SEEK_SET)
67
+ msvcrt.locking(fd, msvcrt.LK_NBLCK, 1)
68
+ return
69
+ except OSError as error:
70
+ if error.errno not in (errno.EACCES, errno.EAGAIN, errno.EDEADLK):
71
+ raise
72
+ time.sleep(delay)
73
+ delay = min(delay * 2, 0.25)
62
74
 
63
75
 
64
76
  IGNORE_DIRS = {
@@ -217,16 +229,20 @@ HTML_BLOCK_START = re.compile(
217
229
  re.IGNORECASE,
218
230
  )
219
231
  REPARSE_NAME_SURROGATE = 0x20000000
220
- ANCHORED_EVIDENCE_SUPPORTED = (
232
+ _POSIX_ANCHORED_EVIDENCE_SUPPORTED = (
221
233
  hasattr(os, "O_DIRECTORY")
222
234
  and hasattr(os, "O_NOFOLLOW")
223
235
  and os.open in os.supports_dir_fd
224
236
  and os.stat in os.supports_dir_fd
225
237
  and os.stat in os.supports_follow_symlinks
226
238
  )
239
+ WINDOWS_ANCHORED_EVIDENCE_SUPPORTED = os.name == "nt"
240
+ ANCHORED_EVIDENCE_SUPPORTED = (
241
+ _POSIX_ANCHORED_EVIDENCE_SUPPORTED or WINDOWS_ANCHORED_EVIDENCE_SUPPORTED
242
+ )
227
243
  LISTDIR_DIR_FD_SUPPORTED = os.listdir in getattr(os, "supports_fd", ())
228
244
  ANCHORED_STATE_CREATE_SUPPORTED = (
229
- ANCHORED_EVIDENCE_SUPPORTED
245
+ _POSIX_ANCHORED_EVIDENCE_SUPPORTED
230
246
  and LISTDIR_DIR_FD_SUPPORTED
231
247
  and os.mkdir in getattr(os, "supports_dir_fd", ())
232
248
  and os.unlink in getattr(os, "supports_dir_fd", ())
@@ -269,11 +285,12 @@ def is_link_like_status(status: os.stat_result) -> bool:
269
285
  def is_link_like(path: Path, status: os.stat_result) -> bool:
270
286
  if is_link_like_status(status):
271
287
  return True
272
- isjunction = getattr(os.path, "isjunction", None)
273
- if os.name != "nt" or isjunction is None:
288
+ if os.name != "nt":
274
289
  return False
275
290
  try:
276
- return bool(isjunction(path))
291
+ return is_link_like_status(os.lstat(path))
292
+ except FileNotFoundError:
293
+ return False
277
294
  except OSError as error:
278
295
  raise DetectError(f"cannot inspect link-like evidence: {path}: {error}") from error
279
296
 
@@ -332,7 +349,7 @@ def is_managed_pipeline_directory(relative: str, root: Path) -> bool:
332
349
  return is_verified_skill_bundle(relative, root)
333
350
 
334
351
 
335
- def is_verified_skill_bundle_at(relative: str, directory_fd: int) -> bool:
352
+ def is_verified_skill_bundle_at(relative: str, directory_fd: int, root: Path) -> bool:
336
353
  parts = PurePosixPath(relative).parts
337
354
  if not parts:
338
355
  return False
@@ -342,19 +359,45 @@ def is_verified_skill_bundle_at(relative: str, directory_fd: int) -> bool:
342
359
  return False
343
360
  if is_verified_installer_bundle(parts):
344
361
  return True
362
+ if _POSIX_ANCHORED_EVIDENCE_SUPPORTED:
363
+ try:
364
+ status = os.stat(
365
+ "SKILL.md",
366
+ dir_fd=directory_fd,
367
+ follow_symlinks=False,
368
+ )
369
+ except FileNotFoundError:
370
+ return False
371
+ except OSError as error:
372
+ raise DetectError(
373
+ f"cannot inspect managed skill bundle: {relative}: {error}"
374
+ ) from error
375
+ return not is_link_like_status(status) and stat.S_ISREG(status.st_mode)
376
+ skill_fd = None
345
377
  try:
346
- status = os.stat(
378
+ win = get_windows_handles()
379
+ skill_fd = win.open_relative(
380
+ directory_fd,
347
381
  "SKILL.md",
348
- dir_fd=directory_fd,
349
- follow_symlinks=False,
382
+ _FILE_READ_ATTRIBUTES,
383
+ _PIN_SHARE,
384
+ _FILE_OPEN,
385
+ 0,
350
386
  )
387
+ status = os.fstat(skill_fd)
351
388
  except FileNotFoundError:
352
389
  return False
353
390
  except OSError as error:
391
+ if getattr(error, "winerror", None) == 2:
392
+ return False
354
393
  raise DetectError(
355
394
  f"cannot inspect managed skill bundle: {relative}: {error}"
356
395
  ) from error
357
- return not is_link_like_status(status) and stat.S_ISREG(status.st_mode)
396
+ finally:
397
+ if skill_fd is not None:
398
+ os.close(skill_fd)
399
+ skill = root.joinpath(*parts) / "SKILL.md"
400
+ return not is_link_like(skill, status) and stat.S_ISREG(status.st_mode)
358
401
 
359
402
 
360
403
  def is_fixed_managed_pipeline_artifact(relative: str) -> bool:
@@ -423,13 +466,20 @@ def read_regular_evidence(
423
466
  ) from error
424
467
  if not relative.parts:
425
468
  raise DetectError(f"{evidence_name} evidence path is empty: {path}")
426
- if ANCHORED_EVIDENCE_SUPPORTED:
469
+ if _POSIX_ANCHORED_EVIDENCE_SUPPORTED:
427
470
  return read_anchored_evidence(
428
471
  relative,
429
472
  root,
430
473
  missing_ok=missing_ok,
431
474
  evidence_name=evidence_name,
432
475
  )
476
+ if WINDOWS_ANCHORED_EVIDENCE_SUPPORTED:
477
+ return read_windows_anchored_evidence(
478
+ relative,
479
+ root,
480
+ missing_ok=missing_ok,
481
+ evidence_name=evidence_name,
482
+ )
433
483
  return read_lstat_open_evidence(
434
484
  relative,
435
485
  root,
@@ -447,6 +497,17 @@ def directory_flags() -> int:
447
497
  )
448
498
 
449
499
 
500
+ def listdir_anchored(dir_fd: int) -> list[str]:
501
+ """List names through a fresh directory descriptor under dir_fd."""
502
+ fresh = os.open(".", directory_flags(), dir_fd=dir_fd)
503
+ try:
504
+ if not os.path.samestat(os.fstat(fresh), os.fstat(dir_fd)):
505
+ raise DetectError("anchored directory listing lost its identity")
506
+ return os.listdir(fresh)
507
+ finally:
508
+ os.close(fresh)
509
+
510
+
450
511
  def file_flags() -> int:
451
512
  return (
452
513
  os.O_RDONLY
@@ -766,9 +827,12 @@ def raise_walk_error(error: OSError) -> None:
766
827
 
767
828
 
768
829
  def iter_worktree_files(root: Path) -> Iterable[str]:
769
- if ANCHORED_EVIDENCE_SUPPORTED and LISTDIR_DIR_FD_SUPPORTED:
830
+ if _POSIX_ANCHORED_EVIDENCE_SUPPORTED and LISTDIR_DIR_FD_SUPPORTED:
770
831
  yield from iter_worktree_files_anchored(root)
771
832
  return
833
+ if WINDOWS_ANCHORED_EVIDENCE_SUPPORTED:
834
+ yield from iter_worktree_files_windows_anchored(root)
835
+ return
772
836
  for dirpath, dirnames, filenames in os.walk(
773
837
  root,
774
838
  followlinks=False,
@@ -826,7 +890,7 @@ def iter_from_dir_fd(
826
890
  dir_fd: int, relative_dir: str, root: Path
827
891
  ) -> Iterable[str]:
828
892
  try:
829
- names = os.listdir(dir_fd)
893
+ names = listdir_anchored(dir_fd)
830
894
  except OSError as error:
831
895
  raise DetectError(f"cannot traverse filesystem evidence: {error}") from error
832
896
  for name in names:
@@ -862,7 +926,7 @@ def iter_from_dir_fd(
862
926
  raise DetectError(
863
927
  f"filesystem evidence changed while traversing: {relative}"
864
928
  )
865
- if is_verified_skill_bundle_at(relative, child):
929
+ if is_verified_skill_bundle_at(relative, child, root):
866
930
  continue
867
931
  yield from iter_from_dir_fd(child, relative, root)
868
932
  finally:
@@ -1302,7 +1366,7 @@ def close_file_descriptors(
1302
1366
 
1303
1367
  def project_entries(project_fd: int, project: Path) -> set:
1304
1368
  """Names in .project without ignored OS junk (Finder may add .DS_Store at any time)."""
1305
- entries = set(os.listdir(project_fd))
1369
+ entries = set(listdir_anchored(project_fd))
1306
1370
  for name in entries & _common.OS_JUNK_NAMES:
1307
1371
  try:
1308
1372
  status = os.stat(name, dir_fd=project_fd, follow_symlinks=False)
@@ -1413,12 +1477,12 @@ def write_state_anchored(
1413
1477
  follow_symlinks=False,
1414
1478
  )
1415
1479
  except FileNotFoundError as error:
1416
- if "STATE.md" in os.listdir(project_fd):
1480
+ if "STATE.md" in listdir_anchored(project_fd):
1417
1481
  raise DetectError("STATE.md already exists") from error
1418
1482
  raise DetectError("temporary STATE.md disappeared") from error
1419
1483
  if not os.path.samestat(created_status, named_temporary):
1420
1484
  raise DetectError("temporary STATE.md changed before writing")
1421
- if "STATE.md" in os.listdir(project_fd):
1485
+ if "STATE.md" in listdir_anchored(project_fd):
1422
1486
  raise DetectError("STATE.md already exists")
1423
1487
  os.ftruncate(state_fd, 0)
1424
1488
  write_all(state_fd, payload)
@@ -1653,6 +1717,167 @@ class _WindowsHandles:
1653
1717
  raise ctypes.WinError(ctypes.get_last_error())
1654
1718
 
1655
1719
 
1720
+ _WINDOWS_HANDLES: Optional[_WindowsHandles] = None
1721
+
1722
+
1723
+ def get_windows_handles() -> _WindowsHandles:
1724
+ global _WINDOWS_HANDLES
1725
+ if _WINDOWS_HANDLES is None:
1726
+ _WINDOWS_HANDLES = _WindowsHandles()
1727
+ return _WINDOWS_HANDLES
1728
+
1729
+
1730
+ def _windows_missing_ok(error: OSError) -> bool:
1731
+ return getattr(error, "winerror", None) in {2, 3}
1732
+
1733
+
1734
+ def read_windows_anchored_evidence(
1735
+ relative: Path,
1736
+ root: Path,
1737
+ *,
1738
+ missing_ok: bool,
1739
+ evidence_name: str,
1740
+ ) -> Optional[str]:
1741
+ win = get_windows_handles()
1742
+ descriptors: list[int] = []
1743
+ try:
1744
+ descriptors.append(win.open_root(root))
1745
+ opened_root = os.fstat(descriptors[-1])
1746
+ if not stat.S_ISDIR(opened_root.st_mode):
1747
+ raise DetectError(f"repo evidence is not a directory: {root}")
1748
+ for index, part in enumerate(relative.parts):
1749
+ current = root.joinpath(*relative.parts[: index + 1])
1750
+ final = index == len(relative.parts) - 1
1751
+ access = _GENERIC_READ if final else _DIRECTORY_ACCESS
1752
+ options = _FILE_NON_DIRECTORY_FILE if final else _FILE_DIRECTORY_FILE
1753
+ try:
1754
+ descriptor = win.open_relative(
1755
+ descriptors[-1],
1756
+ part,
1757
+ access,
1758
+ _PIN_SHARE,
1759
+ _FILE_OPEN,
1760
+ options,
1761
+ )
1762
+ except OSError as error:
1763
+ if missing_ok and _windows_missing_ok(error):
1764
+ return None
1765
+ raise DetectError(
1766
+ f"filesystem evidence disappeared: {root / relative}"
1767
+ ) from error
1768
+ descriptors.append(descriptor)
1769
+ actual = os.fstat(descriptor)
1770
+ if is_link_like(current, actual):
1771
+ raise DetectError(
1772
+ f"link-like {evidence_name} evidence is not allowed: {current}"
1773
+ )
1774
+ if final and not stat.S_ISREG(actual.st_mode):
1775
+ raise DetectError(
1776
+ f"{evidence_name} evidence is not a regular file: {current}"
1777
+ )
1778
+ if not final and not stat.S_ISDIR(actual.st_mode):
1779
+ raise DetectError(
1780
+ f"{evidence_name} evidence parent is not a directory: {current}"
1781
+ )
1782
+ with os.fdopen(descriptors.pop(), "rb") as stream:
1783
+ data = stream.read()
1784
+ except DetectError:
1785
+ raise
1786
+ except OSError as error:
1787
+ raise DetectError(
1788
+ f"cannot read {evidence_name} evidence: {root / relative}"
1789
+ ) from error
1790
+ finally:
1791
+ for descriptor in reversed(descriptors):
1792
+ os.close(descriptor)
1793
+ try:
1794
+ return data.decode("utf-8")
1795
+ except UnicodeDecodeError as error:
1796
+ raise DetectError(
1797
+ f"cannot read {evidence_name} evidence: {root / relative}"
1798
+ ) from error
1799
+
1800
+
1801
+ def iter_worktree_files_windows_anchored(root: Path) -> Iterable[str]:
1802
+ win = get_windows_handles()
1803
+ try:
1804
+ root_fd = win.open_root(root)
1805
+ except OSError as error:
1806
+ raise DetectError(f"cannot traverse filesystem evidence: {error}") from error
1807
+ try:
1808
+ root_status = os.fstat(root_fd)
1809
+ if not stat.S_ISDIR(root_status.st_mode):
1810
+ raise DetectError(f"repo evidence is not a directory: {root}")
1811
+ yield from iter_from_windows_dir_fd(root_fd, "", root, win)
1812
+ finally:
1813
+ os.close(root_fd)
1814
+
1815
+
1816
+ def iter_from_windows_dir_fd(
1817
+ dir_fd: int, relative_dir: str, root: Path, win: _WindowsHandles
1818
+ ) -> Iterable[str]:
1819
+ scan_dir = root if not relative_dir else root.joinpath(*PurePosixPath(relative_dir).parts)
1820
+ try:
1821
+ # Windows CPython cannot listdir(dir_fd); paths come from the pinned walk.
1822
+ names = os.listdir(scan_dir)
1823
+ except OSError as error:
1824
+ raise DetectError(f"cannot traverse filesystem evidence: {error}") from error
1825
+ for name in names:
1826
+ relative = f"{relative_dir}/{name}" if relative_dir else name
1827
+ if is_ignored(relative) or name == ".project":
1828
+ continue
1829
+ path = root.joinpath(*PurePosixPath(relative).parts)
1830
+ try:
1831
+ child_fd = win.open_relative(
1832
+ dir_fd,
1833
+ name,
1834
+ _DIRECTORY_ACCESS,
1835
+ _PIN_SHARE,
1836
+ _FILE_OPEN,
1837
+ _FILE_DIRECTORY_FILE,
1838
+ )
1839
+ except OSError:
1840
+ child_fd = None
1841
+ else:
1842
+ status = os.fstat(child_fd)
1843
+ if is_link_like(path, status):
1844
+ os.close(child_fd)
1845
+ continue
1846
+ if stat.S_ISDIR(status.st_mode):
1847
+ try:
1848
+ if is_verified_skill_bundle_at(relative, child_fd, root):
1849
+ continue
1850
+ yield from iter_from_windows_dir_fd(
1851
+ child_fd, relative, root, win
1852
+ )
1853
+ finally:
1854
+ os.close(child_fd)
1855
+ continue
1856
+ os.close(child_fd)
1857
+ try:
1858
+ file_fd = win.open_relative(
1859
+ dir_fd,
1860
+ name,
1861
+ _FILE_READ_ATTRIBUTES,
1862
+ _PIN_SHARE,
1863
+ _FILE_OPEN,
1864
+ _FILE_NON_DIRECTORY_FILE,
1865
+ )
1866
+ except OSError as error:
1867
+ raise DetectError(
1868
+ f"cannot inspect filesystem evidence: {relative}: {error}"
1869
+ ) from error
1870
+ try:
1871
+ status = os.fstat(file_fd)
1872
+ if is_link_like(path, status) or not stat.S_ISREG(status.st_mode):
1873
+ continue
1874
+ if is_fixed_managed_pipeline_artifact(relative):
1875
+ continue
1876
+ yield relative
1877
+ finally:
1878
+ os.close(file_fd)
1879
+
1880
+
1656
1881
  def windows_project_entries(project: Path) -> set:
1657
1882
  """project_entries for a .project held open without delete sharing."""
1658
1883
  entries = set(os.listdir(project))
@@ -467,15 +467,17 @@ def completed_field(lines: Sequence[str], field: str, artifact: str) -> str:
467
467
 
468
468
 
469
469
  def completed_bullet_field(lines: Sequence[str], field: str, artifact: str) -> str:
470
- pattern = re.compile(rf"^- \*\*{re.escape(field)}\*\*:\s*(.*)$")
471
- values = []
472
- for line in lines:
473
- match = pattern.fullmatch(line)
474
- if match:
475
- values.append(match.group(1).strip().strip("`"))
476
- if len(values) != 1 or not values[0] or archive_milestone.contains_placeholder(values[0]):
470
+ matches = _common.find_review_bullet_field_values(lines, field)
471
+ if len(matches) != 1:
477
472
  raise ArchiveError(f"{artifact} requires one completed {field} field")
478
- return values[0]
473
+ raw_value = matches[0].strip()
474
+ value = raw_value.strip("`")
475
+ # Scan the value as written. Removing an outer backtick first can unbalance
476
+ # every code span after a leading or trailing one. contains_placeholder
477
+ # already handles a wholly quoted placeholder such as `<command>`.
478
+ if not value or archive_milestone.contains_placeholder(raw_value):
479
+ raise ArchiveError(f"{artifact} requires one completed {field} field")
480
+ return value
479
481
 
480
482
 
481
483
  def section_lines(lines: Sequence[str], heading: str, artifact: str) -> Sequence[str]:
@@ -509,6 +511,12 @@ def archived_intent_criteria(archive: Path) -> dict[int, str]:
509
511
 
510
512
 
511
513
  def parse_final_review(archive: Path) -> tuple:
514
+ reviewed_head, criteria, _raw_evidence = _final_review(archive)
515
+ return reviewed_head, criteria
516
+
517
+
518
+ def _final_review(archive: Path) -> tuple:
519
+ """Parse FINAL.md and retain evidence as written for placeholder scans."""
512
520
  if __package__:
513
521
  from . import check_handoffs
514
522
  else:
@@ -562,18 +570,21 @@ def parse_final_review(archive: Path) -> tuple:
562
570
 
563
571
  expected_fields = ("Verdict", "Check", "Observed", "Reference", "Finding", "Fix direction")
564
572
  criteria = []
573
+ raw_evidence = []
565
574
  for position, (heading_index, _, criterion) in enumerate(headings):
566
575
  section_start = heading_index + 1
567
576
  section_end = headings[position + 1][0] if position + 1 < len(headings) else criteria_end
577
+ section = lines[section_start:section_end]
568
578
  values = {}
569
- for line in lines[section_start:section_end]:
570
- match = re.fullmatch(r"- \*\*(Verdict|Check|Observed|Reference|Finding|Fix direction)\*\*:\s*(.*)", line)
571
- if not match:
572
- continue
573
- key, value = match.groups()
574
- if key in values:
579
+ raw_values = {}
580
+ for key in expected_fields:
581
+ matches = _common.find_review_bullet_field_values(section, key)
582
+ if len(matches) > 1:
575
583
  raise ArchiveError(f"FINAL.md repeats {key} for {criterion}")
576
- values[key] = value.strip().strip("`")
584
+ if len(matches) != 1:
585
+ raise ArchiveError(f"FINAL.md criterion is incomplete: {criterion}")
586
+ raw_values[key] = matches[0].strip()
587
+ values[key] = raw_values[key].strip("`")
577
588
  surface_values = [
578
589
  line.removeprefix("- **Surface**:").strip().strip("`")
579
590
  for line in lines[section_start:section_end]
@@ -593,18 +604,19 @@ def parse_final_review(archive: Path) -> tuple:
593
604
  check_handoffs._surface_value(value, f"FINAL.md surface {field}")
594
605
  except check_handoffs.HandoffError as error:
595
606
  raise ArchiveError(str(error)) from error
596
- elif not value or archive_milestone.contains_placeholder(value):
607
+ elif not value or archive_milestone.contains_placeholder(raw_values[field]):
597
608
  raise ArchiveError(f"FINAL.md criterion is incomplete: {criterion}")
598
609
  if values["Verdict"] != "met":
599
610
  raise ArchiveError(f"FINAL.md criterion lacks passing evidence: {criterion}")
600
- evidence = next(
601
- (values[field] for field in ("Reference", "Check", "Observed") if values[field] != "none"),
611
+ evidence_field = next(
612
+ (field for field in ("Reference", "Check", "Observed") if values[field] != "none"),
602
613
  None,
603
614
  )
604
- if evidence is None:
615
+ if evidence_field is None:
605
616
  raise ArchiveError(f"FINAL.md criterion lacks passing evidence: {criterion}")
606
- criteria.append((criterion, "met", evidence))
607
- return reviewed_head.lower(), criteria
617
+ criteria.append((criterion, "met", values[evidence_field]))
618
+ raw_evidence.append(raw_values[evidence_field])
619
+ return reviewed_head.lower(), criteria, raw_evidence
608
620
 
609
621
 
610
622
  def _member_heads(text: str, source: str) -> dict:
@@ -814,10 +826,14 @@ def validate_wave_review(
814
826
  expected_depth: str,
815
827
  expected_lens: Optional[str] = None,
816
828
  task_texts: Optional[dict[str, str]] = None,
829
+ *,
830
+ is_last_cycle: bool = True,
817
831
  ) -> Sequence[str]:
818
832
  lines = path.read_text(encoding="utf-8").splitlines()
819
833
  depth = completed_field(lines, "Depth:", path.name)
820
- if depth != expected_depth:
834
+ if depth not in {"full", "deep", "verify-only"}:
835
+ raise ArchiveError(f"{path.name} has an invalid review depth")
836
+ if is_last_cycle and depth != expected_depth:
821
837
  raise ArchiveError(f"{path.name} has an invalid review depth")
822
838
  if expected_lens is not None:
823
839
  lens = completed_field(lines, "Lens:", path.name)
@@ -842,10 +858,32 @@ def validate_wave_review(
842
858
  normalized_expected = [
843
859
  (task_id, " ".join(title.split())) for task_id, title in expected_tasks
844
860
  ]
845
- if actual_tasks != normalized_expected:
846
- raise ArchiveError(
847
- f"{path.name} tasks and titles do not match its wave task files in order"
848
- )
861
+ expected_by_id = dict(normalized_expected)
862
+ enforce_plan_tasks = is_last_cycle and wave_verdict != "blocked"
863
+ if enforce_plan_tasks:
864
+ if actual_tasks != normalized_expected:
865
+ raise ArchiveError(
866
+ f"{path.name} tasks and titles do not match its wave task files in order"
867
+ )
868
+ else:
869
+ known_ids = set(expected_by_id)
870
+ for task_id in task_ids:
871
+ if task_id not in known_ids:
872
+ raise ArchiveError(
873
+ f"{path.name} tasks and titles do not match its wave task files in order"
874
+ )
875
+ plan_order = [task_id for task_id, _title in normalized_expected]
876
+ reviewed_order = [task_id for task_id in plan_order if task_id in task_ids]
877
+ if task_ids != reviewed_order:
878
+ raise ArchiveError(
879
+ f"{path.name} tasks and titles do not match its wave task files in order"
880
+ )
881
+ for _index, task_id, title, _verdict in headings:
882
+ expected_title = expected_by_id.get(task_id)
883
+ if expected_title is not None and " ".join(title.split()) != expected_title:
884
+ raise ArchiveError(
885
+ f"{path.name} tasks and titles do not match its wave task files in order"
886
+ )
849
887
 
850
888
  verdicts = []
851
889
  for position, (heading_index, _task_id, _title, verdict) in enumerate(headings):
@@ -870,6 +908,9 @@ def validate_wave_review(
870
908
  )
871
909
  verdicts.append(verdict)
872
910
 
911
+ if not is_last_cycle:
912
+ return verdicts
913
+
873
914
  coverage_indexes = [
874
915
  index for index, line in enumerate(lines) if line == "## Intent coverage"
875
916
  ]
@@ -1083,10 +1124,17 @@ def review_cycle_counts(archive: Path) -> Sequence[int]:
1083
1124
  depth,
1084
1125
  lens,
1085
1126
  task_texts,
1127
+ is_last_cycle=cycle == max(cycles),
1086
1128
  )
1087
1129
  )
1088
1130
 
1089
1131
  last_cycle = max(cycles)
1132
+ for cycle, verdicts in wave_verdicts.items():
1133
+ if cycle != last_cycle and "blocked" not in verdicts:
1134
+ raise ArchiveError(
1135
+ f"superseded review cycle {cycle} for wave {wave} "
1136
+ "must include a blocked Wave verdict"
1137
+ )
1090
1138
  if any(verdict != "pass" for verdict in wave_verdicts[last_cycle]):
1091
1139
  raise ArchiveError(f"last review cycle for wave {wave} did not pass")
1092
1140
  if any(task_verdict != "pass" for task_verdict in task_verdicts[last_cycle]):
@@ -1220,7 +1268,7 @@ def validate_manifest(project: Path, archive: Path, state: PipelineState) -> tup
1220
1268
  if fields["Final verdict:"] != "all criteria met; project verify passed":
1221
1269
  raise ArchiveError("manifest final verdict does not record the passed final gate")
1222
1270
 
1223
- reviewed_head, final_criteria = parse_final_review(archive)
1271
+ reviewed_head, final_criteria, final_raw_evidence = _final_review(archive)
1224
1272
  validate_gap_reviews(archive, reviewed_head)
1225
1273
  task_files, attested = archive_milestone.landed_task_evidence(project, archive / "tasks", reviewed_head)
1226
1274
  cycle_counts = review_cycle_counts(archive)
@@ -1264,14 +1312,23 @@ def validate_manifest(project: Path, archive: Path, state: PipelineState) -> tup
1264
1312
  continue
1265
1313
  criteria_rows.append(cells)
1266
1314
  criteria_line_indexes.add(index)
1315
+
1316
+ def evidence_as_written(position: int, row: Sequence[str]) -> str:
1317
+ # Manifest evidence stores FINAL.md's normalized value. Scan the source
1318
+ # text when the row matches, so its inner code spans stay balanced.
1319
+ if position < len(final_criteria) and tuple(row) == tuple(final_criteria[position]):
1320
+ return final_raw_evidence[position]
1321
+ return row[2]
1322
+
1267
1323
  if not criteria_rows or any(
1268
1324
  len(row) != 3
1269
1325
  or not row[0]
1270
1326
  or row[1] != "met"
1271
1327
  or not row[2]
1272
1328
  or row[2] == "none"
1273
- or any(archive_milestone.contains_placeholder(cell) for cell in row[1:])
1274
- for row in criteria_rows
1329
+ or archive_milestone.contains_placeholder(row[1])
1330
+ or archive_milestone.contains_placeholder(evidence_as_written(position, row))
1331
+ for position, row in enumerate(criteria_rows)
1275
1332
  ):
1276
1333
  raise ArchiveError("manifest success-criteria rows are incomplete")
1277
1334
  if [tuple(row) for row in criteria_rows] != list(final_criteria):