@opengsd/gsd-path 1.6.2 → 1.7.1

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 (281) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/DOCS.md +27 -1
  3. package/FULL.md +4 -3
  4. package/HOOKS.md +38 -4
  5. package/QUICK.md +1 -1
  6. package/README.md +19 -11
  7. package/RUNTIME.md +5 -3
  8. package/WORKFLOW.md +11 -4
  9. package/package.json +1 -1
  10. package/scripts/_common.py +76 -2
  11. package/scripts/archive_milestone.py +129 -7
  12. package/scripts/build_recovery.py +25 -2
  13. package/scripts/build_state.py +333 -8
  14. package/scripts/check_handoffs.py +437 -30
  15. package/scripts/check_task_briefs.py +135 -11
  16. package/scripts/discussion_validate.py +46 -10
  17. package/scripts/dispatch_driver.py +33 -3
  18. package/scripts/git_guard.py +44 -4
  19. package/scripts/guard_hook.py +456 -9
  20. package/scripts/install.py +34 -4
  21. package/scripts/integration.py +196 -39
  22. package/scripts/isolation.py +1000 -38
  23. package/scripts/lean_verification.py +28 -13
  24. package/scripts/loop_run.py +9 -4
  25. package/scripts/members.py +17 -3
  26. package/scripts/pipeline_diagnose.py +104 -1
  27. package/scripts/pipeline_state.py +10 -2
  28. package/scripts/runtime_store.py +88 -3
  29. package/scripts/state_checkpoint.py +15 -0
  30. package/scripts/status_runtime.py +73 -2
  31. package/scripts/workflow_run.py +10 -3
  32. package/scripts/worktree_paths.py +45 -0
  33. package/skills/gsd-path/BUILD.md +4 -1
  34. package/skills/gsd-path/DEFINE.md +2 -1
  35. package/skills/gsd-path/PLAN.md +13 -3
  36. package/skills/gsd-path/SHIP.md +41 -8
  37. package/skills/gsd-path/SKILL.md +16 -1
  38. package/skills/gsd-path/references/build-native.md +28 -7
  39. package/skills/gsd-path/references/coder.md +6 -3
  40. package/skills/gsd-path/references/planner.md +4 -3
  41. package/skills/gsd-path/references/reviewer-final.md +10 -0
  42. package/skills/gsd-path/references/reviewer-wave.md +71 -27
  43. package/skills/gsd-path/references/reviewer.md +21 -8
  44. package/skills/gsd-path/references/runtime-selection.md +36 -0
  45. package/skills/gsd-path/references/spec-probes.md +19 -1
  46. package/skills/gsd-path/scripts/_common.py +76 -2
  47. package/skills/gsd-path/scripts/archive_milestone.py +129 -7
  48. package/skills/gsd-path/scripts/build_recovery.py +25 -2
  49. package/skills/gsd-path/scripts/build_state.py +333 -8
  50. package/skills/gsd-path/scripts/check_handoffs.py +437 -30
  51. package/skills/gsd-path/scripts/check_task_briefs.py +135 -11
  52. package/skills/gsd-path/scripts/discussion_validate.py +46 -10
  53. package/skills/gsd-path/scripts/dispatch_driver.py +33 -3
  54. package/skills/gsd-path/scripts/git_guard.py +44 -4
  55. package/skills/gsd-path/scripts/integration.py +196 -39
  56. package/skills/gsd-path/scripts/isolation.py +1000 -38
  57. package/skills/gsd-path/scripts/lean_verification.py +28 -13
  58. package/skills/gsd-path/scripts/loop_run.py +9 -4
  59. package/skills/gsd-path/scripts/members.py +17 -3
  60. package/skills/gsd-path/scripts/pipeline_state.py +10 -2
  61. package/skills/gsd-path/scripts/state_checkpoint.py +15 -0
  62. package/skills/gsd-path/scripts/status_runtime.py +73 -2
  63. package/skills/gsd-path/scripts/workflow_run.py +10 -3
  64. package/skills/gsd-path/scripts/worktree_paths.py +45 -0
  65. package/skills/gsd-path/templates/intent.md +5 -3
  66. package/skills/gsd-path/templates/plan.md +3 -2
  67. package/skills/gsd-path/templates/task.md +8 -1
  68. package/skills/gsd-path/templates/wave-review.md +24 -8
  69. package/skills/gsd-path-build/SKILL.md +4 -1
  70. package/skills/gsd-path-build/references/build-native.md +28 -7
  71. package/skills/gsd-path-build/references/coder.md +6 -3
  72. package/skills/gsd-path-build/references/reviewer-final.md +10 -0
  73. package/skills/gsd-path-build/references/reviewer-wave.md +71 -27
  74. package/skills/gsd-path-build/references/reviewer.md +21 -8
  75. package/skills/gsd-path-build/references/runtime-selection.md +36 -0
  76. package/skills/gsd-path-build/references/spec-probes.md +19 -1
  77. package/skills/gsd-path-build/scripts/_common.py +76 -2
  78. package/skills/gsd-path-build/scripts/archive_milestone.py +129 -7
  79. package/skills/gsd-path-build/scripts/build_recovery.py +25 -2
  80. package/skills/gsd-path-build/scripts/build_state.py +333 -8
  81. package/skills/gsd-path-build/scripts/check_handoffs.py +437 -30
  82. package/skills/gsd-path-build/scripts/check_task_briefs.py +135 -11
  83. package/skills/gsd-path-build/scripts/discussion_validate.py +46 -10
  84. package/skills/gsd-path-build/scripts/dispatch_driver.py +33 -3
  85. package/skills/gsd-path-build/scripts/integration.py +196 -39
  86. package/skills/gsd-path-build/scripts/isolation.py +1000 -38
  87. package/skills/gsd-path-build/scripts/loop_run.py +9 -4
  88. package/skills/gsd-path-build/scripts/members.py +17 -3
  89. package/skills/gsd-path-build/scripts/pipeline_state.py +10 -2
  90. package/skills/gsd-path-build/scripts/state_checkpoint.py +15 -0
  91. package/skills/gsd-path-build/scripts/workflow_run.py +10 -3
  92. package/skills/gsd-path-build/scripts/worktree_paths.py +45 -0
  93. package/skills/gsd-path-build/templates/task.md +8 -1
  94. package/skills/gsd-path-build/templates/wave-review.md +24 -8
  95. package/skills/gsd-path-decide/references/runtime-selection.md +36 -0
  96. package/skills/gsd-path-decide/scripts/_common.py +76 -2
  97. package/skills/gsd-path-decide/scripts/archive_milestone.py +129 -7
  98. package/skills/gsd-path-decide/scripts/build_recovery.py +25 -2
  99. package/skills/gsd-path-decide/scripts/check_handoffs.py +437 -30
  100. package/skills/gsd-path-decide/scripts/check_task_briefs.py +135 -11
  101. package/skills/gsd-path-decide/scripts/discussion_validate.py +46 -10
  102. package/skills/gsd-path-decide/scripts/integration.py +196 -39
  103. package/skills/gsd-path-decide/scripts/isolation.py +1000 -38
  104. package/skills/gsd-path-decide/scripts/loop_run.py +9 -4
  105. package/skills/gsd-path-decide/scripts/members.py +17 -3
  106. package/skills/gsd-path-decide/scripts/pipeline_state.py +10 -2
  107. package/skills/gsd-path-decide/scripts/state_checkpoint.py +15 -0
  108. package/skills/gsd-path-decide/scripts/worktree_paths.py +45 -0
  109. package/skills/gsd-path-define/SKILL.md +2 -1
  110. package/skills/gsd-path-define/references/runtime-selection.md +36 -0
  111. package/skills/gsd-path-define/references/spec-probes.md +19 -1
  112. package/skills/gsd-path-define/scripts/_common.py +76 -2
  113. package/skills/gsd-path-define/scripts/archive_milestone.py +129 -7
  114. package/skills/gsd-path-define/scripts/build_recovery.py +25 -2
  115. package/skills/gsd-path-define/scripts/check_handoffs.py +437 -30
  116. package/skills/gsd-path-define/scripts/check_task_briefs.py +135 -11
  117. package/skills/gsd-path-define/scripts/discussion_validate.py +46 -10
  118. package/skills/gsd-path-define/scripts/integration.py +196 -39
  119. package/skills/gsd-path-define/scripts/isolation.py +1000 -38
  120. package/skills/gsd-path-define/scripts/loop_run.py +9 -4
  121. package/skills/gsd-path-define/scripts/members.py +17 -3
  122. package/skills/gsd-path-define/scripts/pipeline_state.py +10 -2
  123. package/skills/gsd-path-define/scripts/state_checkpoint.py +15 -0
  124. package/skills/gsd-path-define/scripts/worktree_paths.py +45 -0
  125. package/skills/gsd-path-define/templates/intent.md +5 -3
  126. package/skills/gsd-path-discuss/references/runtime-selection.md +36 -0
  127. package/skills/gsd-path-discuss/scripts/_common.py +76 -2
  128. package/skills/gsd-path-discuss/scripts/loop_run.py +9 -4
  129. package/skills/gsd-path-discuss/scripts/worktree_paths.py +45 -0
  130. package/skills/gsd-path-docs-audit/references/runtime-selection.md +36 -0
  131. package/skills/gsd-path-docs-audit/scripts/_common.py +76 -2
  132. package/skills/gsd-path-docs-audit/scripts/archive_milestone.py +129 -7
  133. package/skills/gsd-path-docs-audit/scripts/build_recovery.py +25 -2
  134. package/skills/gsd-path-docs-audit/scripts/check_handoffs.py +437 -30
  135. package/skills/gsd-path-docs-audit/scripts/check_task_briefs.py +135 -11
  136. package/skills/gsd-path-docs-audit/scripts/discussion_validate.py +46 -10
  137. package/skills/gsd-path-docs-audit/scripts/integration.py +196 -39
  138. package/skills/gsd-path-docs-audit/scripts/isolation.py +1000 -38
  139. package/skills/gsd-path-docs-audit/scripts/loop_run.py +9 -4
  140. package/skills/gsd-path-docs-audit/scripts/members.py +17 -3
  141. package/skills/gsd-path-docs-audit/scripts/pipeline_state.py +10 -2
  142. package/skills/gsd-path-docs-audit/scripts/state_checkpoint.py +15 -0
  143. package/skills/gsd-path-docs-audit/scripts/worktree_paths.py +45 -0
  144. package/skills/gsd-path-forensics/references/runtime-selection.md +36 -0
  145. package/skills/gsd-path-forensics/scripts/_common.py +76 -2
  146. package/skills/gsd-path-forensics/scripts/loop_run.py +9 -4
  147. package/skills/gsd-path-forensics/scripts/pipeline_diagnose.py +104 -1
  148. package/skills/gsd-path-forensics/scripts/worktree_paths.py +45 -0
  149. package/skills/gsd-path-inspect/references/runtime-selection.md +36 -0
  150. package/skills/gsd-path-inspect/scripts/_common.py +76 -2
  151. package/skills/gsd-path-inspect/scripts/archive_milestone.py +129 -7
  152. package/skills/gsd-path-inspect/scripts/build_recovery.py +25 -2
  153. package/skills/gsd-path-inspect/scripts/check_handoffs.py +437 -30
  154. package/skills/gsd-path-inspect/scripts/check_task_briefs.py +135 -11
  155. package/skills/gsd-path-inspect/scripts/discussion_validate.py +46 -10
  156. package/skills/gsd-path-inspect/scripts/integration.py +196 -39
  157. package/skills/gsd-path-inspect/scripts/isolation.py +1000 -38
  158. package/skills/gsd-path-inspect/scripts/loop_run.py +9 -4
  159. package/skills/gsd-path-inspect/scripts/members.py +17 -3
  160. package/skills/gsd-path-inspect/scripts/pipeline_state.py +10 -2
  161. package/skills/gsd-path-inspect/scripts/state_checkpoint.py +15 -0
  162. package/skills/gsd-path-inspect/scripts/workflow_run.py +10 -3
  163. package/skills/gsd-path-inspect/scripts/worktree_paths.py +45 -0
  164. package/skills/gsd-path-loop/references/runtime-selection.md +36 -0
  165. package/skills/gsd-path-loop/scripts/_common.py +76 -2
  166. package/skills/gsd-path-loop/scripts/loop_run.py +9 -4
  167. package/skills/gsd-path-loop/scripts/worktree_paths.py +45 -0
  168. package/skills/gsd-path-migrate/references/runtime-selection.md +36 -0
  169. package/skills/gsd-path-migrate/scripts/_common.py +76 -2
  170. package/skills/gsd-path-migrate/scripts/loop_run.py +9 -4
  171. package/skills/gsd-path-plan/SKILL.md +13 -3
  172. package/skills/gsd-path-plan/references/planner.md +4 -3
  173. package/skills/gsd-path-plan/references/runtime-selection.md +36 -0
  174. package/skills/gsd-path-plan/references/spec-probes.md +19 -1
  175. package/skills/gsd-path-plan/scripts/_common.py +76 -2
  176. package/skills/gsd-path-plan/scripts/archive_milestone.py +129 -7
  177. package/skills/gsd-path-plan/scripts/build_recovery.py +25 -2
  178. package/skills/gsd-path-plan/scripts/check_handoffs.py +437 -30
  179. package/skills/gsd-path-plan/scripts/check_task_briefs.py +135 -11
  180. package/skills/gsd-path-plan/scripts/discussion_validate.py +46 -10
  181. package/skills/gsd-path-plan/scripts/integration.py +196 -39
  182. package/skills/gsd-path-plan/scripts/isolation.py +1000 -38
  183. package/skills/gsd-path-plan/scripts/loop_run.py +9 -4
  184. package/skills/gsd-path-plan/scripts/members.py +17 -3
  185. package/skills/gsd-path-plan/scripts/pipeline_state.py +10 -2
  186. package/skills/gsd-path-plan/scripts/state_checkpoint.py +15 -0
  187. package/skills/gsd-path-plan/scripts/workflow_run.py +10 -3
  188. package/skills/gsd-path-plan/scripts/worktree_paths.py +45 -0
  189. package/skills/gsd-path-plan/templates/plan.md +3 -2
  190. package/skills/gsd-path-plan/templates/task.md +8 -1
  191. package/skills/gsd-path-research/references/runtime-selection.md +36 -0
  192. package/skills/gsd-path-research/scripts/_common.py +76 -2
  193. package/skills/gsd-path-research/scripts/archive_milestone.py +129 -7
  194. package/skills/gsd-path-research/scripts/build_recovery.py +25 -2
  195. package/skills/gsd-path-research/scripts/check_handoffs.py +437 -30
  196. package/skills/gsd-path-research/scripts/check_task_briefs.py +135 -11
  197. package/skills/gsd-path-research/scripts/discussion_validate.py +46 -10
  198. package/skills/gsd-path-research/scripts/integration.py +196 -39
  199. package/skills/gsd-path-research/scripts/isolation.py +1000 -38
  200. package/skills/gsd-path-research/scripts/loop_run.py +9 -4
  201. package/skills/gsd-path-research/scripts/members.py +17 -3
  202. package/skills/gsd-path-research/scripts/pipeline_state.py +10 -2
  203. package/skills/gsd-path-research/scripts/state_checkpoint.py +15 -0
  204. package/skills/gsd-path-research/scripts/worktree_paths.py +45 -0
  205. package/skills/gsd-path-roadmap/references/runtime-selection.md +36 -0
  206. package/skills/gsd-path-roadmap/scripts/_common.py +76 -2
  207. package/skills/gsd-path-roadmap/scripts/archive_milestone.py +129 -7
  208. package/skills/gsd-path-roadmap/scripts/build_recovery.py +25 -2
  209. package/skills/gsd-path-roadmap/scripts/check_handoffs.py +437 -30
  210. package/skills/gsd-path-roadmap/scripts/check_task_briefs.py +135 -11
  211. package/skills/gsd-path-roadmap/scripts/discussion_validate.py +46 -10
  212. package/skills/gsd-path-roadmap/scripts/integration.py +196 -39
  213. package/skills/gsd-path-roadmap/scripts/isolation.py +1000 -38
  214. package/skills/gsd-path-roadmap/scripts/loop_run.py +9 -4
  215. package/skills/gsd-path-roadmap/scripts/members.py +17 -3
  216. package/skills/gsd-path-roadmap/scripts/pipeline_state.py +10 -2
  217. package/skills/gsd-path-roadmap/scripts/state_checkpoint.py +15 -0
  218. package/skills/gsd-path-roadmap/scripts/worktree_paths.py +45 -0
  219. package/skills/gsd-path-ship/SKILL.md +41 -8
  220. package/skills/gsd-path-ship/references/reviewer-final.md +10 -0
  221. package/skills/gsd-path-ship/references/reviewer-wave.md +71 -27
  222. package/skills/gsd-path-ship/references/reviewer.md +21 -8
  223. package/skills/gsd-path-ship/references/runtime-selection.md +36 -0
  224. package/skills/gsd-path-ship/references/spec-probes.md +19 -1
  225. package/skills/gsd-path-ship/scripts/_common.py +76 -2
  226. package/skills/gsd-path-ship/scripts/archive_milestone.py +129 -7
  227. package/skills/gsd-path-ship/scripts/build_recovery.py +25 -2
  228. package/skills/gsd-path-ship/scripts/build_state.py +333 -8
  229. package/skills/gsd-path-ship/scripts/check_handoffs.py +437 -30
  230. package/skills/gsd-path-ship/scripts/check_task_briefs.py +135 -11
  231. package/skills/gsd-path-ship/scripts/discussion_validate.py +46 -10
  232. package/skills/gsd-path-ship/scripts/integration.py +196 -39
  233. package/skills/gsd-path-ship/scripts/isolation.py +1000 -38
  234. package/skills/gsd-path-ship/scripts/lean_verification.py +28 -13
  235. package/skills/gsd-path-ship/scripts/loop_run.py +9 -4
  236. package/skills/gsd-path-ship/scripts/members.py +17 -3
  237. package/skills/gsd-path-ship/scripts/pipeline_state.py +10 -2
  238. package/skills/gsd-path-ship/scripts/state_checkpoint.py +15 -0
  239. package/skills/gsd-path-ship/scripts/workflow_run.py +10 -3
  240. package/skills/gsd-path-ship/scripts/worktree_paths.py +45 -0
  241. package/skills/gsd-path-ship/templates/wave-review.md +24 -8
  242. package/skills/gsd-path-undo/references/runtime-selection.md +36 -0
  243. package/skills/gsd-path-undo/scripts/_common.py +76 -2
  244. package/skills/gsd-path-undo/scripts/loop_run.py +9 -4
  245. package/skills/gsd-path-undo/scripts/worktree_paths.py +45 -0
  246. package/skills/path/BUILD.md +4 -1
  247. package/skills/path/DEFINE.md +2 -1
  248. package/skills/path/PLAN.md +13 -3
  249. package/skills/path/SHIP.md +41 -8
  250. package/skills/path/SKILL.md +16 -1
  251. package/skills/path/references/build-native.md +28 -7
  252. package/skills/path/references/coder.md +6 -3
  253. package/skills/path/references/planner.md +4 -3
  254. package/skills/path/references/reviewer-final.md +10 -0
  255. package/skills/path/references/reviewer-wave.md +71 -27
  256. package/skills/path/references/reviewer.md +21 -8
  257. package/skills/path/references/runtime-selection.md +36 -0
  258. package/skills/path/references/spec-probes.md +19 -1
  259. package/skills/path/scripts/_common.py +76 -2
  260. package/skills/path/scripts/archive_milestone.py +129 -7
  261. package/skills/path/scripts/build_recovery.py +25 -2
  262. package/skills/path/scripts/build_state.py +333 -8
  263. package/skills/path/scripts/check_handoffs.py +437 -30
  264. package/skills/path/scripts/check_task_briefs.py +135 -11
  265. package/skills/path/scripts/discussion_validate.py +46 -10
  266. package/skills/path/scripts/dispatch_driver.py +33 -3
  267. package/skills/path/scripts/git_guard.py +44 -4
  268. package/skills/path/scripts/integration.py +196 -39
  269. package/skills/path/scripts/isolation.py +1000 -38
  270. package/skills/path/scripts/lean_verification.py +28 -13
  271. package/skills/path/scripts/loop_run.py +9 -4
  272. package/skills/path/scripts/members.py +17 -3
  273. package/skills/path/scripts/pipeline_state.py +10 -2
  274. package/skills/path/scripts/state_checkpoint.py +15 -0
  275. package/skills/path/scripts/status_runtime.py +73 -2
  276. package/skills/path/scripts/workflow_run.py +10 -3
  277. package/skills/path/scripts/worktree_paths.py +45 -0
  278. package/skills/path/templates/intent.md +5 -3
  279. package/skills/path/templates/plan.md +3 -2
  280. package/skills/path/templates/task.md +8 -1
  281. package/skills/path/templates/wave-review.md +24 -8
@@ -231,8 +231,14 @@ def _retire_execution(repo, head, execution):
231
231
  isolation.retire(repo, worktree, execution["branch"], force=False)
232
232
 
233
233
 
234
- def verify_project(repo, expected_head):
235
- """Execute project Verify in isolation, retaining output for retry reuse."""
234
+ def verify_project(repo, expected_head, *, rerun: bool = False):
235
+ """Execute project Verify in isolation, retaining output for retry reuse.
236
+
237
+ With ``rerun`` (the owner's explicit ``prepare-final --rerun`` after a
238
+ host-caused failure diagnosed through the forensics contract), a recorded
239
+ execution at the same command and commit is retired and the command runs
240
+ again, appending a second ledger entry; the failed entry stays as history.
241
+ """
236
242
  repo = Path(repo).resolve()
237
243
  _require_ship_inputs(repo, expected_head)
238
244
  plan = contracts._read(repo, ".project/plan/PLAN.md")
@@ -247,7 +253,6 @@ def verify_project(repo, expected_head):
247
253
  destination = repo / relative
248
254
  if destination.is_symlink():
249
255
  raise contracts.HandoffError("project verification evidence must be a real file")
250
- expected_destination = hashlib.sha256(destination.read_bytes()).hexdigest() if destination.exists() else None
251
256
  execution = entry.get("execution") if entry else None
252
257
  if entry and execution is None:
253
258
  raise contracts.HandoffError("recorded verification lacks output; preserve and reconcile its evidence")
@@ -263,11 +268,16 @@ def verify_project(repo, expected_head):
263
268
  relative, relative, journal["expected_destination"],
264
269
  )
265
270
  _retire_execution(repo, expected_head, execution)
266
- view = _gap_view(command, expected_head, execution, waves)
267
- if not destination.exists() or destination.read_text(encoding="utf-8") != view:
268
- _common.atomic_write(destination, view)
269
- return {"passed": entry["result"] == "pass", "reused": True,
270
- "path": str(destination), "execution": execution}
271
+ if not rerun:
272
+ view = _gap_view(command, expected_head, execution, waves)
273
+ if not destination.exists() or destination.read_text(encoding="utf-8") != view:
274
+ _common.atomic_write(destination, view)
275
+ return {"passed": entry["result"] == "pass", "reused": True,
276
+ "path": str(destination), "execution": execution}
277
+ # Hash after any prior execution's recovery: a rerun's old-collection
278
+ # recovery can rewrite the gap view, and the fresh collection must expect
279
+ # the file as it stands now, not before the recovery (#454).
280
+ expected_destination = hashlib.sha256(destination.read_bytes()).hexdigest() if destination.exists() else None
271
281
  sidecar = isolation.isolate_verify(repo, expected_head, "project-verify")
272
282
  worktree = Path(sidecar["worktree"])
273
283
  try:
@@ -295,18 +305,20 @@ def verify_project(repo, expected_head):
295
305
  execution["members"] = {name: record["tip"] for name, record in sidecars.items()}
296
306
  else:
297
307
  build_state.verify_record(str(repo), command, expected_head,
298
- "pass" if passed else "fail", execution=execution)
308
+ "pass" if passed else "fail", execution=execution,
309
+ rerun=rerun)
299
310
  isolation.clean_verify(repo, worktree, expected_head, sidecar["branch"])
300
311
  _common.atomic_write(worktree / relative, _gap_view(command, expected_head, execution, waves))
301
312
  collection = isolation.collect_artifact(repo, worktree, expected_head, sidecar["branch"],
302
313
  relative, relative, expected_destination)
303
314
  isolation.retire(repo, worktree, sidecar["branch"], force=False)
304
315
  return {"passed": passed, "reused": False, "path": str(destination),
305
- "execution": execution, "collection": collection}
316
+ "execution": execution, "collection": collection,
317
+ **({"rerun": True} if rerun else {})}
306
318
 
307
319
 
308
- def prepare_final(repo, expected_head):
309
- verification = verify_project(repo, expected_head)
320
+ def prepare_final(repo, expected_head, *, rerun: bool = False):
321
+ verification = verify_project(repo, expected_head, rerun=rerun)
310
322
  if not verification["passed"]:
311
323
  return {"status": "blocked", "verification": verification,
312
324
  "reason": "project Verify failed; use its recorded output"}
@@ -327,9 +339,12 @@ def main(argv=None):
327
339
  parser = argparse.ArgumentParser(description=__doc__)
328
340
  parser.add_argument("--repo", type=Path, required=True)
329
341
  parser.add_argument("--expected-head", required=True)
342
+ parser.add_argument("--rerun", action="store_true",
343
+ help="re-execute project Verify at the same head after a host-caused "
344
+ "failure, appending a new ledger entry")
330
345
  args = parser.parse_args(argv)
331
346
  try:
332
- result = prepare_final(args.repo.resolve(), args.expected_head)
347
+ result = prepare_final(args.repo.resolve(), args.expected_head, rerun=args.rerun)
333
348
  except (contracts.HandoffError, isolation.IsolationError, build_state.BuildStateError,
334
349
  OSError, subprocess.CalledProcessError) as error:
335
350
  print(str(error), file=sys.stderr)
@@ -530,10 +530,15 @@ def run_shell(command: str, timeout: float) -> tuple[Optional[int], str]:
530
530
  except subprocess.TimeoutExpired as drained:
531
531
  stdout = drained.stdout or expired.stdout or b""
532
532
  stderr = drained.stderr or expired.stderr or b""
533
- if process.stdout:
534
- process.stdout.close()
535
- if process.stderr:
536
- process.stderr.close()
533
+ # Windows communicate() reads in threads that hold each pipe's lock,
534
+ # so close() would block until an orphaned grandchild (bash forks
535
+ # commands it runs from a script file) exits; leave them to the
536
+ # daemon reader threads there.
537
+ if os.name != "nt":
538
+ if process.stdout:
539
+ process.stdout.close()
540
+ if process.stderr:
541
+ process.stderr.close()
537
542
  process.kill()
538
543
  process.wait(timeout=5)
539
544
  output = stdout.decode("utf-8", errors="replace") + stderr.decode("utf-8", errors="replace")
@@ -34,9 +34,17 @@ MEMBERS_FILE = "MEMBERS.md"
34
34
  MARKER_SCHEMA = "gsd-path/member/v1"
35
35
  MARKER_KEYS = {"schema", "coordinator", "project", "name"}
36
36
  NAME_RE = re.compile(r"^[a-z0-9][a-z0-9-]*$")
37
- FIELDS = ("Checkout", "Remote", "Integration")
37
+ FIELDS = ("Checkout", "Remote", "Integration", "Prepare")
38
+ # The first three are required; Prepare (isolate provisioning, #463) and
39
+ # Default branch are optional.
40
+ REQUIRED_FIELDS = ("Checkout", "Remote", "Integration")
38
41
  # Optional: `add` records it one time; a row without it means main.
39
42
  DEFAULT_BRANCH_FIELD = "Default branch"
43
+ # Optional: a shell command run inside each fresh member isolate before any
44
+ # Verify (#463). It provisions what a bare worktree lacks — submodules from
45
+ # the primary checkout's object store, gitignored build inputs — and must
46
+ # copy, never link, outside the isolate (#462).
47
+ PREPARE_FIELD = "Prepare"
40
48
  INTEGRATIONS = ("default", "direct", "pull-request")
41
49
  GITHUB_REMOTE_RE = re.compile(
42
50
  r"(?:https://github\.com/|git@github\.com:|ssh://git@github\.com/)([^/\s]+)/([^/\s]+)"
@@ -206,10 +214,15 @@ def read_members(coordinator: Path) -> list[dict[str, str]]:
206
214
  return []
207
215
  if path.is_symlink() or not path.is_file():
208
216
  raise MembersError("MEMBERS.md must be a regular file")
217
+ return parse_members(path.read_text(encoding="utf-8"))
218
+
219
+
220
+ def parse_members(text: str) -> list[dict[str, str]]:
221
+ """Parse MEMBERS.md text."""
209
222
  members: list[dict[str, str]] = []
210
223
  current: Optional[dict[str, str]] = None
211
224
  preamble: list[str] = []
212
- for number, line in enumerate(path.read_text(encoding="utf-8").splitlines(), 1):
225
+ for number, line in enumerate(text.splitlines(), 1):
213
226
  if line.startswith("## "):
214
227
  name = line[3:].strip()
215
228
  if not NAME_RE.fullmatch(name):
@@ -241,7 +254,7 @@ def read_members(coordinator: Path) -> list[dict[str, str]]:
241
254
  if len(set(names)) != len(names):
242
255
  raise MembersError("MEMBERS.md repeats a member name")
243
256
  for member in members:
244
- missing = [field for field in FIELDS if not member.get(field.lower())]
257
+ missing = [field for field in REQUIRED_FIELDS if not member.get(field.lower())]
245
258
  if missing:
246
259
  raise MembersError(f"MEMBERS.md member {member['name']} lacks {', '.join(missing)}")
247
260
  if member["integration"] not in INTEGRATIONS:
@@ -510,6 +523,7 @@ def render(members: Sequence[dict[str, str]]) -> str:
510
523
  f"Checkout: {member['checkout']}\n"
511
524
  f"Remote: {member['remote']}\n"
512
525
  f"Integration: {member['integration']}\n"
526
+ + (f"{PREPARE_FIELD}: {member['prepare']}\n" if "prepare" in member else "")
513
527
  # A row that never had the field keeps none, so old rows are not rewritten.
514
528
  + (f"{DEFAULT_BRANCH_FIELD}: {member['default_branch']}\n" if "default_branch" in member else "")
515
529
  for member in members
@@ -1407,6 +1407,12 @@ def _route_state(repo: Path, project_dir: str = ".project") -> dict[str, object]
1407
1407
  mode="milestone" if status == "done" else "normal",
1408
1408
  reason=f"state is {phase}/{status}",
1409
1409
  )
1410
+ if phase == "plan" and status == "blocked":
1411
+ return _route_result(
1412
+ state,
1413
+ "block",
1414
+ reason="plan is blocked; resolve the correction or user decision named by the plan gate before resuming",
1415
+ )
1410
1416
  if phase == "plan" and status != "done":
1411
1417
  return _route_result(state, "run-phase", phase="plan", reason=f"state is {phase}/{status}")
1412
1418
  if phase == "plan":
@@ -2373,9 +2379,9 @@ def main(argv: Optional[Sequence[str]] = None) -> int:
2373
2379
  args.project_dir,
2374
2380
  )
2375
2381
  elif args.command == "approve" and (args.defer_checkpoint or args.patch):
2376
- if args.expected_head is not None:
2382
+ if args.defer_checkpoint and args.expected_head is not None:
2377
2383
  raise PipelineStateError(
2378
- "--expected-head is not accepted with --defer-checkpoint or --patch"
2384
+ "--expected-head is not accepted with --defer-checkpoint"
2379
2385
  )
2380
2386
  result = state_checkpoint.defer_approval(
2381
2387
  args.repo,
@@ -2384,6 +2390,8 @@ def main(argv: Optional[Sequence[str]] = None) -> int:
2384
2390
  args.milestone,
2385
2391
  args.patch,
2386
2392
  )
2393
+ if args.expected_head is not None:
2394
+ result["expected_head_accepted"] = args.expected_head
2387
2395
  elif args.command == "approve":
2388
2396
  if args.expected_head is None:
2389
2397
  raise PipelineStateError(
@@ -458,6 +458,21 @@ def _validate_plan_briefs(repo: Path, kind: str, project_dir: str) -> None:
458
458
  if head.returncode != 0:
459
459
  # Pre-Git approval defers base-dependent checks to build.
460
460
  return
461
+ if project_dir == ".project":
462
+ # Unsupported .project entries must surface at approval, before build,
463
+ # where removing them is cheapest (#466). Like the checkpoint, judge
464
+ # each git-visible entry by whether it still exists in the worktree.
465
+ project = repo / ".project"
466
+ unsupported = sorted(
467
+ name
468
+ for name in pipeline_state._common.git_visible_entries(project) - PROJECT_ENTRIES
469
+ if (project / name).exists() or (project / name).is_symlink()
470
+ )
471
+ if unsupported:
472
+ raise PipelineStateError(
473
+ "unsupported .project artifacts: " + ", ".join(unsupported)
474
+ + "; remove or relocate them before approving the plan"
475
+ )
461
476
  if __package__:
462
477
  from .check_task_briefs import BriefError, _member_bases, plan_validation_bases, validate_task_briefs
463
478
  else:
@@ -128,7 +128,76 @@ def manifest_digest(manifest: dict) -> str:
128
128
  return hashlib.sha256(json.dumps(manifest, sort_keys=True, separators=(",", ":")).encode()).hexdigest()
129
129
 
130
130
 
131
+ def manifest_successors(manifest: dict) -> tuple:
132
+ """The validated successor digests a runtime's hashed manifest names (#468).
133
+
134
+ `successors` is an optional list of unique 64-hex lowercase store digests.
135
+ It lives inside the manifest, so the runtime's own digest covers it: a
136
+ runtime can only bless successors its hashed bytes name. A malformed
137
+ field makes the whole runtime invalid — acceptance never widens on a
138
+ shape the store cannot prove.
139
+ """
140
+ successors = manifest.get("successors", [])
141
+ if (not isinstance(successors, list)
142
+ or not all(
143
+ isinstance(item, str) and len(item) == 64
144
+ and all(char in "0123456789abcdef" for char in item)
145
+ for item in successors
146
+ )
147
+ or len(set(successors)) != len(successors)):
148
+ raise ValueError("invalid manifest successors")
149
+ return tuple(successors)
150
+
151
+
152
+ def accepted_runtime_dirs(repo: Path) -> set:
153
+ """The pinned runtime's directory plus every validated successor (#468).
154
+
155
+ Owner-ruled option (i) on #468: acceptance after ship is anchored in the
156
+ pinned runtime's own hashed manifest. It names successor digests; each
157
+ hop is validated exactly like the pin and may name further successors.
158
+ A named entry that is missing or fails validation simply does not widen
159
+ the set — the pinned runtime keeps working — and a visited set ends
160
+ blessed cycles. Nothing outside the pin's hashed reach is ever accepted.
161
+ """
162
+ pin = declaration(repo)
163
+ pin_root, pin_manifest = _validated_manifest(pin)
164
+ accepted = {pin_root.resolve()}
165
+ seen = {pin["digest"]}
166
+ pending = list(manifest_successors(pin_manifest))
167
+ while pending:
168
+ digest = pending.pop()
169
+ if digest in seen:
170
+ continue
171
+ seen.add(digest)
172
+ try:
173
+ # version None: the hop's trust anchor is the digest the pinned
174
+ # bytes named, never a version claim. The successors followed
175
+ # come from the same manifest read whose digest was proven —
176
+ # a mid-walk entry swap cannot smuggle a different list.
177
+ root, manifest = _validated_manifest(
178
+ {"schema": RUNTIME_SCHEMA, "version": None, "digest": digest}
179
+ )
180
+ named = manifest_successors(manifest)
181
+ except (OSError, ValueError, TypeError):
182
+ continue
183
+ pending.extend(named)
184
+ accepted.add(root.resolve())
185
+ return accepted
186
+
187
+
131
188
  def validate_runtime(data: dict) -> Path:
189
+ return _validated_manifest(data)[0]
190
+
191
+
192
+ def _validated_manifest(data: dict) -> tuple:
193
+ """validate_runtime, returning the exact manifest whose digest was proven.
194
+
195
+ Callers that act on manifest content (the #468 successor walk) must use
196
+ this returned object, never a second read of manifest.json: a concurrent
197
+ swap between reads could otherwise validate one manifest while following
198
+ successors from another. `version` None anchors on the digest alone —
199
+ used by the walk for hops whose version no declaration supplies.
200
+ """
132
201
  root = runtime_home() / data["digest"]
133
202
  hint = (f"Runtime {data['version']} ({data['digest']}) is unavailable or invalid at {root}. "
134
203
  "Run gsd-path --runtime-restore --project <project-root> "
@@ -141,7 +210,8 @@ def validate_runtime(data: dict) -> Path:
141
210
  raise ValueError("symlinked manifest")
142
211
  manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
143
212
  if (not isinstance(manifest, dict) or manifest_digest(manifest) != data["digest"]
144
- or manifest.get("version") != data["version"]):
213
+ or (data["version"] is not None
214
+ and manifest.get("version") != data["version"])):
145
215
  raise ValueError("manifest identity mismatch")
146
216
  files = manifest.get("files")
147
217
  if not isinstance(files, dict) or "pipeline_state.py" not in files:
@@ -164,9 +234,10 @@ def validate_runtime(data: dict) -> Path:
164
234
  runtime_entries.add(path.name)
165
235
  if runtime_entries != set(files) | {"manifest.json"}:
166
236
  raise ValueError("unexpected runtime files")
237
+ manifest_successors(manifest)
167
238
  except (OSError, ValueError, TypeError) as error:
168
239
  raise ValueError(hint + f" ({error})") from error
169
- return root
240
+ return root, manifest
170
241
 
171
242
 
172
243
  def resolve_runtime(repo: Path) -> Path:
@@ -43,7 +43,8 @@ def _task_member(repo: Path, project_dir: str, task_id: str):
43
43
 
44
44
  def run_workflow(repo: Path, action: str, project_dir: str, expected_head: str = None,
45
45
  task_id: str = None, round_size: int = None, inspection: Path = None,
46
- mapper_reviewed: bool = False, kind: str = None) -> dict:
46
+ mapper_reviewed: bool = False, kind: str = None,
47
+ rerun: bool = False) -> dict:
47
48
  steps = []
48
49
  outputs = {}
49
50
  scripts = Path(__file__).resolve().parent
@@ -231,7 +232,8 @@ def run_workflow(repo: Path, action: str, project_dir: str, expected_head: str =
231
232
  if pending["pending"]:
232
233
  raise StepFailed("pending discussion requires its owner disposition")
233
234
  step("build_state.py", "verify-landed", *common, "--head", expected_head)
234
- step("lean_verification.py", "--repo", str(repo), "--expected-head", expected_head)
235
+ rerun_flag = ("--rerun",) if rerun else ()
236
+ step("lean_verification.py", "--repo", str(repo), "--expected-head", expected_head, *rerun_flag)
235
237
  elif action == "preauthorize":
236
238
  # A block here is not a failure: the gate asks its owner as usual.
237
239
  next_step = "ask the owner at this gate"
@@ -302,6 +304,9 @@ def main(argv=None):
302
304
  parser.add_argument("--inspection", type=Path)
303
305
  parser.add_argument("--mapper-reviewed", action="store_true")
304
306
  parser.add_argument("--kind", choices=("intent", "plan"))
307
+ parser.add_argument("--rerun", action="store_true",
308
+ help="prepare-final: re-execute project Verify at the same head after a "
309
+ "host-caused failure, appending a new ledger entry")
305
310
  arguments = parser.parse_args(argv)
306
311
  if arguments.action in {"approve-plan", "build-evidence", "prepare-task", "prepare-final", "prepare-inspect", "finish-inspect"} and not arguments.expected_head:
307
312
  parser.error(f"{arguments.action} requires --expected-head")
@@ -309,10 +314,12 @@ def main(argv=None):
309
314
  parser.error("prepare-task requires --task-id and --round-size")
310
315
  if arguments.action == "preauthorize" and not arguments.kind:
311
316
  parser.error("preauthorize requires --kind")
317
+ if arguments.rerun and arguments.action != "prepare-final":
318
+ parser.error(f"--rerun applies only to prepare-final, not {arguments.action}")
312
319
  result = run_workflow(arguments.repo.resolve(), arguments.action,
313
320
  arguments.project_dir, arguments.expected_head,
314
321
  arguments.task_id, arguments.round_size, arguments.inspection,
315
- arguments.mapper_reviewed, arguments.kind)
322
+ arguments.mapper_reviewed, arguments.kind, arguments.rerun)
316
323
  print(json.dumps(result, sort_keys=True))
317
324
  return 0 if result["status"] == "complete" else 1
318
325
 
@@ -7,11 +7,13 @@ import sys
7
7
  # Runtime helpers must not modify their immutable installation.
8
8
  sys.dont_write_bytecode = True
9
9
 
10
+ import contextlib
10
11
  import hashlib
11
12
  import json
12
13
  import os
13
14
  import tempfile
14
15
  from pathlib import Path
16
+ from typing import Optional
15
17
 
16
18
  if __package__:
17
19
  from . import _common
@@ -22,7 +24,39 @@ else:
22
24
  from scripts import _common
23
25
 
24
26
 
27
+ # Run-scoped cache for read-only consumers (#469): recovery resolves a
28
+ # placement per task, and each resolution lists the whole worktree registry
29
+ # and re-reads the placement receipt. Within one read-only run none of that
30
+ # can change, so the listing and the derived workspace answer once. Flows
31
+ # that create, move, or retire worktrees never enter this scope.
32
+ _PLACEMENT_CACHE: Optional[dict] = None
33
+
34
+
35
+ @contextlib.contextmanager
36
+ def readonly_placement_cache():
37
+ global _PLACEMENT_CACHE
38
+ if _PLACEMENT_CACHE is not None: # already inside a read-only run
39
+ yield
40
+ return
41
+ _PLACEMENT_CACHE = {"porcelain": {}, "workspace": {}}
42
+ try:
43
+ yield
44
+ finally:
45
+ _PLACEMENT_CACHE = None
46
+
47
+
25
48
  def _worktree_list_porcelain(primary: Path) -> tuple[str, bool]:
49
+ if _PLACEMENT_CACHE is not None:
50
+ key = str(primary)
51
+ if key in _PLACEMENT_CACHE["porcelain"]:
52
+ return _PLACEMENT_CACHE["porcelain"][key] # type: ignore[index]
53
+ output = _worktree_list_porcelain_uncached(primary)
54
+ _PLACEMENT_CACHE["porcelain"][key] = output
55
+ return output
56
+ return _worktree_list_porcelain_uncached(primary)
57
+
58
+
59
+ def _worktree_list_porcelain_uncached(primary: Path) -> tuple[str, bool]:
26
60
  try:
27
61
  if __package__:
28
62
  from . import pipeline_git as pg
@@ -58,6 +92,17 @@ def _identity(path: Path) -> str:
58
92
 
59
93
 
60
94
  def _workspace(primary: Path, pin: bool) -> Path:
95
+ if _PLACEMENT_CACHE is not None and not pin:
96
+ key = str(primary)
97
+ if key in _PLACEMENT_CACHE["workspace"]:
98
+ return _PLACEMENT_CACHE["workspace"][key] # type: ignore[index]
99
+ workspace = _workspace_uncached(primary, False)
100
+ _PLACEMENT_CACHE["workspace"][key] = workspace
101
+ return workspace
102
+ return _workspace_uncached(primary, pin)
103
+
104
+
105
+ def _workspace_uncached(primary: Path, pin: bool) -> Path:
61
106
  common = Path(_git(primary, "rev-parse", "--git-common-dir"))
62
107
  common = (primary / common).resolve()
63
108
  identity = _identity(primary)
@@ -123,6 +123,8 @@ paragraph the user signed off on — do not edit without a new sign-off.>
123
123
 
124
124
  ## Corrections
125
125
 
126
- <!-- Verbatim user corrections from playback and later phases. Append-only.
127
- These are the highest-signal intent data in the file. -->
128
- - <date>: "<what the user said>"
126
+ <!-- Verbatim user corrections from playback and later phases. Append-only,
127
+ numbered from C1: the id gives a correction citable identity — wave
128
+ dispositions and scope exclusions authorize through it. These are the
129
+ highest-signal intent data in the file. -->
130
+ - C1 <YYYY-MM-DD>: "<what the user said>"
@@ -35,8 +35,9 @@ Project verify: `<command that installs dependencies, builds + tests the whole p
35
35
  Goal: <what this wave proves; if it fails, what changes>
36
36
  Review depth: full <!-- full | deep | verify-only. Wave 1 and auth/payments/
37
37
  data migration/concurrency waves must be full or
38
- deep. Assign deep sparingly: irreversible or
39
- security-critical merges. -->
38
+ deep. Use full unless the user explicitly requested
39
+ adversarial review, recorded in INTENT.md. Risk alone
40
+ does not authorize deep. -->
40
41
 
41
42
  <!-- No task table. Task id, title, deps, and files live only in each
42
43
  `tasks/T###-slug.md` frontmatter; `wave: N` there assigns it here. -->
@@ -3,15 +3,22 @@ id: T001
3
3
  title: <imperative, specific>
4
4
  wave: 1
5
5
  deps: [] # task ids whose output or landed effect this task needs
6
- status: pending # orchestrator-owned: pending | in-progress | done | failed | blocked
6
+ status: pending # orchestrator-owned: pending | in-progress | done | failed | blocked | withdrawn
7
7
  agent: null # orchestrator-owned: set at dispatch
8
8
  base: null # orchestrator-owned: clean layer SHA for isolated Verify
9
9
  worktree: null # orchestrator-owned: isolated task worktree while active
10
10
  task_branch: null # orchestrator-owned: gsd-path-task/<id> while parallel; null when serial
11
+ # member_base: <sha> # orchestrator-owned, member-repo tasks only: the member repository's starting commit
12
+ # reads: # optional read-only cross-repository inputs, one per line (#471):
13
+ # - web: .github/workflows/build.yml # a member path at that member's validation base
14
+ # - coordinator: docs/decisions.tsv # from a member task: the coordinator's layer base
11
15
  # Optional explicit dispatch overrides: model: <advertised slug|inherit>
12
16
  # effort: <advertised effort|inherit>
13
17
  # repo: <member> # multi-repo only: the MEMBERS.md member this task changes; omit for the coordinator
14
18
  # supersedes: [<task id>] # only when INTENT requires it: earlier-wave tasks whose behavior this task replaces; retires their Verify
19
+ # Withdrawn — <YYYY-MM-DD>: "<owner ruling verbatim>" # only in the Log, only
20
+ # after an owner ruling: status: withdrawn keeps the id in inventory with no
21
+ # dispatch and no landing; the citation line is mandatory or the gate fails (#483)
15
22
  files: # every file this task may touch — dispatch checks overlap
16
23
  - <path>
17
24
  ---
@@ -31,15 +31,25 @@ Tasks reviewed: <count>
31
31
  the task file. An evidence line starts with `- ✅ ` (pass) or `- ❌ `
32
32
  (fail). A section needs one or more lines with the marker of its own
33
33
  verdict, so a failed task needs a `- ❌ ` criterion line.
34
- Per task criterion: copy every word of the acceptance/interface
35
- criterion before the em dash; join wrapped lines and sub-bullets with
36
- spaces (the `- ` markers of sub-bullets are optional). An AC id or
37
- summary alone cannot be matched by review_findings.py.
34
+ On a `- ✅ ` line, name the criterion either by copying every word of
35
+ the acceptance/interface criterion before the em dash (join wrapped
36
+ lines and sub-bullets with spaces; the `- ` markers of sub-bullets are
37
+ optional) or by citing its stable id in the task's numbered list
38
+ (`T001/AC1`, `T001/IF2`) together with the evidence. A `- ❌ ` line
39
+ always copies the criterion verbatim: review_findings.py groups
40
+ findings by that text.
38
41
  Write literal markup and code in backticks inside a sentence: text
39
42
  such as <b> outside backticks reads as an unfilled placeholder. -->
40
43
  - ✅ <criterion> — <evidence: verify output / file:line checked>
41
44
  - ❌ <criterion> — found: <what exists instead, file:line>
42
45
  fix: <concrete direction a coder can execute without re-investigating>
46
+ - <task id>/<ACn|IFn> — disposition: <authority and date> (<Cn>): <reason>
47
+ <!-- Only for a claim an INTENT.md correction defers or excludes; cite
48
+ its number (C1) — a disposition with no citation, or with a Cn
49
+ that INTENT.md `## Corrections` does not define, fails the gate. Every
50
+ acceptance criterion and interface bullet must otherwise appear
51
+ above, as evidence or a finding. One line may cite several ids
52
+ (T001/AC1, T001/AC2) when one observation covers them. -->
43
53
 
44
54
  Warnings (non-blocking):
45
55
  - <weakened test / unfailable verify / none>
@@ -52,18 +62,24 @@ Contract violations (blocking):
52
62
  ## Intent coverage
53
63
 
54
64
  <!-- One heading per INTENT.md success criterion owned by a task in this
55
- wave, ending `: pass` or `: fail`. `scripts/check_handoffs.py wave
56
- --review <this file>` gates the SC id set and verdicts. Omit this
57
- section when no task in the wave owns an SC.
65
+ wave, ending `: pass` or `: fail`, in ascending SC id order (SC2
66
+ before SC4), with an em dash (—) after the id. `scripts/check_handoffs.py
67
+ wave --review <this file>` gates the SC ids, their order, and
68
+ verdicts. Omit this section when no task in the wave owns an SC.
58
69
  Copy every word of the criterion from INTENT.md `## Success criteria`:
59
70
  join indented continuation lines and sub-bullets with a single space
60
71
  (the `- ` markers of sub-bullets are optional). The gate compares every
61
72
  other word: no paraphrase, no dropped sub-bullet.
62
73
  Each SC block needs an evidence line with the marker of its own
63
- verdict: `- ✅ ` (pass) or `- ❌ ` (fail). -->
74
+ verdict: `- ✅ ` (pass) or `- ❌ ` (fail).
75
+ Under a `pass` SC only, record each subclaim an INTENT.md correction
76
+ excludes as `- Excluded by <Cn>: <subclaim>`; the Cn must exist in
77
+ INTENT.md `## Corrections`. An exclusion records retained scope, not
78
+ a failure. -->
64
79
 
65
80
  ### SC1 — <criterion from INTENT.md>: <pass | fail>
66
81
  - ✅ <evidence: verify output / file:line checked>
82
+ - Excluded by <Cn>: <subclaim the correction removes>
67
83
  - ❌ <criterion> — found: <what exists instead, file:line>
68
84
  fix: <concrete direction a coder can execute without re-investigating>
69
85
  <!-- Quick-lane final scope (Review scope: final): add after the evidence line: