@opengsd/gsd-path 1.6.1 → 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 +34 -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 +20 -29
  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 +1015 -37
  23. package/scripts/lean_verification.py +33 -14
  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 +5 -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 +43 -9
  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 +1015 -37
  56. package/skills/gsd-path/scripts/lean_verification.py +33 -14
  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 +5 -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 +1015 -37
  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 +1015 -37
  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 +1015 -37
  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 +1015 -37
  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 +1015 -37
  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 +1015 -37
  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 +1015 -37
  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 +1015 -37
  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 +43 -9
  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 +1015 -37
  230. package/skills/gsd-path-ship/scripts/lean_verification.py +33 -14
  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 +5 -1
  243. package/skills/path/DEFINE.md +2 -1
  244. package/skills/path/PLAN.md +10 -0
  245. package/skills/path/SHIP.md +43 -9
  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 +1015 -37
  265. package/skills/path/scripts/lean_verification.py +33 -14
  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
@@ -11,7 +11,7 @@ import json
11
11
  import re
12
12
  import contextlib
13
13
  from pathlib import Path, PurePosixPath
14
- from typing import Optional
14
+ from typing import Mapping, Optional
15
15
 
16
16
  if __package__:
17
17
  from .pipeline_git import (
@@ -25,7 +25,7 @@ if __package__:
25
25
  ship_subject,
26
26
  )
27
27
  from .pipeline_state import PipelineState
28
- from .isolation import publication_base
28
+ from .isolation import publication_base, _unlink_link_like, IsolationError
29
29
  from . import _common
30
30
  from . import worktree_paths
31
31
  else:
@@ -41,7 +41,7 @@ else:
41
41
  ship_subject,
42
42
  )
43
43
  from pipeline_state import PipelineState
44
- from isolation import publication_base
44
+ from isolation import publication_base, _unlink_link_like, IsolationError
45
45
  import _common
46
46
  import worktree_paths
47
47
  except ImportError: # pragma: no cover - package import used by tests
@@ -56,7 +56,7 @@ else:
56
56
  ship_subject,
57
57
  )
58
58
  from scripts.pipeline_state import PipelineState
59
- from scripts.isolation import publication_base
59
+ from scripts.isolation import publication_base, _unlink_link_like, IsolationError
60
60
  from scripts import _common
61
61
  from scripts import worktree_paths
62
62
 
@@ -79,6 +79,13 @@ run_git = _common.run_git
79
79
  run_command = _common.run_command
80
80
 
81
81
 
82
+ def _defuse_links(tree: Path) -> None:
83
+ try:
84
+ _unlink_link_like(tree) # never traverse links (#462)
85
+ except IsolationError as error:
86
+ raise ArchiveError(str(error)) from error
87
+
88
+
82
89
  def resolve_remote_default(project: Path) -> str:
83
90
  result = run_git(project, "symbolic-ref", "--quiet", "refs/remotes/origin/HEAD")
84
91
  remote_ref = result.stdout.strip()
@@ -107,12 +114,25 @@ def live_remote_ref(project: Path, ref: str) -> Optional[str]:
107
114
  return rows[0][0]
108
115
 
109
116
 
110
- def publishable_bound_branch(project: Path, branch: str, ship_commit: str) -> Optional[str]:
117
+ def publishable_bound_branch(
118
+ project: Path, branch: str, ship_commit: str, adopt_published: Optional[str] = None
119
+ ) -> Optional[str]:
111
120
  """The live origin SHA the bound ref may move from: absent, the ship
112
- commit, or an owner-adopted rebased head proven by its archived receipt."""
121
+ commit, an owner-adopted rebased head proven by its archived receipt, or a
122
+ user-attested strict ancestor adopted through --adopt-published-bound
123
+ (#464: a build task legitimately pushed the bound branch as a deploy pin)."""
113
124
  remote_sha = live_remote_ref(project, f"refs/heads/{branch}")
114
125
  if remote_sha is None or remote_sha == ship_commit:
115
126
  return remote_sha
127
+ if adopt_published is not None:
128
+ if remote_sha != adopt_published:
129
+ raise ArchiveError(
130
+ f"origin/{branch} moved or collides: {remote_sha} != {adopt_published}")
131
+ if run_git(project, "merge-base", "--is-ancestor", remote_sha, ship_commit).returncode != 0:
132
+ raise ArchiveError(
133
+ f"origin/{branch} {remote_sha} diverges from the ship commit; "
134
+ "an adoption cannot rewind")
135
+ return remote_sha
116
136
  try:
117
137
  approved_base = publication_base(project, branch, ship_commit)
118
138
  except (RuntimeError, OSError) as error:
@@ -122,9 +142,11 @@ def publishable_bound_branch(project: Path, branch: str, ship_commit: str) -> Op
122
142
  return remote_sha
123
143
 
124
144
 
125
- def publish_bound_branch(project: Path, branch: str, ship_commit: str) -> None:
145
+ def publish_bound_branch(
146
+ project: Path, branch: str, ship_commit: str, adopt_published: Optional[str] = None
147
+ ) -> None:
126
148
  remote_ref = f"refs/heads/{branch}"
127
- remote_sha = publishable_bound_branch(project, branch, ship_commit)
149
+ remote_sha = publishable_bound_branch(project, branch, ship_commit, adopt_published)
128
150
  if remote_sha != ship_commit:
129
151
  # An absent ref carries an empty lease; an adopted head leases its exact SHA.
130
152
  archive_milestone.require_git_success(
@@ -232,6 +254,8 @@ def remove_registered_worktree(project: Path, branch: str, expected_path: Path)
232
254
  )
233
255
  if status:
234
256
  raise ArchiveError("integration worktree is dirty; refusing cleanup")
257
+ if registered.is_dir():
258
+ _defuse_links(registered)
235
259
  archive_milestone.require_git_success(
236
260
  run_git(project, "worktree", "remove", str(registered)),
237
261
  "remove integration worktree",
@@ -792,17 +816,7 @@ def pull_request_tag_message(
792
816
 
793
817
 
794
818
  def pull_request_tag_metadata(project: Path, tag_ref: str) -> dict[str, str]:
795
- message = archive_milestone.require_git_success(
796
- run_git(project, "for-each-ref", "--format=%(contents)", tag_ref),
797
- "inspect pull-request milestone tag",
798
- )
799
- fields: dict[str, str] = {}
800
- for line in message.splitlines():
801
- match = re.fullmatch(r"(Mode|Pull-Request|Ship|Landing): (.+)", line)
802
- if match:
803
- if match.group(1) in fields:
804
- raise ArchiveError("pull-request milestone tag repeats metadata")
805
- fields[match.group(1)] = match.group(2)
819
+ fields = archive_milestone.tag_metadata_fields(project, tag_ref)
806
820
  if set(fields) != {"Mode", "Pull-Request", "Ship", "Landing"}:
807
821
  raise ArchiveError("pull-request milestone tag is missing metadata")
808
822
  if fields["Mode"] != "pull-request":
@@ -1158,7 +1172,7 @@ def integrate_external_landing(
1158
1172
  return validate_integrated(project, archive_milestone.milestone_slug(state))
1159
1173
 
1160
1174
 
1161
- def integrate(repo: Path, slug: str) -> dict:
1175
+ def integrate(repo: Path, slug: str, adopt_published: Optional[str] = None) -> dict:
1162
1176
  project = repo.resolve()
1163
1177
  active_root = archive_milestone.require_project_layout(project)
1164
1178
  state, _, loaded_state_path = archive_milestone.strict_state(project)
@@ -1201,6 +1215,10 @@ def integrate(repo: Path, slug: str) -> dict:
1201
1215
  remote_default_sha = archive_milestone.require_git_success(
1202
1216
  run_git(project, "rev-parse", remote_default), "resolve remote default"
1203
1217
  )
1218
+ if adopt_published is not None and state.integration != "direct":
1219
+ raise ArchiveError(
1220
+ "--adopt-published-bound applies only to direct integration, "
1221
+ f"not {state.integration}")
1204
1222
  if state.integration == "pull-request":
1205
1223
  return integrate_pull_request(
1206
1224
  project,
@@ -1220,7 +1238,7 @@ def integrate(repo: Path, slug: str) -> dict:
1220
1238
  default_name,
1221
1239
  bound_branch,
1222
1240
  )
1223
- publishable_bound_branch(project, bound_branch, ship_commit)
1241
+ publishable_bound_branch(project, bound_branch, ship_commit, adopt_published)
1224
1242
  integration_branch, worktree = integration_names(project, archive_name, pin=True)
1225
1243
  interrupted_worktree = registered_worktree(project, integration_branch)
1226
1244
  if (
@@ -1311,7 +1329,7 @@ def integrate(repo: Path, slug: str) -> dict:
1311
1329
  elif published_merge.returncode != 0:
1312
1330
  archive_milestone.require_git_success(published_merge, "inspect published integration merge")
1313
1331
 
1314
- publish_bound_branch(project, bound_branch, ship_commit)
1332
+ publish_bound_branch(project, bound_branch, ship_commit, adopt_published)
1315
1333
  if not tag_published:
1316
1334
  archive_milestone.require_git_success(
1317
1335
  run_git(
@@ -1462,13 +1480,21 @@ def validate_integrated(
1462
1480
  if run_git(project, "rev-parse", "--verify", "--quiet", tag_ref).returncode != 0:
1463
1481
  raise ArchiveError(f"missing milestone tag: {tag_name}")
1464
1482
  metadata = pull_request_tag_metadata(project, tag_ref)
1465
- if metadata["Ship"] != ship_commit:
1483
+ # A rebase merge rewrites the ship commits, so historical validation
1484
+ # over landed history finds the tag's Landing where the bound branch
1485
+ # found its Ship (#450); archive validate proved the mapping.
1486
+ if metadata["Ship"] != ship_commit and not (
1487
+ shipped.get("rebased_ship") == metadata["Ship"]
1488
+ and metadata["Landing"] == ship_commit
1489
+ ):
1466
1490
  raise ArchiveError("pull-request milestone tag names the wrong ship commit")
1467
1491
  merge_commit = metadata["Landing"]
1468
1492
  pull_request = metadata["Pull-Request"]
1469
1493
  require_github_authentication()
1470
1494
  repository = github_repository(project)
1471
- pull = find_pull_request(repository, bound_branch, ship_commit, default_name)
1495
+ # The PR head is the original ship commit, which the tag names; a
1496
+ # rebase-merged landing rewrites it on the default branch (#450).
1497
+ pull = find_pull_request(repository, bound_branch, metadata["Ship"], default_name)
1472
1498
  if pull is None:
1473
1499
  raise ArchiveError("GitHub pull request for the ship commit is missing")
1474
1500
  if pull["html_url"] != pull_request:
@@ -1484,8 +1510,14 @@ def validate_integrated(
1484
1510
  pull["number"],
1485
1511
  merge_commit,
1486
1512
  )
1513
+ # Prove the pair the tag names: the original ship commit, whose objects
1514
+ # archive validation already required to resolve (#450).
1487
1515
  require_pull_request_merge(
1488
- project, merge_commit, ship_commit, remote_default, allow_rebase=True
1516
+ project,
1517
+ merge_commit,
1518
+ metadata["Ship"],
1519
+ remote_default,
1520
+ allow_rebase=True,
1489
1521
  )
1490
1522
  require_annotated_tag(
1491
1523
  project,
@@ -1495,7 +1527,7 @@ def validate_integrated(
1495
1527
  pull_request_tag_message(
1496
1528
  archive_name,
1497
1529
  pull_request,
1498
- ship_commit,
1530
+ metadata["Ship"],
1499
1531
  merge_commit,
1500
1532
  ),
1501
1533
  )
@@ -1561,6 +1593,7 @@ def validate_integrated(
1561
1593
  ship_commit,
1562
1594
  tag_name,
1563
1595
  merge_commit,
1596
+ tag_ship=shipped.get("rebased_ship"),
1564
1597
  allow_missing_bound=state.integration in ("pull-request", "external-landing")
1565
1598
  or (historical and has_origin),
1566
1599
  retired=historical,
@@ -1609,10 +1642,14 @@ def require_published_integration(
1609
1642
  merge_commit: str,
1610
1643
  allow_missing_bound: bool = False,
1611
1644
  retired: bool = False,
1645
+ tag_ship: Optional[str] = None,
1612
1646
  ) -> None:
1613
1647
  if allow_missing_bound:
1614
1648
  published_ship = live_remote_ref(project, f"refs/heads/{bound_branch}")
1615
- if published_ship is not None and published_ship != ship_commit:
1649
+ # A surviving bound branch stays at the original ship commit, which a
1650
+ # rebase merge rewrote into the landing validated here (#450).
1651
+ expected_ships = {ship_commit} | ({tag_ship} if tag_ship else set())
1652
+ if published_ship is not None and published_ship not in expected_ships:
1616
1653
  raise ArchiveError(
1617
1654
  f"origin/{bound_branch} is {published_ship}, expected ship commit {ship_commit}"
1618
1655
  )
@@ -1691,6 +1728,28 @@ def member_pull_request_tag_message(tag_name: str, pull_request: str, reviewed_h
1691
1728
  f"Pull-Request: {pull_request}\nReviewed-HEAD: {reviewed_head}\nLanding: {landing}")
1692
1729
 
1693
1730
 
1731
+ def member_adopted_tag_message(tag_name: str, reviewed_head: str, landing: str) -> str:
1732
+ return (f"milestone {tag_name.removeprefix('milestone/')}\n\nMode: adopted-landing\n"
1733
+ f"Reviewed-HEAD: {reviewed_head}\nLanding: {landing}\n\n"
1734
+ "The reviewed HEAD reached the member default branch through earlier user-approved "
1735
+ "publications; Path made no merge.")
1736
+
1737
+
1738
+ def member_adopted_tag_metadata(contents: str) -> dict[str, str]:
1739
+ fields: dict[str, str] = {}
1740
+ for line in contents.splitlines():
1741
+ match = re.fullmatch(r"(Mode|Reviewed-HEAD|Landing): (.+)", line)
1742
+ if match:
1743
+ if match.group(1) in fields:
1744
+ raise ArchiveError("adopted-landing member tag repeats metadata")
1745
+ fields[match.group(1)] = match.group(2)
1746
+ if set(fields) != {"Mode", "Reviewed-HEAD", "Landing"}:
1747
+ raise ArchiveError("adopted-landing member tag is missing metadata")
1748
+ if fields["Mode"] != "adopted-landing":
1749
+ raise ArchiveError("adopted-landing member tag has the wrong mode")
1750
+ return fields
1751
+
1752
+
1694
1753
  def member_pull_request_tag_metadata(contents: str) -> dict[str, str]:
1695
1754
  fields: dict[str, str] = {}
1696
1755
  for line in contents.splitlines():
@@ -1805,7 +1864,8 @@ def _require_member_close(checkout: Path, member: str, bound: str, reviewed_head
1805
1864
  raise ArchiveError(f"member {member} remote default must be {default}, got {live!r}")
1806
1865
 
1807
1866
 
1808
- def integrate_member(coordinator: Path, member: str, archive_path: str, reviewed_head: str) -> dict:
1867
+ def integrate_member(coordinator: Path, member: str, archive_path: str, reviewed_head: str,
1868
+ adopt: Optional[str] = None) -> dict:
1809
1869
  """Direct-mode member close: publish the bound branch, merge it into the member
1810
1870
  default branch, then tag."""
1811
1871
  checkout, project, bound, subject, tag_name, default = _member_names(coordinator, member, archive_path)
@@ -1813,16 +1873,32 @@ def integrate_member(coordinator: Path, member: str, archive_path: str, reviewed
1813
1873
  body = _member_merge_body(archive_path, member, reviewed_head)
1814
1874
  _require_member_close(checkout, member, bound, reviewed_head, default)
1815
1875
  merge = _find_member_merge(checkout, subject, body, reviewed_head, default)
1876
+ adopted = False
1816
1877
  if merge is None:
1817
1878
  origin_main = archive_milestone.require_git_success(
1818
1879
  run_git(checkout, "rev-parse", origin_default), f"resolve member {origin_default}")
1819
1880
  ancestry = run_git(checkout, "merge-base", "--is-ancestor", reviewed_head, origin_main)
1820
1881
  if ancestry.returncode == 0:
1821
- raise ArchiveError(
1822
- f"member reviewed HEAD reached {origin_default} outside Path; user-approved recovery required")
1823
- if ancestry.returncode != 1:
1882
+ if live_remote_ref(checkout, f"refs/tags/{tag_name}") is not None:
1883
+ return validate_member_integrated(coordinator, member, archive_path, reviewed_head)
1884
+ if adopt is None:
1885
+ raise ArchiveError(
1886
+ f"member reviewed HEAD reached {origin_default} outside Path; user-approved recovery "
1887
+ f"required: rerun close-members with --adopt-landed-member {member}={origin_main}")
1888
+ if adopt != origin_main:
1889
+ raise ArchiveError(
1890
+ f"member {member} --adopt-landed-member names {adopt}, "
1891
+ f"but {origin_default} is {origin_main}")
1892
+ merge = origin_main
1893
+ adopted = True
1894
+ elif ancestry.returncode != 1:
1824
1895
  archive_milestone.require_git_success(ancestry, "inspect member reviewed HEAD ancestry")
1825
- merge = _create_member_merge(checkout, project, archive_path, subject, body, reviewed_head, origin_main)
1896
+ elif adopt is not None:
1897
+ raise ArchiveError(
1898
+ f"member {member} --adopt-landed-member was given, but its reviewed HEAD "
1899
+ f"has not reached {origin_default}")
1900
+ if merge is None:
1901
+ merge = _create_member_merge(checkout, project, archive_path, subject, body, reviewed_head, origin_main)
1826
1902
  # Publish in order: bound branch, default branch, tag; a rerun publishes only what is missing.
1827
1903
  _publish_member_bound(checkout, project, member, bound, reviewed_head)
1828
1904
  if run_git(checkout, "merge-base", "--is-ancestor", merge, origin_default).returncode != 0:
@@ -1830,7 +1906,10 @@ def integrate_member(coordinator: Path, member: str, archive_path: str, reviewed
1830
1906
  archive_milestone.require_git_success(
1831
1907
  run_git(checkout, "update-ref", f"refs/remotes/{origin_default}", merge),
1832
1908
  f"refresh member {origin_default}")
1833
- tag_object, tag_published = ensure_integration_tag(checkout, tag_name, merge)
1909
+ tag_message = (
1910
+ member_adopted_tag_message(tag_name, reviewed_head, merge) if adopted else None
1911
+ )
1912
+ tag_object, tag_published = ensure_integration_tag(checkout, tag_name, merge, tag_message)
1834
1913
  if not tag_published:
1835
1914
  _push_member_ref(checkout, project, f"refs/tags/{tag_name}", f"refs/tags/{tag_name}", tag_object, None)
1836
1915
  archive_milestone.require_git_success(
@@ -1903,6 +1982,54 @@ def validate_member_integrated(
1903
1982
  run_git(checkout, "update-ref", tracking, published), "refresh member milestone-tag ref")
1904
1983
  if published is not None:
1905
1984
  contents = run_git(checkout, "for-each-ref", "--format=%(contents)", tracking).stdout
1985
+ if "Mode: adopted-landing" in contents.splitlines():
1986
+ # A user-approved adoption: the reviewed HEAD reached the member default
1987
+ # branch through earlier publications, so the tag proves the landing in
1988
+ # place of the canonical merge Path never made (#458). Only a
1989
+ # direct-mode member may carry one; a pull-request member validates
1990
+ # through its merged PR instead.
1991
+ try:
1992
+ import members as members_module
1993
+ except ImportError: # pragma: no cover - package import used by tests
1994
+ from scripts import members as members_module
1995
+ common = Path(archive_milestone.require_git_success(
1996
+ run_git(coordinator, "rev-parse", "--path-format=absolute", "--git-common-dir"),
1997
+ "resolve Git common dir"))
1998
+ try:
1999
+ closed_modes = {
2000
+ row["name"]: row["mode"]
2001
+ for row in members_module.read_member_close(common, PurePosixPath(archive_path).name)
2002
+ }
2003
+ effective_mode = closed_modes.get(member) or {
2004
+ item["name"]: item["integration"]
2005
+ for item in members_module.read_members(coordinator)
2006
+ }.get(member, "default")
2007
+ except members_module.MembersError as error:
2008
+ raise ArchiveError(str(error)) from error
2009
+ if effective_mode == "default":
2010
+ effective_mode = archive_milestone.strict_state(coordinator)[0].integration
2011
+ if effective_mode != "direct":
2012
+ raise ArchiveError(
2013
+ f"member {member} is not direct-mode; an adopted-landing tag cannot close it")
2014
+ metadata = member_adopted_tag_metadata(contents)
2015
+ if metadata["Reviewed-HEAD"] != reviewed_head:
2016
+ raise ArchiveError(f"member {member} tag {tag_name} names a different reviewed HEAD")
2017
+ merge = archive_milestone.require_git_success(
2018
+ run_git(checkout, "rev-parse", f"{tracking}^{{commit}}"), "resolve member tag target")
2019
+ if metadata["Landing"] != merge:
2020
+ raise ArchiveError(f"member {member} tag {tag_name} names a different landing")
2021
+ require_annotated_tag(
2022
+ checkout, tracking, merge, f"member tag {tag_name}",
2023
+ member_adopted_tag_message(tag_name, reviewed_head, merge),
2024
+ )
2025
+ origin_default_ref = f"origin/{default}"
2026
+ for earlier, later in ((reviewed_head, merge), (merge, origin_default_ref)):
2027
+ if run_git(checkout, "merge-base", "--is-ancestor", earlier, later).returncode != 0:
2028
+ raise ArchiveError(
2029
+ f"member {member} adopted landing {merge} does not follow the reviewed HEAD "
2030
+ f"on {origin_default_ref}")
2031
+ return {"member": member, "merge": merge, "tag": tag_name, "tag_object": published,
2032
+ "adopted": True}
1906
2033
  if "Mode: pull-request" in contents.splitlines():
1907
2034
  metadata = member_pull_request_tag_metadata(contents)
1908
2035
  if metadata["Reviewed-HEAD"] != reviewed_head:
@@ -1945,6 +2072,10 @@ def retire_member(coordinator: Path, member: str, archive_path: str, reviewed_he
1945
2072
  import members
1946
2073
  except ImportError: # pragma: no cover - package import used by tests
1947
2074
  from scripts import members
2075
+ try:
2076
+ import isolation
2077
+ except ImportError: # pragma: no cover - package import used by tests
2078
+ from scripts import isolation
1948
2079
  checkout, project, bound, _, tag_name, default = _member_names(coordinator, member, archive_path)
1949
2080
  proven = validate_member_integrated(coordinator, member, archive_path, reviewed_head)
1950
2081
  if (proven["merge"], proven["tag"]) != (merge, tag):
@@ -1978,8 +2109,14 @@ def retire_member(coordinator: Path, member: str, archive_path: str, reviewed_he
1978
2109
  run_git(checkout, "push", f"--force-with-lease={bound_ref}:{reviewed_head}", "origin", "--delete", bound),
1979
2110
  f"delete member origin/{bound}")
1980
2111
  if holder is not None:
1981
- archive_milestone.require_git_success(run_git(checkout, "worktree", "remove", str(holder)),
1982
- "remove member bound checkout")
2112
+ # The bound checkout may contain submodules: git refuses a plain worktree
2113
+ # removal for them regardless of cleanliness. isolation's remover defuses
2114
+ # links, deinits submodules, and forces the removal after the clean check
2115
+ # above has already guarded the dirty case.
2116
+ try:
2117
+ isolation._remove_worktree(checkout, holder, force=False)
2118
+ except isolation.IsolationError as error:
2119
+ raise ArchiveError(f"remove member bound checkout failed: {error}") from error
1983
2120
  if local is not None:
1984
2121
  archive_milestone.require_git_success(run_git(checkout, "update-ref", "-d", bound_ref, reviewed_head),
1985
2122
  "delete member bound branch")
@@ -1994,8 +2131,13 @@ def retire_member(coordinator: Path, member: str, archive_path: str, reviewed_he
1994
2131
  return {"member": member, "retired": bound}
1995
2132
 
1996
2133
 
1997
- def close_members(repo: Path) -> dict:
1998
- """Close each locked member in lock order before the coordinator ship commit; resumable."""
2134
+ def close_members(repo: Path, adopt: Optional[Mapping[str, str]] = None) -> dict:
2135
+ """Close each locked member in lock order before the coordinator ship commit; resumable.
2136
+
2137
+ ``adopt`` carries user-approved ``--adopt-landed-member`` recoveries: the
2138
+ member name maps to the exact live default-branch tip whose history
2139
+ already contains the reviewed HEAD (#458).
2140
+ """
1999
2141
  try:
2000
2142
  import check_handoffs
2001
2143
  import members
@@ -2026,6 +2168,19 @@ def close_members(repo: Path) -> dict:
2026
2168
  rows = {row["name"]: row for row in members.read_member_close(common, archive_name)}
2027
2169
  except members.MembersError as error:
2028
2170
  raise ArchiveError(str(error)) from error
2171
+ if adopt:
2172
+ locked_names = {entry["name"] for entry in locked}
2173
+ unknown = sorted(set(adopt) - locked_names)
2174
+ if unknown:
2175
+ raise ArchiveError(
2176
+ f"--adopt-landed-member names member(s) not locked in this milestone: {', '.join(unknown)}")
2177
+ for adopted_name in sorted(adopt):
2178
+ mode = modes.get(adopted_name, "default")
2179
+ if mode == "default":
2180
+ mode = state.integration
2181
+ if mode != "direct":
2182
+ raise ArchiveError(
2183
+ f"--adopt-landed-member names {adopted_name}, which is not a direct-mode member")
2029
2184
  def save() -> None:
2030
2185
  members.write_member_close(common, archive_name, [rows[item["name"]] for item in locked if item["name"] in rows])
2031
2186
 
@@ -2048,14 +2203,16 @@ def close_members(repo: Path) -> dict:
2048
2203
  rows[name] = row
2049
2204
  save()
2050
2205
  if mode == "direct":
2051
- result = integrate_member(project, name, state.archive, heads[name])
2206
+ result = integrate_member(project, name, state.archive, heads[name],
2207
+ adopt=(adopt or {}).get(name))
2052
2208
  else:
2053
2209
  result = integrate_member_pull_request(project, name, state.archive, heads[name])
2054
2210
  if result.get("status") == "awaiting-merge":
2055
2211
  row["pull_request"] = result["pull_request"]
2056
2212
  save()
2057
2213
  return {"status": "awaiting-merge", "member": name, "pull_request": result["pull_request"]}
2058
- row.update(status="integrated", merge=result["merge"], tag=result["tag"])
2214
+ row.update(status="integrated", merge=result["merge"], tag=result["tag"],
2215
+ **({"adopted": True} if result.get("adopted") else {}))
2059
2216
  save()
2060
2217
  closed.append(row)
2061
2218
  return {"status": "integrated", "members": closed, "body": ship_commit_body(state.archive, reviewed, closed)}