@opengsd/gsd-path 1.6.2 → 1.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (281) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/DOCS.md +27 -1
  3. package/FULL.md +4 -3
  4. package/HOOKS.md +38 -4
  5. package/QUICK.md +1 -1
  6. package/README.md +19 -11
  7. package/RUNTIME.md +5 -3
  8. package/WORKFLOW.md +11 -4
  9. package/package.json +1 -1
  10. package/scripts/_common.py +76 -2
  11. package/scripts/archive_milestone.py +129 -7
  12. package/scripts/build_recovery.py +25 -2
  13. package/scripts/build_state.py +333 -8
  14. package/scripts/check_handoffs.py +437 -30
  15. package/scripts/check_task_briefs.py +135 -11
  16. package/scripts/discussion_validate.py +46 -10
  17. package/scripts/dispatch_driver.py +33 -3
  18. package/scripts/git_guard.py +44 -4
  19. package/scripts/guard_hook.py +456 -9
  20. package/scripts/install.py +34 -4
  21. package/scripts/integration.py +196 -39
  22. package/scripts/isolation.py +1000 -38
  23. package/scripts/lean_verification.py +28 -13
  24. package/scripts/loop_run.py +9 -4
  25. package/scripts/members.py +17 -3
  26. package/scripts/pipeline_diagnose.py +104 -1
  27. package/scripts/pipeline_state.py +10 -2
  28. package/scripts/runtime_store.py +88 -3
  29. package/scripts/state_checkpoint.py +15 -0
  30. package/scripts/status_runtime.py +73 -2
  31. package/scripts/workflow_run.py +10 -3
  32. package/scripts/worktree_paths.py +45 -0
  33. package/skills/gsd-path/BUILD.md +4 -1
  34. package/skills/gsd-path/DEFINE.md +2 -1
  35. package/skills/gsd-path/PLAN.md +13 -3
  36. package/skills/gsd-path/SHIP.md +41 -8
  37. package/skills/gsd-path/SKILL.md +16 -1
  38. package/skills/gsd-path/references/build-native.md +28 -7
  39. package/skills/gsd-path/references/coder.md +6 -3
  40. package/skills/gsd-path/references/planner.md +4 -3
  41. package/skills/gsd-path/references/reviewer-final.md +10 -0
  42. package/skills/gsd-path/references/reviewer-wave.md +71 -27
  43. package/skills/gsd-path/references/reviewer.md +21 -8
  44. package/skills/gsd-path/references/runtime-selection.md +36 -0
  45. package/skills/gsd-path/references/spec-probes.md +19 -1
  46. package/skills/gsd-path/scripts/_common.py +76 -2
  47. package/skills/gsd-path/scripts/archive_milestone.py +129 -7
  48. package/skills/gsd-path/scripts/build_recovery.py +25 -2
  49. package/skills/gsd-path/scripts/build_state.py +333 -8
  50. package/skills/gsd-path/scripts/check_handoffs.py +437 -30
  51. package/skills/gsd-path/scripts/check_task_briefs.py +135 -11
  52. package/skills/gsd-path/scripts/discussion_validate.py +46 -10
  53. package/skills/gsd-path/scripts/dispatch_driver.py +33 -3
  54. package/skills/gsd-path/scripts/git_guard.py +44 -4
  55. package/skills/gsd-path/scripts/integration.py +196 -39
  56. package/skills/gsd-path/scripts/isolation.py +1000 -38
  57. package/skills/gsd-path/scripts/lean_verification.py +28 -13
  58. package/skills/gsd-path/scripts/loop_run.py +9 -4
  59. package/skills/gsd-path/scripts/members.py +17 -3
  60. package/skills/gsd-path/scripts/pipeline_state.py +10 -2
  61. package/skills/gsd-path/scripts/state_checkpoint.py +15 -0
  62. package/skills/gsd-path/scripts/status_runtime.py +73 -2
  63. package/skills/gsd-path/scripts/workflow_run.py +10 -3
  64. package/skills/gsd-path/scripts/worktree_paths.py +45 -0
  65. package/skills/gsd-path/templates/intent.md +5 -3
  66. package/skills/gsd-path/templates/plan.md +3 -2
  67. package/skills/gsd-path/templates/task.md +8 -1
  68. package/skills/gsd-path/templates/wave-review.md +24 -8
  69. package/skills/gsd-path-build/SKILL.md +4 -1
  70. package/skills/gsd-path-build/references/build-native.md +28 -7
  71. package/skills/gsd-path-build/references/coder.md +6 -3
  72. package/skills/gsd-path-build/references/reviewer-final.md +10 -0
  73. package/skills/gsd-path-build/references/reviewer-wave.md +71 -27
  74. package/skills/gsd-path-build/references/reviewer.md +21 -8
  75. package/skills/gsd-path-build/references/runtime-selection.md +36 -0
  76. package/skills/gsd-path-build/references/spec-probes.md +19 -1
  77. package/skills/gsd-path-build/scripts/_common.py +76 -2
  78. package/skills/gsd-path-build/scripts/archive_milestone.py +129 -7
  79. package/skills/gsd-path-build/scripts/build_recovery.py +25 -2
  80. package/skills/gsd-path-build/scripts/build_state.py +333 -8
  81. package/skills/gsd-path-build/scripts/check_handoffs.py +437 -30
  82. package/skills/gsd-path-build/scripts/check_task_briefs.py +135 -11
  83. package/skills/gsd-path-build/scripts/discussion_validate.py +46 -10
  84. package/skills/gsd-path-build/scripts/dispatch_driver.py +33 -3
  85. package/skills/gsd-path-build/scripts/integration.py +196 -39
  86. package/skills/gsd-path-build/scripts/isolation.py +1000 -38
  87. package/skills/gsd-path-build/scripts/loop_run.py +9 -4
  88. package/skills/gsd-path-build/scripts/members.py +17 -3
  89. package/skills/gsd-path-build/scripts/pipeline_state.py +10 -2
  90. package/skills/gsd-path-build/scripts/state_checkpoint.py +15 -0
  91. package/skills/gsd-path-build/scripts/workflow_run.py +10 -3
  92. package/skills/gsd-path-build/scripts/worktree_paths.py +45 -0
  93. package/skills/gsd-path-build/templates/task.md +8 -1
  94. package/skills/gsd-path-build/templates/wave-review.md +24 -8
  95. package/skills/gsd-path-decide/references/runtime-selection.md +36 -0
  96. package/skills/gsd-path-decide/scripts/_common.py +76 -2
  97. package/skills/gsd-path-decide/scripts/archive_milestone.py +129 -7
  98. package/skills/gsd-path-decide/scripts/build_recovery.py +25 -2
  99. package/skills/gsd-path-decide/scripts/check_handoffs.py +437 -30
  100. package/skills/gsd-path-decide/scripts/check_task_briefs.py +135 -11
  101. package/skills/gsd-path-decide/scripts/discussion_validate.py +46 -10
  102. package/skills/gsd-path-decide/scripts/integration.py +196 -39
  103. package/skills/gsd-path-decide/scripts/isolation.py +1000 -38
  104. package/skills/gsd-path-decide/scripts/loop_run.py +9 -4
  105. package/skills/gsd-path-decide/scripts/members.py +17 -3
  106. package/skills/gsd-path-decide/scripts/pipeline_state.py +10 -2
  107. package/skills/gsd-path-decide/scripts/state_checkpoint.py +15 -0
  108. package/skills/gsd-path-decide/scripts/worktree_paths.py +45 -0
  109. package/skills/gsd-path-define/SKILL.md +2 -1
  110. package/skills/gsd-path-define/references/runtime-selection.md +36 -0
  111. package/skills/gsd-path-define/references/spec-probes.md +19 -1
  112. package/skills/gsd-path-define/scripts/_common.py +76 -2
  113. package/skills/gsd-path-define/scripts/archive_milestone.py +129 -7
  114. package/skills/gsd-path-define/scripts/build_recovery.py +25 -2
  115. package/skills/gsd-path-define/scripts/check_handoffs.py +437 -30
  116. package/skills/gsd-path-define/scripts/check_task_briefs.py +135 -11
  117. package/skills/gsd-path-define/scripts/discussion_validate.py +46 -10
  118. package/skills/gsd-path-define/scripts/integration.py +196 -39
  119. package/skills/gsd-path-define/scripts/isolation.py +1000 -38
  120. package/skills/gsd-path-define/scripts/loop_run.py +9 -4
  121. package/skills/gsd-path-define/scripts/members.py +17 -3
  122. package/skills/gsd-path-define/scripts/pipeline_state.py +10 -2
  123. package/skills/gsd-path-define/scripts/state_checkpoint.py +15 -0
  124. package/skills/gsd-path-define/scripts/worktree_paths.py +45 -0
  125. package/skills/gsd-path-define/templates/intent.md +5 -3
  126. package/skills/gsd-path-discuss/references/runtime-selection.md +36 -0
  127. package/skills/gsd-path-discuss/scripts/_common.py +76 -2
  128. package/skills/gsd-path-discuss/scripts/loop_run.py +9 -4
  129. package/skills/gsd-path-discuss/scripts/worktree_paths.py +45 -0
  130. package/skills/gsd-path-docs-audit/references/runtime-selection.md +36 -0
  131. package/skills/gsd-path-docs-audit/scripts/_common.py +76 -2
  132. package/skills/gsd-path-docs-audit/scripts/archive_milestone.py +129 -7
  133. package/skills/gsd-path-docs-audit/scripts/build_recovery.py +25 -2
  134. package/skills/gsd-path-docs-audit/scripts/check_handoffs.py +437 -30
  135. package/skills/gsd-path-docs-audit/scripts/check_task_briefs.py +135 -11
  136. package/skills/gsd-path-docs-audit/scripts/discussion_validate.py +46 -10
  137. package/skills/gsd-path-docs-audit/scripts/integration.py +196 -39
  138. package/skills/gsd-path-docs-audit/scripts/isolation.py +1000 -38
  139. package/skills/gsd-path-docs-audit/scripts/loop_run.py +9 -4
  140. package/skills/gsd-path-docs-audit/scripts/members.py +17 -3
  141. package/skills/gsd-path-docs-audit/scripts/pipeline_state.py +10 -2
  142. package/skills/gsd-path-docs-audit/scripts/state_checkpoint.py +15 -0
  143. package/skills/gsd-path-docs-audit/scripts/worktree_paths.py +45 -0
  144. package/skills/gsd-path-forensics/references/runtime-selection.md +36 -0
  145. package/skills/gsd-path-forensics/scripts/_common.py +76 -2
  146. package/skills/gsd-path-forensics/scripts/loop_run.py +9 -4
  147. package/skills/gsd-path-forensics/scripts/pipeline_diagnose.py +104 -1
  148. package/skills/gsd-path-forensics/scripts/worktree_paths.py +45 -0
  149. package/skills/gsd-path-inspect/references/runtime-selection.md +36 -0
  150. package/skills/gsd-path-inspect/scripts/_common.py +76 -2
  151. package/skills/gsd-path-inspect/scripts/archive_milestone.py +129 -7
  152. package/skills/gsd-path-inspect/scripts/build_recovery.py +25 -2
  153. package/skills/gsd-path-inspect/scripts/check_handoffs.py +437 -30
  154. package/skills/gsd-path-inspect/scripts/check_task_briefs.py +135 -11
  155. package/skills/gsd-path-inspect/scripts/discussion_validate.py +46 -10
  156. package/skills/gsd-path-inspect/scripts/integration.py +196 -39
  157. package/skills/gsd-path-inspect/scripts/isolation.py +1000 -38
  158. package/skills/gsd-path-inspect/scripts/loop_run.py +9 -4
  159. package/skills/gsd-path-inspect/scripts/members.py +17 -3
  160. package/skills/gsd-path-inspect/scripts/pipeline_state.py +10 -2
  161. package/skills/gsd-path-inspect/scripts/state_checkpoint.py +15 -0
  162. package/skills/gsd-path-inspect/scripts/workflow_run.py +10 -3
  163. package/skills/gsd-path-inspect/scripts/worktree_paths.py +45 -0
  164. package/skills/gsd-path-loop/references/runtime-selection.md +36 -0
  165. package/skills/gsd-path-loop/scripts/_common.py +76 -2
  166. package/skills/gsd-path-loop/scripts/loop_run.py +9 -4
  167. package/skills/gsd-path-loop/scripts/worktree_paths.py +45 -0
  168. package/skills/gsd-path-migrate/references/runtime-selection.md +36 -0
  169. package/skills/gsd-path-migrate/scripts/_common.py +76 -2
  170. package/skills/gsd-path-migrate/scripts/loop_run.py +9 -4
  171. package/skills/gsd-path-plan/SKILL.md +13 -3
  172. package/skills/gsd-path-plan/references/planner.md +4 -3
  173. package/skills/gsd-path-plan/references/runtime-selection.md +36 -0
  174. package/skills/gsd-path-plan/references/spec-probes.md +19 -1
  175. package/skills/gsd-path-plan/scripts/_common.py +76 -2
  176. package/skills/gsd-path-plan/scripts/archive_milestone.py +129 -7
  177. package/skills/gsd-path-plan/scripts/build_recovery.py +25 -2
  178. package/skills/gsd-path-plan/scripts/check_handoffs.py +437 -30
  179. package/skills/gsd-path-plan/scripts/check_task_briefs.py +135 -11
  180. package/skills/gsd-path-plan/scripts/discussion_validate.py +46 -10
  181. package/skills/gsd-path-plan/scripts/integration.py +196 -39
  182. package/skills/gsd-path-plan/scripts/isolation.py +1000 -38
  183. package/skills/gsd-path-plan/scripts/loop_run.py +9 -4
  184. package/skills/gsd-path-plan/scripts/members.py +17 -3
  185. package/skills/gsd-path-plan/scripts/pipeline_state.py +10 -2
  186. package/skills/gsd-path-plan/scripts/state_checkpoint.py +15 -0
  187. package/skills/gsd-path-plan/scripts/workflow_run.py +10 -3
  188. package/skills/gsd-path-plan/scripts/worktree_paths.py +45 -0
  189. package/skills/gsd-path-plan/templates/plan.md +3 -2
  190. package/skills/gsd-path-plan/templates/task.md +8 -1
  191. package/skills/gsd-path-research/references/runtime-selection.md +36 -0
  192. package/skills/gsd-path-research/scripts/_common.py +76 -2
  193. package/skills/gsd-path-research/scripts/archive_milestone.py +129 -7
  194. package/skills/gsd-path-research/scripts/build_recovery.py +25 -2
  195. package/skills/gsd-path-research/scripts/check_handoffs.py +437 -30
  196. package/skills/gsd-path-research/scripts/check_task_briefs.py +135 -11
  197. package/skills/gsd-path-research/scripts/discussion_validate.py +46 -10
  198. package/skills/gsd-path-research/scripts/integration.py +196 -39
  199. package/skills/gsd-path-research/scripts/isolation.py +1000 -38
  200. package/skills/gsd-path-research/scripts/loop_run.py +9 -4
  201. package/skills/gsd-path-research/scripts/members.py +17 -3
  202. package/skills/gsd-path-research/scripts/pipeline_state.py +10 -2
  203. package/skills/gsd-path-research/scripts/state_checkpoint.py +15 -0
  204. package/skills/gsd-path-research/scripts/worktree_paths.py +45 -0
  205. package/skills/gsd-path-roadmap/references/runtime-selection.md +36 -0
  206. package/skills/gsd-path-roadmap/scripts/_common.py +76 -2
  207. package/skills/gsd-path-roadmap/scripts/archive_milestone.py +129 -7
  208. package/skills/gsd-path-roadmap/scripts/build_recovery.py +25 -2
  209. package/skills/gsd-path-roadmap/scripts/check_handoffs.py +437 -30
  210. package/skills/gsd-path-roadmap/scripts/check_task_briefs.py +135 -11
  211. package/skills/gsd-path-roadmap/scripts/discussion_validate.py +46 -10
  212. package/skills/gsd-path-roadmap/scripts/integration.py +196 -39
  213. package/skills/gsd-path-roadmap/scripts/isolation.py +1000 -38
  214. package/skills/gsd-path-roadmap/scripts/loop_run.py +9 -4
  215. package/skills/gsd-path-roadmap/scripts/members.py +17 -3
  216. package/skills/gsd-path-roadmap/scripts/pipeline_state.py +10 -2
  217. package/skills/gsd-path-roadmap/scripts/state_checkpoint.py +15 -0
  218. package/skills/gsd-path-roadmap/scripts/worktree_paths.py +45 -0
  219. package/skills/gsd-path-ship/SKILL.md +41 -8
  220. package/skills/gsd-path-ship/references/reviewer-final.md +10 -0
  221. package/skills/gsd-path-ship/references/reviewer-wave.md +71 -27
  222. package/skills/gsd-path-ship/references/reviewer.md +21 -8
  223. package/skills/gsd-path-ship/references/runtime-selection.md +36 -0
  224. package/skills/gsd-path-ship/references/spec-probes.md +19 -1
  225. package/skills/gsd-path-ship/scripts/_common.py +76 -2
  226. package/skills/gsd-path-ship/scripts/archive_milestone.py +129 -7
  227. package/skills/gsd-path-ship/scripts/build_recovery.py +25 -2
  228. package/skills/gsd-path-ship/scripts/build_state.py +333 -8
  229. package/skills/gsd-path-ship/scripts/check_handoffs.py +437 -30
  230. package/skills/gsd-path-ship/scripts/check_task_briefs.py +135 -11
  231. package/skills/gsd-path-ship/scripts/discussion_validate.py +46 -10
  232. package/skills/gsd-path-ship/scripts/integration.py +196 -39
  233. package/skills/gsd-path-ship/scripts/isolation.py +1000 -38
  234. package/skills/gsd-path-ship/scripts/lean_verification.py +28 -13
  235. package/skills/gsd-path-ship/scripts/loop_run.py +9 -4
  236. package/skills/gsd-path-ship/scripts/members.py +17 -3
  237. package/skills/gsd-path-ship/scripts/pipeline_state.py +10 -2
  238. package/skills/gsd-path-ship/scripts/state_checkpoint.py +15 -0
  239. package/skills/gsd-path-ship/scripts/workflow_run.py +10 -3
  240. package/skills/gsd-path-ship/scripts/worktree_paths.py +45 -0
  241. package/skills/gsd-path-ship/templates/wave-review.md +24 -8
  242. package/skills/gsd-path-undo/references/runtime-selection.md +36 -0
  243. package/skills/gsd-path-undo/scripts/_common.py +76 -2
  244. package/skills/gsd-path-undo/scripts/loop_run.py +9 -4
  245. package/skills/gsd-path-undo/scripts/worktree_paths.py +45 -0
  246. package/skills/path/BUILD.md +4 -1
  247. package/skills/path/DEFINE.md +2 -1
  248. package/skills/path/PLAN.md +13 -3
  249. package/skills/path/SHIP.md +41 -8
  250. package/skills/path/SKILL.md +16 -1
  251. package/skills/path/references/build-native.md +28 -7
  252. package/skills/path/references/coder.md +6 -3
  253. package/skills/path/references/planner.md +4 -3
  254. package/skills/path/references/reviewer-final.md +10 -0
  255. package/skills/path/references/reviewer-wave.md +71 -27
  256. package/skills/path/references/reviewer.md +21 -8
  257. package/skills/path/references/runtime-selection.md +36 -0
  258. package/skills/path/references/spec-probes.md +19 -1
  259. package/skills/path/scripts/_common.py +76 -2
  260. package/skills/path/scripts/archive_milestone.py +129 -7
  261. package/skills/path/scripts/build_recovery.py +25 -2
  262. package/skills/path/scripts/build_state.py +333 -8
  263. package/skills/path/scripts/check_handoffs.py +437 -30
  264. package/skills/path/scripts/check_task_briefs.py +135 -11
  265. package/skills/path/scripts/discussion_validate.py +46 -10
  266. package/skills/path/scripts/dispatch_driver.py +33 -3
  267. package/skills/path/scripts/git_guard.py +44 -4
  268. package/skills/path/scripts/integration.py +196 -39
  269. package/skills/path/scripts/isolation.py +1000 -38
  270. package/skills/path/scripts/lean_verification.py +28 -13
  271. package/skills/path/scripts/loop_run.py +9 -4
  272. package/skills/path/scripts/members.py +17 -3
  273. package/skills/path/scripts/pipeline_state.py +10 -2
  274. package/skills/path/scripts/state_checkpoint.py +15 -0
  275. package/skills/path/scripts/status_runtime.py +73 -2
  276. package/skills/path/scripts/workflow_run.py +10 -3
  277. package/skills/path/scripts/worktree_paths.py +45 -0
  278. package/skills/path/templates/intent.md +5 -3
  279. package/skills/path/templates/plan.md +3 -2
  280. package/skills/path/templates/task.md +8 -1
  281. package/skills/path/templates/wave-review.md +24 -8
@@ -14,6 +14,7 @@ import sys
14
14
  # Runtime helpers must not modify their immutable installation.
15
15
  sys.dont_write_bytecode = True
16
16
 
17
+ import atexit
17
18
  import contextlib
18
19
  import errno
19
20
  import json
@@ -23,9 +24,10 @@ import shlex
23
24
  import shutil
24
25
  import signal
25
26
  import subprocess
27
+ import tempfile
26
28
  import time
27
29
  from pathlib import Path
28
- from typing import BinaryIO, Iterable, Iterator, List, Optional, Sequence, Tuple
30
+ from typing import BinaryIO, Iterable, Iterator, List, Optional, Sequence, Set, Tuple
29
31
 
30
32
 
31
33
  # Chosen by what imports, not os.name, so tests can emulate Windows on POSIX.
@@ -65,6 +67,23 @@ VERIFY_LEDGER_PATH = ".project/build/verify-ledger.jsonl"
65
67
  VERIFY_RESULTS = ("pass", "fail")
66
68
  VERIFY_LEDGER_SCHEMA = "gsd-path/verify-ledger/v2"
67
69
  VERIFY_BLOCK_PATTERN = re.compile(r"```bash[ \t]*\n(?P<block>.*?)```", re.DOTALL)
70
+ # The owner citation a withdrawn task tombstone must carry in its Log (#483).
71
+ # One definition shared by every gate that reads the representation.
72
+ WITHDRAWN_CITATION = re.compile(
73
+ r"^- Withdrawn — (?P<date>\d{4}-\d{2}-\d{2}): (?P<ruling>.+)$", re.MULTILINE)
74
+
75
+
76
+ def withdrawn_citations(task_text: str) -> list[str]:
77
+ """The owner citations in a withdrawn task's Log, as `date: ruling`.
78
+
79
+ The Log is the append-only record of rulings; a Withdrawn line in any
80
+ other section is not the citation the gates accept (#483).
81
+ """
82
+ body = section_body(task_text, "Log")
83
+ if body is None:
84
+ return []
85
+ return [f"{m.group('date')}: {m.group('ruling')}"
86
+ for m in WITHDRAWN_CITATION.finditer(body)]
68
87
 
69
88
  REVIEW_BULLET_HEADING_PATTERN = re.compile(r"^#{2,3} ")
70
89
  REVIEW_BULLET_FIELD_LABEL_PATTERN = re.compile(r"^- \*\*[^*]+\*\*:")
@@ -197,6 +216,19 @@ def section_body(text: str, heading: str) -> Optional[str]:
197
216
  return match.group("body") if match else None
198
217
 
199
218
 
219
+ def contract_shapes(body: str) -> Set[str]:
220
+ """The whitespace-normalized `- ` entries of an Interface contract body.
221
+
222
+ HTML comments are dropped and continuation lines join their entry. A
223
+ body that says only `None` has no shapes.
224
+ """
225
+ cleaned = re.sub(r"(?s)<!--.*?-->", "", body).strip()
226
+ if " ".join(cleaned.split()) in {"", "None", "- None"}:
227
+ return set()
228
+ entries = re.split(r"(?m)^-[ \t]+", cleaned)
229
+ return {shape for shape in (" ".join(entry.split()) for entry in entries) if shape}
230
+
231
+
200
232
  def overlapping_paths(left: Sequence[str], right: Sequence[str]) -> List[str]:
201
233
  """Return the sorted paths that two task `files` lists both cover.
202
234
 
@@ -875,8 +907,50 @@ def find_bash() -> str:
875
907
  )
876
908
 
877
909
 
910
+ def _remove_quietly(path: str) -> None:
911
+ try:
912
+ os.remove(path)
913
+ except OSError:
914
+ pass
915
+
916
+
917
+ def _command_script_dir() -> str:
918
+ """A temp directory for command scripts that cannot dirty the running tree.
919
+
920
+ TMPDIR/TEMP/TMP may point into the tree being verified, and a script file
921
+ there shows up in git status and blocks landings (#448), so fall back to a
922
+ system directory when the environment temp sits inside the current tree.
923
+ """
924
+ candidate = os.path.realpath(tempfile.gettempdir())
925
+ tree = os.path.realpath(os.getcwd())
926
+ if candidate != tree and not candidate.startswith(tree + os.sep):
927
+ return candidate
928
+ for fallback in (
929
+ os.path.join(os.environ.get("SystemRoot", r"C:\Windows"), "Temp"),
930
+ "/var/tmp",
931
+ "/tmp",
932
+ ):
933
+ resolved = os.path.realpath(fallback)
934
+ if os.path.isdir(resolved) and resolved != tree and not resolved.startswith(tree + os.sep):
935
+ return resolved
936
+ raise OSError("no temp directory outside the current worktree for a command script")
937
+
938
+
878
939
  def bash_argv(command: str) -> List[str]:
879
- return [find_bash(), "-c", command]
940
+ if os.name != "nt":
941
+ return [find_bash(), "-c", command]
942
+ # Git for Windows' bash (MSYS2) cuts an argument that carries a double
943
+ # quote near 8 KB before bash sees it, so a long Verify dies mid-script
944
+ # or silently passes when the cut lands in comments (#448). Run the exact
945
+ # bytes from a temp file outside every worktree instead.
946
+ bash = find_bash()
947
+ descriptor, path = tempfile.mkstemp(
948
+ prefix="gsd-path-command-", suffix=".sh", dir=_command_script_dir()
949
+ )
950
+ with os.fdopen(descriptor, "w", encoding="utf-8", newline="\n") as handle:
951
+ handle.write(command)
952
+ atexit.register(_remove_quietly, path)
953
+ return [bash, path]
880
954
 
881
955
 
882
956
  # cmd.exe re-parses the arguments of a .cmd or .bat target, so these would
@@ -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: