@opengsd/gsd-path 1.6.2 → 1.7.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 (275) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/DOCS.md +27 -1
  3. package/FULL.md +2 -1
  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 +9 -2
  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 +28 -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 +996 -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 +3 -1
  34. package/skills/gsd-path/DEFINE.md +2 -1
  35. package/skills/gsd-path/PLAN.md +10 -0
  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 +20 -3
  39. package/skills/gsd-path/references/coder.md +6 -3
  40. package/skills/gsd-path/references/reviewer-final.md +10 -0
  41. package/skills/gsd-path/references/reviewer-wave.md +61 -22
  42. package/skills/gsd-path/references/reviewer.md +8 -6
  43. package/skills/gsd-path/references/runtime-selection.md +36 -0
  44. package/skills/gsd-path/references/spec-probes.md +19 -1
  45. package/skills/gsd-path/scripts/_common.py +76 -2
  46. package/skills/gsd-path/scripts/archive_milestone.py +129 -7
  47. package/skills/gsd-path/scripts/build_recovery.py +25 -2
  48. package/skills/gsd-path/scripts/build_state.py +333 -8
  49. package/skills/gsd-path/scripts/check_handoffs.py +437 -30
  50. package/skills/gsd-path/scripts/check_task_briefs.py +135 -11
  51. package/skills/gsd-path/scripts/discussion_validate.py +46 -10
  52. package/skills/gsd-path/scripts/dispatch_driver.py +28 -3
  53. package/skills/gsd-path/scripts/git_guard.py +44 -4
  54. package/skills/gsd-path/scripts/integration.py +196 -39
  55. package/skills/gsd-path/scripts/isolation.py +996 -38
  56. package/skills/gsd-path/scripts/lean_verification.py +28 -13
  57. package/skills/gsd-path/scripts/loop_run.py +9 -4
  58. package/skills/gsd-path/scripts/members.py +17 -3
  59. package/skills/gsd-path/scripts/pipeline_state.py +10 -2
  60. package/skills/gsd-path/scripts/state_checkpoint.py +15 -0
  61. package/skills/gsd-path/scripts/status_runtime.py +73 -2
  62. package/skills/gsd-path/scripts/workflow_run.py +10 -3
  63. package/skills/gsd-path/scripts/worktree_paths.py +45 -0
  64. package/skills/gsd-path/templates/intent.md +5 -3
  65. package/skills/gsd-path/templates/task.md +8 -1
  66. package/skills/gsd-path/templates/wave-review.md +24 -8
  67. package/skills/gsd-path-build/SKILL.md +3 -1
  68. package/skills/gsd-path-build/references/build-native.md +20 -3
  69. package/skills/gsd-path-build/references/coder.md +6 -3
  70. package/skills/gsd-path-build/references/reviewer-final.md +10 -0
  71. package/skills/gsd-path-build/references/reviewer-wave.md +61 -22
  72. package/skills/gsd-path-build/references/reviewer.md +8 -6
  73. package/skills/gsd-path-build/references/runtime-selection.md +36 -0
  74. package/skills/gsd-path-build/references/spec-probes.md +19 -1
  75. package/skills/gsd-path-build/scripts/_common.py +76 -2
  76. package/skills/gsd-path-build/scripts/archive_milestone.py +129 -7
  77. package/skills/gsd-path-build/scripts/build_recovery.py +25 -2
  78. package/skills/gsd-path-build/scripts/build_state.py +333 -8
  79. package/skills/gsd-path-build/scripts/check_handoffs.py +437 -30
  80. package/skills/gsd-path-build/scripts/check_task_briefs.py +135 -11
  81. package/skills/gsd-path-build/scripts/discussion_validate.py +46 -10
  82. package/skills/gsd-path-build/scripts/dispatch_driver.py +28 -3
  83. package/skills/gsd-path-build/scripts/integration.py +196 -39
  84. package/skills/gsd-path-build/scripts/isolation.py +996 -38
  85. package/skills/gsd-path-build/scripts/loop_run.py +9 -4
  86. package/skills/gsd-path-build/scripts/members.py +17 -3
  87. package/skills/gsd-path-build/scripts/pipeline_state.py +10 -2
  88. package/skills/gsd-path-build/scripts/state_checkpoint.py +15 -0
  89. package/skills/gsd-path-build/scripts/workflow_run.py +10 -3
  90. package/skills/gsd-path-build/scripts/worktree_paths.py +45 -0
  91. package/skills/gsd-path-build/templates/task.md +8 -1
  92. package/skills/gsd-path-build/templates/wave-review.md +24 -8
  93. package/skills/gsd-path-decide/references/runtime-selection.md +36 -0
  94. package/skills/gsd-path-decide/scripts/_common.py +76 -2
  95. package/skills/gsd-path-decide/scripts/archive_milestone.py +129 -7
  96. package/skills/gsd-path-decide/scripts/build_recovery.py +25 -2
  97. package/skills/gsd-path-decide/scripts/check_handoffs.py +437 -30
  98. package/skills/gsd-path-decide/scripts/check_task_briefs.py +135 -11
  99. package/skills/gsd-path-decide/scripts/discussion_validate.py +46 -10
  100. package/skills/gsd-path-decide/scripts/integration.py +196 -39
  101. package/skills/gsd-path-decide/scripts/isolation.py +996 -38
  102. package/skills/gsd-path-decide/scripts/loop_run.py +9 -4
  103. package/skills/gsd-path-decide/scripts/members.py +17 -3
  104. package/skills/gsd-path-decide/scripts/pipeline_state.py +10 -2
  105. package/skills/gsd-path-decide/scripts/state_checkpoint.py +15 -0
  106. package/skills/gsd-path-decide/scripts/worktree_paths.py +45 -0
  107. package/skills/gsd-path-define/SKILL.md +2 -1
  108. package/skills/gsd-path-define/references/runtime-selection.md +36 -0
  109. package/skills/gsd-path-define/references/spec-probes.md +19 -1
  110. package/skills/gsd-path-define/scripts/_common.py +76 -2
  111. package/skills/gsd-path-define/scripts/archive_milestone.py +129 -7
  112. package/skills/gsd-path-define/scripts/build_recovery.py +25 -2
  113. package/skills/gsd-path-define/scripts/check_handoffs.py +437 -30
  114. package/skills/gsd-path-define/scripts/check_task_briefs.py +135 -11
  115. package/skills/gsd-path-define/scripts/discussion_validate.py +46 -10
  116. package/skills/gsd-path-define/scripts/integration.py +196 -39
  117. package/skills/gsd-path-define/scripts/isolation.py +996 -38
  118. package/skills/gsd-path-define/scripts/loop_run.py +9 -4
  119. package/skills/gsd-path-define/scripts/members.py +17 -3
  120. package/skills/gsd-path-define/scripts/pipeline_state.py +10 -2
  121. package/skills/gsd-path-define/scripts/state_checkpoint.py +15 -0
  122. package/skills/gsd-path-define/scripts/worktree_paths.py +45 -0
  123. package/skills/gsd-path-define/templates/intent.md +5 -3
  124. package/skills/gsd-path-discuss/references/runtime-selection.md +36 -0
  125. package/skills/gsd-path-discuss/scripts/_common.py +76 -2
  126. package/skills/gsd-path-discuss/scripts/loop_run.py +9 -4
  127. package/skills/gsd-path-discuss/scripts/worktree_paths.py +45 -0
  128. package/skills/gsd-path-docs-audit/references/runtime-selection.md +36 -0
  129. package/skills/gsd-path-docs-audit/scripts/_common.py +76 -2
  130. package/skills/gsd-path-docs-audit/scripts/archive_milestone.py +129 -7
  131. package/skills/gsd-path-docs-audit/scripts/build_recovery.py +25 -2
  132. package/skills/gsd-path-docs-audit/scripts/check_handoffs.py +437 -30
  133. package/skills/gsd-path-docs-audit/scripts/check_task_briefs.py +135 -11
  134. package/skills/gsd-path-docs-audit/scripts/discussion_validate.py +46 -10
  135. package/skills/gsd-path-docs-audit/scripts/integration.py +196 -39
  136. package/skills/gsd-path-docs-audit/scripts/isolation.py +996 -38
  137. package/skills/gsd-path-docs-audit/scripts/loop_run.py +9 -4
  138. package/skills/gsd-path-docs-audit/scripts/members.py +17 -3
  139. package/skills/gsd-path-docs-audit/scripts/pipeline_state.py +10 -2
  140. package/skills/gsd-path-docs-audit/scripts/state_checkpoint.py +15 -0
  141. package/skills/gsd-path-docs-audit/scripts/worktree_paths.py +45 -0
  142. package/skills/gsd-path-forensics/references/runtime-selection.md +36 -0
  143. package/skills/gsd-path-forensics/scripts/_common.py +76 -2
  144. package/skills/gsd-path-forensics/scripts/loop_run.py +9 -4
  145. package/skills/gsd-path-forensics/scripts/pipeline_diagnose.py +104 -1
  146. package/skills/gsd-path-forensics/scripts/worktree_paths.py +45 -0
  147. package/skills/gsd-path-inspect/references/runtime-selection.md +36 -0
  148. package/skills/gsd-path-inspect/scripts/_common.py +76 -2
  149. package/skills/gsd-path-inspect/scripts/archive_milestone.py +129 -7
  150. package/skills/gsd-path-inspect/scripts/build_recovery.py +25 -2
  151. package/skills/gsd-path-inspect/scripts/check_handoffs.py +437 -30
  152. package/skills/gsd-path-inspect/scripts/check_task_briefs.py +135 -11
  153. package/skills/gsd-path-inspect/scripts/discussion_validate.py +46 -10
  154. package/skills/gsd-path-inspect/scripts/integration.py +196 -39
  155. package/skills/gsd-path-inspect/scripts/isolation.py +996 -38
  156. package/skills/gsd-path-inspect/scripts/loop_run.py +9 -4
  157. package/skills/gsd-path-inspect/scripts/members.py +17 -3
  158. package/skills/gsd-path-inspect/scripts/pipeline_state.py +10 -2
  159. package/skills/gsd-path-inspect/scripts/state_checkpoint.py +15 -0
  160. package/skills/gsd-path-inspect/scripts/workflow_run.py +10 -3
  161. package/skills/gsd-path-inspect/scripts/worktree_paths.py +45 -0
  162. package/skills/gsd-path-loop/references/runtime-selection.md +36 -0
  163. package/skills/gsd-path-loop/scripts/_common.py +76 -2
  164. package/skills/gsd-path-loop/scripts/loop_run.py +9 -4
  165. package/skills/gsd-path-loop/scripts/worktree_paths.py +45 -0
  166. package/skills/gsd-path-migrate/references/runtime-selection.md +36 -0
  167. package/skills/gsd-path-migrate/scripts/_common.py +76 -2
  168. package/skills/gsd-path-migrate/scripts/loop_run.py +9 -4
  169. package/skills/gsd-path-plan/SKILL.md +10 -0
  170. package/skills/gsd-path-plan/references/runtime-selection.md +36 -0
  171. package/skills/gsd-path-plan/references/spec-probes.md +19 -1
  172. package/skills/gsd-path-plan/scripts/_common.py +76 -2
  173. package/skills/gsd-path-plan/scripts/archive_milestone.py +129 -7
  174. package/skills/gsd-path-plan/scripts/build_recovery.py +25 -2
  175. package/skills/gsd-path-plan/scripts/check_handoffs.py +437 -30
  176. package/skills/gsd-path-plan/scripts/check_task_briefs.py +135 -11
  177. package/skills/gsd-path-plan/scripts/discussion_validate.py +46 -10
  178. package/skills/gsd-path-plan/scripts/integration.py +196 -39
  179. package/skills/gsd-path-plan/scripts/isolation.py +996 -38
  180. package/skills/gsd-path-plan/scripts/loop_run.py +9 -4
  181. package/skills/gsd-path-plan/scripts/members.py +17 -3
  182. package/skills/gsd-path-plan/scripts/pipeline_state.py +10 -2
  183. package/skills/gsd-path-plan/scripts/state_checkpoint.py +15 -0
  184. package/skills/gsd-path-plan/scripts/workflow_run.py +10 -3
  185. package/skills/gsd-path-plan/scripts/worktree_paths.py +45 -0
  186. package/skills/gsd-path-plan/templates/task.md +8 -1
  187. package/skills/gsd-path-research/references/runtime-selection.md +36 -0
  188. package/skills/gsd-path-research/scripts/_common.py +76 -2
  189. package/skills/gsd-path-research/scripts/archive_milestone.py +129 -7
  190. package/skills/gsd-path-research/scripts/build_recovery.py +25 -2
  191. package/skills/gsd-path-research/scripts/check_handoffs.py +437 -30
  192. package/skills/gsd-path-research/scripts/check_task_briefs.py +135 -11
  193. package/skills/gsd-path-research/scripts/discussion_validate.py +46 -10
  194. package/skills/gsd-path-research/scripts/integration.py +196 -39
  195. package/skills/gsd-path-research/scripts/isolation.py +996 -38
  196. package/skills/gsd-path-research/scripts/loop_run.py +9 -4
  197. package/skills/gsd-path-research/scripts/members.py +17 -3
  198. package/skills/gsd-path-research/scripts/pipeline_state.py +10 -2
  199. package/skills/gsd-path-research/scripts/state_checkpoint.py +15 -0
  200. package/skills/gsd-path-research/scripts/worktree_paths.py +45 -0
  201. package/skills/gsd-path-roadmap/references/runtime-selection.md +36 -0
  202. package/skills/gsd-path-roadmap/scripts/_common.py +76 -2
  203. package/skills/gsd-path-roadmap/scripts/archive_milestone.py +129 -7
  204. package/skills/gsd-path-roadmap/scripts/build_recovery.py +25 -2
  205. package/skills/gsd-path-roadmap/scripts/check_handoffs.py +437 -30
  206. package/skills/gsd-path-roadmap/scripts/check_task_briefs.py +135 -11
  207. package/skills/gsd-path-roadmap/scripts/discussion_validate.py +46 -10
  208. package/skills/gsd-path-roadmap/scripts/integration.py +196 -39
  209. package/skills/gsd-path-roadmap/scripts/isolation.py +996 -38
  210. package/skills/gsd-path-roadmap/scripts/loop_run.py +9 -4
  211. package/skills/gsd-path-roadmap/scripts/members.py +17 -3
  212. package/skills/gsd-path-roadmap/scripts/pipeline_state.py +10 -2
  213. package/skills/gsd-path-roadmap/scripts/state_checkpoint.py +15 -0
  214. package/skills/gsd-path-roadmap/scripts/worktree_paths.py +45 -0
  215. package/skills/gsd-path-ship/SKILL.md +41 -8
  216. package/skills/gsd-path-ship/references/reviewer-final.md +10 -0
  217. package/skills/gsd-path-ship/references/reviewer-wave.md +61 -22
  218. package/skills/gsd-path-ship/references/reviewer.md +8 -6
  219. package/skills/gsd-path-ship/references/runtime-selection.md +36 -0
  220. package/skills/gsd-path-ship/references/spec-probes.md +19 -1
  221. package/skills/gsd-path-ship/scripts/_common.py +76 -2
  222. package/skills/gsd-path-ship/scripts/archive_milestone.py +129 -7
  223. package/skills/gsd-path-ship/scripts/build_recovery.py +25 -2
  224. package/skills/gsd-path-ship/scripts/build_state.py +333 -8
  225. package/skills/gsd-path-ship/scripts/check_handoffs.py +437 -30
  226. package/skills/gsd-path-ship/scripts/check_task_briefs.py +135 -11
  227. package/skills/gsd-path-ship/scripts/discussion_validate.py +46 -10
  228. package/skills/gsd-path-ship/scripts/integration.py +196 -39
  229. package/skills/gsd-path-ship/scripts/isolation.py +996 -38
  230. package/skills/gsd-path-ship/scripts/lean_verification.py +28 -13
  231. package/skills/gsd-path-ship/scripts/loop_run.py +9 -4
  232. package/skills/gsd-path-ship/scripts/members.py +17 -3
  233. package/skills/gsd-path-ship/scripts/pipeline_state.py +10 -2
  234. package/skills/gsd-path-ship/scripts/state_checkpoint.py +15 -0
  235. package/skills/gsd-path-ship/scripts/workflow_run.py +10 -3
  236. package/skills/gsd-path-ship/scripts/worktree_paths.py +45 -0
  237. package/skills/gsd-path-ship/templates/wave-review.md +24 -8
  238. package/skills/gsd-path-undo/references/runtime-selection.md +36 -0
  239. package/skills/gsd-path-undo/scripts/_common.py +76 -2
  240. package/skills/gsd-path-undo/scripts/loop_run.py +9 -4
  241. package/skills/gsd-path-undo/scripts/worktree_paths.py +45 -0
  242. package/skills/path/BUILD.md +3 -1
  243. package/skills/path/DEFINE.md +2 -1
  244. package/skills/path/PLAN.md +10 -0
  245. package/skills/path/SHIP.md +41 -8
  246. package/skills/path/SKILL.md +16 -1
  247. package/skills/path/references/build-native.md +20 -3
  248. package/skills/path/references/coder.md +6 -3
  249. package/skills/path/references/reviewer-final.md +10 -0
  250. package/skills/path/references/reviewer-wave.md +61 -22
  251. package/skills/path/references/reviewer.md +8 -6
  252. package/skills/path/references/runtime-selection.md +36 -0
  253. package/skills/path/references/spec-probes.md +19 -1
  254. package/skills/path/scripts/_common.py +76 -2
  255. package/skills/path/scripts/archive_milestone.py +129 -7
  256. package/skills/path/scripts/build_recovery.py +25 -2
  257. package/skills/path/scripts/build_state.py +333 -8
  258. package/skills/path/scripts/check_handoffs.py +437 -30
  259. package/skills/path/scripts/check_task_briefs.py +135 -11
  260. package/skills/path/scripts/discussion_validate.py +46 -10
  261. package/skills/path/scripts/dispatch_driver.py +28 -3
  262. package/skills/path/scripts/git_guard.py +44 -4
  263. package/skills/path/scripts/integration.py +196 -39
  264. package/skills/path/scripts/isolation.py +996 -38
  265. package/skills/path/scripts/lean_verification.py +28 -13
  266. package/skills/path/scripts/loop_run.py +9 -4
  267. package/skills/path/scripts/members.py +17 -3
  268. package/skills/path/scripts/pipeline_state.py +10 -2
  269. package/skills/path/scripts/state_checkpoint.py +15 -0
  270. package/skills/path/scripts/status_runtime.py +73 -2
  271. package/skills/path/scripts/workflow_run.py +10 -3
  272. package/skills/path/scripts/worktree_paths.py +45 -0
  273. package/skills/path/templates/intent.md +5 -3
  274. package/skills/path/templates/task.md +8 -1
  275. package/skills/path/templates/wave-review.md +24 -8
@@ -144,7 +144,13 @@ def settled_tasks(repo: Path) -> dict[str, tuple[str, dict]]:
144
144
  raise state.PipelineStateError(f"invalid recovery task path: {path.name}")
145
145
  text = state._read_real_file(path, path.name)
146
146
  fields = task_fields(text, path.name)
147
- if fields.get("status") not in {"pending", "done"}:
147
+ if fields.get("status") == "withdrawn":
148
+ # A cited tombstone is settled inventory; recovery skips it (#483).
149
+ if not state._common.withdrawn_citations(text):
150
+ raise state.PipelineStateError(
151
+ f"withdrawn task lacks an owner citation: {path.name}"
152
+ )
153
+ elif fields.get("status") not in {"pending", "done"}:
148
154
  raise state.PipelineStateError(f"settle task ownership before recovery: {path.name}")
149
155
  if fields["status"] == "pending" and any(
150
156
  fields.get(key) != "null" for key in ("agent", "base", "worktree", "task_branch")
@@ -250,7 +256,24 @@ def validate_review_backup(repo: Path, recovery: dict, directory: Path) -> None:
250
256
  for path in directory.rglob("*")
251
257
  if path.is_file() and not state._common.is_ignored_junk(path)} if directory.exists() else {}
252
258
  if actual != expected:
253
- raise state.PipelineStateError("review backup differs from the recovery base")
259
+ extra = sorted(set(actual) - set(expected))
260
+ changed = sorted(
261
+ name for name in set(actual) & set(expected) if actual[name] != expected[name]
262
+ )
263
+ missing = sorted(set(expected) - set(actual))
264
+ detail = ""
265
+ if extra:
266
+ detail += f" files in the review backup that the recovery base does not have: {', '.join(extra)}."
267
+ if changed:
268
+ detail += f" files differing from the recovery base: {', '.join(changed)}."
269
+ if missing:
270
+ detail += f" files at the recovery base missing from the review backup: {', '.join(missing)}."
271
+ raise state.PipelineStateError(
272
+ "review backup differs from the recovery base." + detail
273
+ + " prepare-build-recovery must run before re-planning touches any recovery"
274
+ " artifact; remove the extra files and restore the missing or changed ones"
275
+ " to the recovery base's content, then rerun prepare"
276
+ )
254
277
 
255
278
 
256
279
  def prepare(repo: Path) -> dict:
@@ -21,6 +21,7 @@ if __package__:
21
21
  IsolationError,
22
22
  recover as recover_isolation,
23
23
  verify_landed_task_files,
24
+ withdrawn_citations,
24
25
  )
25
26
  from .pipeline_git import task_commit_subject
26
27
  from .pipeline_state import PipelineStateError, load_state
@@ -33,6 +34,7 @@ else:
33
34
  IsolationError,
34
35
  recover as recover_isolation,
35
36
  verify_landed_task_files,
37
+ withdrawn_citations,
36
38
  )
37
39
  from pipeline_git import task_commit_subject
38
40
  from pipeline_state import PipelineStateError, load_state
@@ -45,6 +47,7 @@ else:
45
47
  IsolationError,
46
48
  recover as recover_isolation,
47
49
  verify_landed_task_files,
50
+ withdrawn_citations,
48
51
  )
49
52
  from scripts.pipeline_git import task_commit_subject
50
53
  from scripts.pipeline_state import PipelineStateError, load_state
@@ -63,7 +66,7 @@ REVIEW_VERDICT_RE = re.compile(
63
66
  )
64
67
  REVIEW_FINDINGS_HEADING = "Review findings"
65
68
  VERIFY_HEAVY_RE = re.compile(r"(?m)^Heavy:\s*(?P<value>yes|no)\s*(?:<!--.*-->)?\s*$")
66
- VALID_TASK_STATUSES = {"pending", "in-progress", "done", "failed", "blocked"}
69
+ VALID_TASK_STATUSES = {"pending", "in-progress", "done", "failed", "blocked", "withdrawn"}
67
70
  VERIFY_RESULTS = _common.VERIFY_RESULTS
68
71
 
69
72
 
@@ -271,6 +274,16 @@ def _validate_graph(tasks: Sequence[Task]) -> None:
271
274
  f"{task.task_id} depends on later-wave {dependency}",
272
275
  {"task": task.task_id, "dependency": dependency},
273
276
  )
277
+ if by_id[dependency].status == "withdrawn" and task.status not in (
278
+ "done",
279
+ "withdrawn",
280
+ ):
281
+ raise BuildStateError(
282
+ "withdrawn-dependency",
283
+ f"{task.task_id} depends on withdrawn {dependency}; re-point "
284
+ "the dependency or withdraw this task too (#483)",
285
+ {"task": task.task_id, "dependency": dependency},
286
+ )
274
287
 
275
288
  visiting: List[str] = []
276
289
  visited = set()
@@ -342,6 +355,15 @@ def _load_tasks(
342
355
  "plan-task-mismatch",
343
356
  f"{task.task_id} names missing PLAN.md Wave {task.wave}",
344
357
  )
358
+ if task.status == "withdrawn" and not withdrawn_citations(
359
+ path.read_text(encoding="utf-8")):
360
+ raise BuildStateError(
361
+ "uncited-withdrawn",
362
+ f"{task.task_id} is withdrawn without an owner citation; append "
363
+ "a `- Withdrawn — <YYYY-MM-DD>: <owner ruling verbatim>` Log "
364
+ "entry (#483)",
365
+ {"task": task.task_id},
366
+ )
345
367
  parsed.append(task)
346
368
  missing_waves = sorted(set(plan_waves) - {task.wave for task in parsed})
347
369
  if missing_waves:
@@ -431,6 +453,10 @@ def _validate_ready_metadata(project: Project) -> Dict[str, Mapping[str, object]
431
453
  seen_worktrees: Dict[str, str] = {}
432
454
  seen_branches: Dict[str, str] = {}
433
455
  for task in project.tasks:
456
+ if task.status == "withdrawn":
457
+ # A tombstone is inventory, not work: no dispatch metadata is
458
+ # expected and recovery reports it as none (#483).
459
+ continue
434
460
  if task.status == "pending":
435
461
  if any(
436
462
  value is not None
@@ -479,7 +505,10 @@ def _validate_ready_metadata(project: Project) -> Dict[str, Mapping[str, object]
479
505
  _invalid_state(task, f"worktree is also owned by {previous}")
480
506
  continue
481
507
  if task.status == "done":
482
- if any(by_id[dependency].status != "done" for dependency in task.deps):
508
+ if any(
509
+ by_id[dependency].status not in ("done", "withdrawn")
510
+ for dependency in task.deps
511
+ ):
483
512
  _invalid_state(task, "done task has an incomplete dependency")
484
513
  if task.agent is None or task.base is None:
485
514
  _invalid_state(task, "done task is missing agent or base")
@@ -679,7 +708,11 @@ def _unreviewed_waves(project: Project, waves: Sequence[int]) -> List[int]:
679
708
  review_dir = project.tasks_dir.parent / "review"
680
709
  reviewed = {
681
710
  wave for wave in waves
682
- if all(task.review_findings for task in project.tasks if task.wave == wave)
711
+ if all(
712
+ task.review_findings
713
+ for task in project.tasks
714
+ if task.wave == wave and task.status != "withdrawn"
715
+ )
683
716
  }
684
717
  if review_dir.is_dir() and not review_dir.is_symlink():
685
718
  for path in review_dir.iterdir():
@@ -696,7 +729,12 @@ def ready(repo: str, project_dir: str = DEFAULT_PROJECT_DIR, *,
696
729
  project = _load_project(repo, project_dir, ("active", "done") if allow_done else ("active",))
697
730
  reports = _validate_ready_metadata(project)
698
731
  by_id = {task.task_id: task for task in project.tasks}
699
- unfinished_waves = sorted({task.wave for task in project.tasks if task.status != "done"})
732
+ # A withdrawn tombstone carries no obligation: its wave is finished
733
+ # unless live work remains in it (#483).
734
+ unfinished_waves = sorted({
735
+ task.wave for task in project.tasks
736
+ if task.status != "done" and task.status != "withdrawn"
737
+ })
700
738
  current_wave = unfinished_waves[0] if unfinished_waves else None
701
739
  blocked_reviews = _blocked_wave_reviews(project)
702
740
  blocked_wave = min(
@@ -705,7 +743,10 @@ def ready(repo: str, project_dir: str = DEFAULT_PROJECT_DIR, *,
705
743
  for wave in blocked_reviews
706
744
  if (current_wave is None or wave < current_wave)
707
745
  and any(task.wave == wave for task in project.tasks)
708
- and all(task.status == "done" for task in project.tasks if task.wave == wave)
746
+ and all(
747
+ task.status in ("done", "withdrawn")
748
+ for task in project.tasks if task.wave == wave
749
+ )
709
750
  ),
710
751
  default=None,
711
752
  )
@@ -716,7 +757,7 @@ def ready(repo: str, project_dir: str = DEFAULT_PROJECT_DIR, *,
716
757
  repairs = [
717
758
  task
718
759
  for task in project.tasks
719
- if task.status != "done"
760
+ if task.status != "done" and task.status != "withdrawn"
720
761
  and task.wave > blocked_wave
721
762
  and task.review_findings
722
763
  and source_ids.intersection(task.deps)
@@ -785,7 +826,8 @@ def ready(repo: str, project_dir: str = DEFAULT_PROJECT_DIR, *,
785
826
  ],
786
827
  }
787
828
  for task in project.tasks
788
- if task.wave == current_wave and task.status != "done"
829
+ if task.wave == current_wave
830
+ and task.status != "done" and task.status != "withdrawn"
789
831
  ]
790
832
  raise BuildStateError(
791
833
  "dependency-deadlock",
@@ -946,6 +988,8 @@ def verify_landed_tasks(
946
988
  canonical_project_dir=canonical_dir,
947
989
  )
948
990
  for task in tasks:
991
+ if task.status == "withdrawn":
992
+ continue # A cited tombstone ships without a landing (#483)
949
993
  if task.status != "done":
950
994
  _invalid_state(task, "task must have status: done before shipping")
951
995
  try:
@@ -965,6 +1009,8 @@ def verify_landed_tasks(
965
1009
  }
966
1010
  evidence = []
967
1011
  for task in tasks:
1012
+ if task.status == "withdrawn":
1013
+ continue # A cited tombstone has no landing to prove (#483)
968
1014
  report = reports.get(task.task_id)
969
1015
  if report is None or report.get("verdict") not in LANDED_VERDICTS:
970
1016
  _invalid_state(task, "task landing is not canonical")
@@ -1044,10 +1090,13 @@ def _ledger_entries(path: Path) -> List[Dict[str, object]]:
1044
1090
 
1045
1091
  def verify_record(repo: str, command: str, commit: str, result: str,
1046
1092
  execution: Optional[Dict[str, object]] = None,
1047
- member: Optional[str] = None) -> Dict[str, object]:
1093
+ member: Optional[str] = None,
1094
+ rerun: bool = False) -> Dict[str, object]:
1048
1095
  """Append one verify run (command, commit, result, timestamp) to the ledger.
1049
1096
 
1050
1097
  A member run names its member and a commit in that member's repository.
1098
+ ``rerun`` marks an owner-requested re-execution at the same command and
1099
+ commit, so the ledger shows the earlier entry was superseded, not erased.
1051
1100
  """
1052
1101
 
1053
1102
  repository = _repo_root(repo)
@@ -1066,6 +1115,7 @@ def verify_record(repo: str, command: str, commit: str, result: str,
1066
1115
  "result": result,
1067
1116
  "recorded_at": datetime.now(timezone.utc).isoformat(timespec="seconds"),
1068
1117
  **({"repo": member} if member else {}),
1118
+ **({"rerun": True} if rerun else {}),
1069
1119
  }
1070
1120
  if execution is not None:
1071
1121
  if (not isinstance(execution.get("stdout"), str)
@@ -1073,6 +1123,18 @@ def verify_record(repo: str, command: str, commit: str, result: str,
1073
1123
  or type(execution.get("exit_code")) is not int
1074
1124
  or (execution["exit_code"] == 0) != (result == "pass")):
1075
1125
  raise BuildStateError("invalid-execution", "verification output and result disagree")
1126
+ if "cwd" in execution and not isinstance(execution["cwd"], str):
1127
+ raise BuildStateError("invalid-execution", "execution cwd must be a path string")
1128
+ if "base" in execution and not re.fullmatch(r"[0-9a-f]{40}", str(execution["base"])):
1129
+ raise BuildStateError("invalid-execution", "execution base must be a commit sha")
1130
+ location = execution.get("location")
1131
+ if isinstance(location, str) and "cwd" in execution and "worktree" in execution:
1132
+ if location.startswith("sidecar") and execution["cwd"] == execution["worktree"]:
1133
+ raise BuildStateError("invalid-execution",
1134
+ "a sidecar Verify cannot name the task checkout as its directory")
1135
+ if location.startswith("isolate") and execution["cwd"] != execution["worktree"]:
1136
+ raise BuildStateError("invalid-execution",
1137
+ "an isolate Verify must name its task worktree as its directory")
1076
1138
  entry["execution"] = execution
1077
1139
  path.parent.mkdir(parents=True, exist_ok=True)
1078
1140
  previous = path.read_text(encoding="utf-8") if path.exists() else ""
@@ -1095,6 +1157,255 @@ def verify_lookup(repo: str, command: str, commit: str, member: Optional[str] =
1095
1157
  }
1096
1158
 
1097
1159
 
1160
+ VERIFY_LOG_RE = re.compile(r"orchestrator Verify \((?:isolate|sidecar) [^)]+\): (pass|fail), exit \d+")
1161
+ MEMBER_RECORD_RE = re.compile(r"^Member: (\S+) ([0-9a-f]{40}) ([0-9a-f]{40})$", re.MULTILINE)
1162
+ GAP_MEMBER_RE = re.compile(r"^Member reviewed HEAD: (\S+) ([0-9a-f]{40})$", re.MULTILINE)
1163
+ GAP_HEAD_RE = re.compile(r"^Reviewed HEAD: ([0-9a-f]{40})$", re.MULTILINE)
1164
+ GAP_VERDICT_RE = re.compile(r"^Gap verdict: (pass|blocked)$", re.MULTILINE)
1165
+
1166
+
1167
+ def _proven_landings(
1168
+ repository: Path,
1169
+ done_paths: Dict[str, Path],
1170
+ canonical_tasks_dir: str,
1171
+ head: str,
1172
+ ) -> Tuple[Dict[str, str], Dict[str, str]]:
1173
+ """Map each provably landed task id to its landing commit and record proof failures."""
1174
+
1175
+ def collect(payload: Mapping[str, object]) -> Dict[str, str]:
1176
+ proven: Dict[str, str] = {}
1177
+ for item in payload["tasks"]: # type: ignore[index]
1178
+ if not isinstance(item, Mapping):
1179
+ continue
1180
+ if str(item.get("verdict")) in LANDED_VERDICTS and item.get("commit"):
1181
+ proven[str(item.get("task_id"))] = str(item["commit"])
1182
+ return proven
1183
+
1184
+ try:
1185
+ batch = verify_landed_task_files(repository, list(done_paths.values()), canonical_tasks_dir, head)
1186
+ return collect(batch), {}
1187
+ except IsolationError:
1188
+ pass
1189
+ proven: Dict[str, str] = {}
1190
+ errors: Dict[str, str] = {}
1191
+ for task_id, path in sorted(done_paths.items()):
1192
+ try:
1193
+ single = verify_landed_task_files(repository, [path], canonical_tasks_dir, head)
1194
+ except IsolationError as error:
1195
+ errors[task_id] = str(error)
1196
+ continue
1197
+ found = collect(single)
1198
+ if task_id in found:
1199
+ proven[task_id] = found[task_id]
1200
+ return proven, errors
1201
+
1202
+
1203
+ def _ledger_absence_reason(
1204
+ repository: Path,
1205
+ task: Task,
1206
+ landing: str,
1207
+ member_landing: Optional[str],
1208
+ member_base: Optional[str],
1209
+ ) -> str:
1210
+ """Explain why a passing task has no reusable ledger row."""
1211
+
1212
+ if task.repo:
1213
+ if not member_landing or not member_base:
1214
+ return "member record does not name the member landing"
1215
+ try:
1216
+ checkout = _member_checkout(repository, task.repo)
1217
+ parent = _git_output(checkout, "rev-parse", f"{member_landing}^")
1218
+ except BuildStateError:
1219
+ return "member checkout unavailable — the record condition cannot be re-derived"
1220
+ return ("ledger row absent although the member landing meets the record condition"
1221
+ if parent == member_base else
1222
+ "member landing parent differs from the member base — row deliberately not written")
1223
+ parent = _git_output(repository, "rev-parse", f"{landing}^")
1224
+ return ("ledger row absent although the landing meets the record condition"
1225
+ if parent == task.base else
1226
+ "landing parent differs from the recorded base — row deliberately not written")
1227
+
1228
+
1229
+ def evidence_manifest(
1230
+ repo: str,
1231
+ project_dir: str,
1232
+ head: str,
1233
+ canonical_project_dir: str = DEFAULT_PROJECT_DIR,
1234
+ ) -> Dict[str, object]:
1235
+ """Join every task to its authoritative accepted evidence and classify the result.
1236
+
1237
+ States: `ledger` (a reusable verify-ledger row matches the task's Verify command
1238
+ at its proven landing — for member tasks, at the member landing recorded in the
1239
+ coordinator record body), `log-only` (the retained task Log records the isolated
1240
+ Verify pass and the landing is proven, but no reusable row exists), `rejected`
1241
+ (the Log records a failed Verify, or a ledger row at the landing failed), and
1242
+ `superseded` / `missing` (a plan-approved supersedes retired the Verify, or no
1243
+ durable evidence exists at this head), plus `withdrawn` (a cited owner-approved
1244
+ tombstone that never owed a landing — distinct from every completed state). A pass is never inferred from a Log label
1245
+ alone: done tasks require a proven first-parent landing, and member rows must
1246
+ match the recorded member landing. The project section reports the member
1247
+ milestone project Verify only when the project Verify gap (final-gap-1.md)
1248
+ records a passing verdict at exactly this head (`gap-embedded`), or a reusable ledger row matches PLAN's
1249
+ project Verify command at this head (`ledger`); a blocked gap verdict or a
1250
+ different reviewed HEAD is listed but never accepted. Limits: failed-attempt
1251
+ executions kept only in uncommitted dispatch records are not consulted, and a
1252
+ noncanonical task artifact aborts with `invalid-task-file` naming the file — the
1253
+ same strictness as every other build_state command; fix or flag the file first.
1254
+ """
1255
+
1256
+ repository = _repo_root(repo)
1257
+ artifact_dir, artifact_path = _project_path(repository, project_dir)
1258
+ canonical_dir, _ = _project_path(repository, canonical_project_dir)
1259
+ if not FULL_SHA_RE.fullmatch(head) or not _commit_resolves(repository, head):
1260
+ raise BuildStateError("invalid-head", "--head must be a full, existing commit SHA")
1261
+ if _git_output(repository, "rev-parse", f"{head}^{{commit}}") != head:
1262
+ raise BuildStateError("invalid-head", "--head must resolve to itself")
1263
+ tasks = _load_tasks(
1264
+ repository, artifact_dir, artifact_path, canonical_project_dir=canonical_dir
1265
+ )
1266
+ superseded_by: Dict[str, str] = {}
1267
+ for task in tasks:
1268
+ if task.status != "done":
1269
+ continue
1270
+ for earlier in task.supersedes:
1271
+ superseded_by.setdefault(earlier, task.task_id)
1272
+ done_paths = {
1273
+ task.task_id: artifact_path / "tasks" / Path(task.task_file).name
1274
+ for task in tasks if task.status == "done"
1275
+ }
1276
+ canonical_tasks_dir = str(PurePosixPath(canonical_dir) / "tasks")
1277
+ proven, proof_errors = (
1278
+ _proven_landings(repository, done_paths, canonical_tasks_dir, head) if done_paths else ({}, {})
1279
+ )
1280
+ ledger_path = _ledger_path(repository)
1281
+ entries = _ledger_entries(ledger_path) if ledger_path.exists() else []
1282
+ manifest: List[Dict[str, object]] = []
1283
+ for task in tasks:
1284
+ text = _read_file(
1285
+ artifact_path / "tasks" / Path(task.task_file).name,
1286
+ f"{artifact_dir}/tasks/{Path(task.task_file).name}",
1287
+ )
1288
+ results = VERIFY_LOG_RE.findall(text)
1289
+ entry: Dict[str, object] = {
1290
+ "task": task.task_id,
1291
+ "title": task.title,
1292
+ "status": task.status,
1293
+ "repo": task.repo or None,
1294
+ "verify_command": _common.task_verify_command(text),
1295
+ "superseded_by": superseded_by.get(task.task_id),
1296
+ }
1297
+ manifest.append(entry)
1298
+ if task.status == "withdrawn":
1299
+ # The tombstone stays distinct from completed acceptance: the
1300
+ # manifest records the withdrawal and its owner citations (#483).
1301
+ entry["state"] = "withdrawn"
1302
+ entry["reason"] = "; ".join(withdrawn_citations(text))
1303
+ continue
1304
+ if task.status != "done":
1305
+ entry["state"] = "rejected" if "fail" in results else "missing"
1306
+ entry["reason"] = "verify failed" if "fail" in results else "task never landed"
1307
+ if "fail" in results:
1308
+ entry["receipt"] = {"source": "task-log", "path": task.task_file}
1309
+ continue
1310
+ landing = proven.get(task.task_id)
1311
+ if landing is None:
1312
+ entry["state"] = "missing"
1313
+ entry["reason"] = ("landing proof failed: " + proof_errors[task.task_id]
1314
+ if task.task_id in proof_errors
1315
+ else "done task without a provable first-parent landing at this head")
1316
+ continue
1317
+ entry["landing"] = landing
1318
+ member_landing: Optional[str] = None
1319
+ member_base: Optional[str] = None
1320
+ if task.repo:
1321
+ body = _git_output(repository, "log", "-1", "--format=%b", landing)
1322
+ record = MEMBER_RECORD_RE.search(body)
1323
+ if record is None or record.group(1) != task.repo:
1324
+ entry["state"] = "missing"
1325
+ entry["reason"] = "member record commit does not name this member's landing"
1326
+ continue
1327
+ member_landing, member_base = record.group(2), record.group(3)
1328
+ lookup_commit = member_landing or landing
1329
+ if not entry["verify_command"]:
1330
+ entry["state"] = "missing"
1331
+ entry["reason"] = "landed task without a Verify command"
1332
+ continue
1333
+ try:
1334
+ lookup = verify_lookup(str(repository), str(entry["verify_command"]), lookup_commit,
1335
+ task.repo or None)
1336
+ except BuildStateError:
1337
+ lookup = None
1338
+ if lookup is not None and lookup["hit"]:
1339
+ entry["state"] = "ledger" if lookup["reuse"] else "rejected"
1340
+ entry["receipt"] = {"source": "verify-ledger", "commit": lookup_commit}
1341
+ elif "pass" in results:
1342
+ entry["state"] = "log-only"
1343
+ entry["receipt"] = {"source": "task-log", "path": task.task_file}
1344
+ entry["reason"] = _ledger_absence_reason(
1345
+ repository, task, landing, member_landing, member_base)
1346
+ elif entry["superseded_by"] in proven:
1347
+ entry["state"] = "superseded"
1348
+ entry["reason"] = (f"verify retired by {entry['superseded_by']}; "
1349
+ "the superseding task carries the evidence")
1350
+ else:
1351
+ entry["state"] = "missing"
1352
+ entry["reason"] = "proven landing without a recorded Verify result"
1353
+ if entry["superseded_by"] and entry["superseded_by"] not in proven:
1354
+ note = f"superseding task {entry['superseded_by']} is not landed at this head"
1355
+ entry["reason"] = f"{entry['reason']}; {note}" if entry.get("reason") else note
1356
+ gap_members: List[Dict[str, object]] = []
1357
+ gap_accepted = False
1358
+ gap_reasons: List[str] = []
1359
+ gap_path = repository.joinpath(*PurePosixPath(canonical_dir).parts) / "review" / "final-gap-1.md"
1360
+ if gap_path.is_file() and not gap_path.is_symlink():
1361
+ text = gap_path.read_text(encoding="utf-8")
1362
+ verdict = GAP_VERDICT_RE.findall(text)
1363
+ reviewed = GAP_HEAD_RE.findall(text)
1364
+ for name, sha in GAP_MEMBER_RE.findall(text):
1365
+ gap_members.append({
1366
+ "member": name,
1367
+ "head": sha,
1368
+ "verdict": verdict[-1] if verdict else None,
1369
+ "source": f"{canonical_dir}/review/{gap_path.name}",
1370
+ })
1371
+ if gap_members:
1372
+ if verdict and verdict[-1] == "pass" and reviewed and reviewed[-1] == head:
1373
+ gap_accepted = True
1374
+ else:
1375
+ gap_reasons.append(
1376
+ f"{gap_path.name}: verdict {verdict[-1] if verdict else 'unreadable'}"
1377
+ f" at reviewed HEAD {reviewed[-1] if reviewed else 'unreadable'}")
1378
+ plan = _read_file(artifact_path / "plan" / "PLAN.md", f"{artifact_dir}/plan/PLAN.md")
1379
+ try:
1380
+ project_command = check_handoffs._line_value(plan, "Project verify:")
1381
+ except check_handoffs.HandoffError:
1382
+ project_command = ""
1383
+ project_state = "missing"
1384
+ project_reason: Optional[str] = None
1385
+ if gap_accepted:
1386
+ project_state = "gap-embedded"
1387
+ elif gap_reasons:
1388
+ project_reason = "; ".join(gap_reasons)
1389
+ elif project_command:
1390
+ project_lookup = verify_lookup(str(repository), project_command, head)
1391
+ if project_lookup["reuse"]:
1392
+ project_state = "ledger"
1393
+ elif project_lookup["hit"]:
1394
+ project_reason = "the recorded project Verify row at this head failed"
1395
+ return {
1396
+ "command": "evidence-manifest",
1397
+ "project_dir": artifact_dir,
1398
+ "canonical_project_dir": canonical_dir,
1399
+ "head": head,
1400
+ "tasks": manifest,
1401
+ "project": {
1402
+ "members": gap_members or None,
1403
+ "state": project_state,
1404
+ **({"reason": project_reason} if project_reason else {}),
1405
+ },
1406
+ }
1407
+
1408
+
1098
1409
  def _parser() -> argparse.ArgumentParser:
1099
1410
  parser = argparse.ArgumentParser(description=__doc__)
1100
1411
  subcommands = parser.add_subparsers(dest="command", required=True)
@@ -1127,6 +1438,13 @@ def _parser() -> argparse.ArgumentParser:
1127
1438
  lookup_parser.add_argument("--repo", required=True)
1128
1439
  lookup_parser.add_argument("--command", required=True, dest="verify_command")
1129
1440
  lookup_parser.add_argument("--commit", required=True)
1441
+ manifest_parser = subcommands.add_parser(
1442
+ "evidence-manifest", help="join every task to its authoritative accepted evidence"
1443
+ )
1444
+ manifest_parser.add_argument("--repo", required=True)
1445
+ manifest_parser.add_argument("--project-dir", default=DEFAULT_PROJECT_DIR)
1446
+ manifest_parser.add_argument("--head", required=True)
1447
+ manifest_parser.add_argument("--canonical-project-dir", default=DEFAULT_PROJECT_DIR)
1130
1448
  return parser
1131
1449
 
1132
1450
 
@@ -1143,6 +1461,13 @@ def main(argv: Optional[Sequence[str]] = None) -> int:
1143
1461
  )
1144
1462
  elif arguments.command == "verify-lookup":
1145
1463
  result = verify_lookup(arguments.repo, arguments.verify_command, arguments.commit)
1464
+ elif arguments.command == "evidence-manifest":
1465
+ result = evidence_manifest(
1466
+ arguments.repo,
1467
+ arguments.project_dir,
1468
+ arguments.head,
1469
+ arguments.canonical_project_dir,
1470
+ )
1146
1471
  else:
1147
1472
  result = verify_landed_tasks(
1148
1473
  arguments.repo,