@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
@@ -781,7 +781,14 @@ def validate_panel_skip_receipt(path: Path, expected_mode: str) -> None:
781
781
  not isinstance(item, dict)
782
782
  or set(item) != {"family", "reason"}
783
783
  or item["family"] not in KNOWN_FAMILIES
784
- or item["reason"] not in {"parent family", "not advertised", "cap"}
784
+ # review_panel resolve writes every one of these reasons (#460);
785
+ # the receipt must validate whatever the helper can produce.
786
+ or item["reason"] not in {
787
+ "parent family",
788
+ "not advertised",
789
+ "cap",
790
+ "excluded reviewer family",
791
+ }
785
792
  for item in skipped
786
793
  ):
787
794
  raise ArchiveError(f"{path.name} has invalid skipped families")
@@ -1998,6 +2005,62 @@ def member_ship_rows(project: Path, archive: Path, ship_commit: str) -> list:
1998
2005
  return rows
1999
2006
 
2000
2007
 
2008
+ def tag_metadata_fields(project: Path, tag_ref: str) -> dict[str, str]:
2009
+ """The Key: value fields of an annotated milestone tag's message."""
2010
+ message = require_git_success(
2011
+ run_git(project, "for-each-ref", "--format=%(contents)", tag_ref),
2012
+ "inspect milestone tag",
2013
+ )
2014
+ fields: dict[str, str] = {}
2015
+ for line in message.splitlines():
2016
+ match = re.fullmatch(r"(Mode|Pull-Request|Ship|Landing): (.+)", line)
2017
+ if match:
2018
+ if match.group(1) in fields:
2019
+ raise ArchiveError("milestone tag repeats metadata")
2020
+ fields[match.group(1)] = match.group(2)
2021
+ return fields
2022
+
2023
+
2024
+ def accept_rebased_ship(
2025
+ project: Path, archive: Path, ship_commit: str, reviewed_head: str
2026
+ ) -> Optional[str]:
2027
+ """Accept the rebased ship copy a rebase merge landed, keyed by the milestone tag.
2028
+
2029
+ Historical validation only (issue #450): after a rebase merge the landed
2030
+ ship copy's parent is never the reviewed HEAD, so the strict check cannot
2031
+ pass over landed history. Acceptance requires the milestone tag to name
2032
+ exactly this commit as its Landing, the original ship commit the tag
2033
+ names to still resolve, the strict invariant re-proven against the
2034
+ original ship's parent, and the rebase replay to hold. Returns the tag's
2035
+ original Ship sha, or None when the landing is not a proven rebase: a
2036
+ landing whose originals no longer resolve anywhere is refused, because
2037
+ the per-task landing proofs elsewhere in validation pin the original
2038
+ commits and a tag receipt alone cannot carry a full validation.
2039
+ """
2040
+ tag_ref = f"refs/tags/milestone/{archive.name}"
2041
+ if run_git(project, "rev-parse", "--verify", "--quiet", tag_ref).returncode != 0:
2042
+ return None
2043
+ try:
2044
+ fields = tag_metadata_fields(project, tag_ref)
2045
+ except ArchiveError:
2046
+ return None
2047
+ if fields.get("Mode") != "pull-request":
2048
+ return None
2049
+ original = fields.get("Ship")
2050
+ if not original or fields.get("Landing") != ship_commit:
2051
+ return None
2052
+ if run_git(project, "cat-file", "-e", f"{original}^{{commit}}").returncode != 0:
2053
+ return None
2054
+ original_parent = require_git_success(
2055
+ run_git(project, "rev-parse", f"{original}^"), "resolve original ship parent"
2056
+ )
2057
+ if not reviewed_head_covers(project, reviewed_head, original_parent):
2058
+ return None
2059
+ if _common.rebase_landing_problem(project, original, ship_commit) is not None:
2060
+ return None
2061
+ return original
2062
+
2063
+
2001
2064
  def validate(repo: Path, *, historical: bool = False) -> dict:
2002
2065
  project, _, state, configured, archive, archived_files, reviewed_head = prepared_transaction(
2003
2066
  repo,
@@ -2093,11 +2156,17 @@ def validate(repo: Path, *, historical: bool = False) -> dict:
2093
2156
 
2094
2157
  members = member_ship_rows(project, archive, ship_commit)
2095
2158
  ship_parent = require_git_success(run_git(project, "rev-parse", f"{ship_commit}^"), "resolve ship parent")
2159
+ rebased_ship: Optional[str] = None
2096
2160
  if not reviewed_head_covers(project, reviewed_head, ship_parent):
2097
- raise ArchiveError(
2098
- f"FINAL.md Reviewed HEAD {reviewed_head} does not match ship parent {ship_parent}; "
2099
- f"{_common.RUNTIME_PIN_ALLOWANCE}"
2161
+ rebased_ship = (
2162
+ accept_rebased_ship(project, archive, ship_commit, reviewed_head)
2163
+ if historical else None
2100
2164
  )
2165
+ if rebased_ship is None:
2166
+ raise ArchiveError(
2167
+ f"FINAL.md Reviewed HEAD {reviewed_head} does not match ship parent {ship_parent}; "
2168
+ f"{_common.RUNTIME_PIN_ALLOWANCE}"
2169
+ )
2101
2170
  require_canonical_commit_body(
2102
2171
  project,
2103
2172
  ship_commit,
@@ -2141,6 +2210,8 @@ def validate(repo: Path, *, historical: bool = False) -> dict:
2141
2210
  )
2142
2211
 
2143
2212
  result = {"archive": configured, "commit": ship_commit}
2213
+ if rebased_ship is not None:
2214
+ result["rebased_ship"] = rebased_ship
2144
2215
  if members:
2145
2216
  result["members"] = members
2146
2217
  return result
@@ -2178,6 +2249,15 @@ def parser() -> argparse.ArgumentParser:
2178
2249
  )
2179
2250
  validate_integrated_parser.add_argument("--repo", required=True, type=Path)
2180
2251
  validate_integrated_parser.add_argument("--slug", required=True)
2252
+ validate_integrated_parser.add_argument(
2253
+ "--historical",
2254
+ action="store_true",
2255
+ help=(
2256
+ "validate from the landed default branch after the milestone closed "
2257
+ "(the bound branch may be gone; a rebase-merged landing is proven "
2258
+ "through the milestone tag)"
2259
+ ),
2260
+ )
2181
2261
 
2182
2262
  refresh_parser = subparsers.add_parser(
2183
2263
  "refresh-origin",
@@ -2190,6 +2270,17 @@ def parser() -> argparse.ArgumentParser:
2190
2270
  help="after preflight, integrate each locked member in lock order before the ship commit",
2191
2271
  )
2192
2272
  close_members_parser.add_argument("--repo", required=True, type=Path)
2273
+ close_members_parser.add_argument(
2274
+ "--adopt-landed-member",
2275
+ action="append",
2276
+ default=[],
2277
+ metavar="MEMBER=SHA",
2278
+ help=(
2279
+ "user-approved recovery for a member whose reviewed HEAD already reached its "
2280
+ "default branch outside Path; MEMBER=SHA names the live default-branch tip "
2281
+ "to adopt as the landing"
2282
+ ),
2283
+ )
2193
2284
 
2194
2285
  integrate_parser = subparsers.add_parser(
2195
2286
  "integrate",
@@ -2197,6 +2288,16 @@ def parser() -> argparse.ArgumentParser:
2197
2288
  )
2198
2289
  integrate_parser.add_argument("--repo", required=True, type=Path)
2199
2290
  integrate_parser.add_argument("--slug", required=True)
2291
+ integrate_parser.add_argument(
2292
+ "--adopt-published-bound",
2293
+ metavar="SHA",
2294
+ help=(
2295
+ "user-approved recovery when origin's bound branch is a strict ancestor "
2296
+ "of the ship commit (a build task pushed it as a deploy pin, #464); "
2297
+ "the 40-hex SHA must equal the live remote tip exactly; the publish "
2298
+ "fast-forwards under the existing exact lease. Direct integration only"
2299
+ ),
2300
+ )
2200
2301
 
2201
2302
  abandon_parser = subparsers.add_parser(
2202
2303
  "abandon",
@@ -2225,13 +2326,34 @@ def main(argv: Optional[Sequence[str]] = None) -> int:
2225
2326
  elif arguments.command == "abandon":
2226
2327
  result = abandon(arguments.repo, arguments.slug, arguments.reason)
2227
2328
  elif arguments.command == "validate-integrated":
2228
- result = integration.validate_integrated(arguments.repo, arguments.slug)
2329
+ result = integration.validate_integrated(
2330
+ arguments.repo, arguments.slug, historical=arguments.historical
2331
+ )
2229
2332
  elif arguments.command == "refresh-origin":
2230
2333
  result = integration.refresh_origin(arguments.repo)
2231
2334
  elif arguments.command == "close-members":
2232
- result = integration.close_members(arguments.repo)
2335
+ adopt: dict[str, str] = {}
2336
+ for item in arguments.adopt_landed_member:
2337
+ name, separator, sha = item.partition("=")
2338
+ if not name or not separator or not re.fullmatch(r"[0-9a-f]{40}", sha):
2339
+ parser_error = (
2340
+ f"--adopt-landed-member expects MEMBER=SHA (40-hex), got {item!r}"
2341
+ )
2342
+ print(parser_error, file=sys.stderr)
2343
+ return 2
2344
+ adopt[name] = sha
2345
+ result = integration.close_members(arguments.repo, adopt=adopt or None)
2233
2346
  elif arguments.command == "integrate":
2234
- result = integration.integrate(arguments.repo, arguments.slug)
2347
+ adopt_bound = arguments.adopt_published_bound
2348
+ if adopt_bound is not None:
2349
+ adopt_bound = adopt_bound.lower()
2350
+ if adopt_bound is not None and not re.fullmatch(r"[0-9a-f]{40}", adopt_bound):
2351
+ print(
2352
+ f"--adopt-published-bound expects a 40-hex SHA, got {adopt_bound!r}",
2353
+ file=sys.stderr,
2354
+ )
2355
+ return 2
2356
+ result = integration.integrate(arguments.repo, arguments.slug, adopt_bound)
2235
2357
  else:
2236
2358
  result = validate(arguments.repo)
2237
2359
  except (ArchiveError, OSError) as error:
@@ -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: