@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
package/CHANGELOG.md CHANGED
@@ -11,6 +11,36 @@ app and the daemon have their own file,
11
11
  [daemon/app/CHANGELOG.md](daemon/app/CHANGELOG.md). Release bookkeeping and
12
12
  test-only changes are not listed; the Git history has them.
13
13
 
14
+ ## [1.7.1] - 2026-10-31
15
+
16
+ ### Changed
17
+ - Focus reviews on agreed requirements and required proof, without searching for unrelated improvements.
18
+ - Require an explicit request for adversarial review, and limit repeat reviews to repaired failures and affected checks.
19
+
20
+ ### Fixed
21
+ - Resume a patch planner after its re-entry checkpoint without rejecting its task log.
22
+
23
+ ## [1.7.0] - 2026-10-30
24
+
25
+ ### Added
26
+ - Publish runtime-store entries without installing them, and adopt approved successor runtimes on closed milestones.
27
+ - Prepare member task environments with a declared command and retry tasks after an environment failure.
28
+ - Recover member work that already reached its default branch, including published ancestors and rebase-merged pull requests.
29
+ - Trace accepted task evidence through a generated evidence manifest.
30
+
31
+ ### Changed
32
+ - Check task brief paths in batches and report progress during larger plans.
33
+ - Record the actual verification directory and native agent in member task receipts.
34
+
35
+ ### Fixed
36
+ - Resume patch work after a ship-stage review without losing its checkpoint.
37
+ - Keep blocked plans and repair waves on the correct recovery route.
38
+ - Require wave reviews to account for every task claim and authorize claim dispositions through owner corrections.
39
+ - Preserve disabled hooks, permit safe read-only fetches, and tighten PowerShell and repository deletion guards.
40
+ - Remove isolated worktrees without traversing symlinks or Windows junctions.
41
+ - Pin sibling task files during member activation and resolve member markers in final-review sidecars.
42
+ - Retry project verification after a host failure and run long verification scripts correctly on Windows.
43
+
14
44
  ## [1.6.2] - 2026-10-17
15
45
 
16
46
  ### Changed
package/DOCS.md CHANGED
@@ -412,9 +412,35 @@ A new machine needs the selected runtime installed before status and guards can
412
412
  run. `gsd-path --runtime-restore --project /absolute/project --source-root
413
413
  /path/to/matching/package` restores that exact version without changing project
414
414
  files. The supplied package must match both the version and digest; custom source
415
- builds may share a release version while having different digests. No command
415
+ builds may share a release version while having different digests. One exception:
416
+ a pin on a runtime that names successors (below) restores from its existing
417
+ stamped store entry when the package has the same version and files, since only
418
+ the successors stamp differs. No command
416
419
  silently substitutes a newer runtime, and status/guards never install or download.
417
420
 
421
+ To publish a runtime to the shared store without selecting it for any project,
422
+ run `python3 -B <absolute runtime_store.py> publish --source /path/to/package`
423
+ (and optionally `--provenance "hotfix for <issue>"`). It prints the runtime's
424
+ version, digest, and verified store path as JSON. Publication alone neither
425
+ authorizes execution nor changes a project's trusted runtime — a project runs
426
+ new bytes only after its own pin selects that digest, or after the pinned
427
+ runtime's own hashed manifest names them (below). `--expected-version`
428
+ with `--expected-digest` refuses a source that does not match, `--dry-run`
429
+ computes the identity without writing, and `--repair` replaces an entry that
430
+ fails verification.
431
+
432
+ A runtime can bless successors: publish the successor package first, then
433
+ publish its predecessor with `--name-successor <successor digest>` (repeatable).
434
+ The list is recorded inside the predecessor's hashed `manifest.json`, so the
435
+ blessing is covered by the predecessor's own digest — nothing outside its bytes
436
+ can add to it. On a closed milestone, where the pin can no longer change, the
437
+ guard accepts a helper from a successor only when the pinned runtime's hashed
438
+ manifest names it (directly or through a chain of blessed runtimes, each hop
439
+ hash-locked); every unlisted runtime stays refused. Runtimes published before
440
+ successors existed can never name one without changing their digest, so pins
441
+ on those runtimes keep the run-from-another-cwd workaround until an
442
+ owner-approved repin on a legal branch.
443
+
418
444
  For an old `.gsd-path/runtime/`, run `gsd-path --runtime-migrate --project
419
445
  /absolute/project --dry-run`, then the same command without `--dry-run` and review
420
446
  the Git diff. Migration removes the generated files from the checkout and writes
package/FULL.md CHANGED
@@ -293,8 +293,8 @@ from `files` unless it is that allowed Project-verify copy.
293
293
 
294
294
  Build reviews each completed wave against task criteria and the INTENT success
295
295
  criteria that wave owns. Each wave carries a review depth — `full`,
296
- `verify-only` for low-risk waves, or sparingly `deep`
297
- for irreversible or security-critical waves. The canonical
296
+ `verify-only` for low-risk waves, or `deep` only when INTENT.md records an
297
+ explicit user request for adversarial review. The canonical
298
298
  [build contract](skills/gsd-path-build/SKILL.md) defines each depth's writer
299
299
  and evidence rules. Review findings are keyed by their failed criterion and
300
300
  carried forward across cycles, so a criterion failing again after its fix task
@@ -464,7 +464,8 @@ approves.
464
464
  from two sessions at the same time.
465
465
  - Do not push `gsd-path/M00N` by hand, and do not merge another branch into
466
466
  it.
467
- - Do not squash or rebase the integration pull request or merge request.
467
+ - Do not squash the integration pull request or merge request, and do not
468
+ rebase or fast-forward an `external-landing` merge request.
468
469
 
469
470
  ---
470
471
 
package/HOOKS.md CHANGED
@@ -11,13 +11,18 @@ Optional enforcement for pipeline invariants prompt contracts cannot guarantee:
11
11
 
12
12
  Closed branches still allow read-only Git inspection (`status`, `log`, `diff`, plain
13
13
  `cat-file` reads of one object, branch and worktree listing) and the router handoff helpers, including
14
- `git fetch origin`, SHA resolution, and next-base selection. Branch switching remains
15
- router-owned. The [ship contract](skills/gsd-path-ship/SKILL.md) owns integration
14
+ `git fetch [REMOTE]`, SHA resolution, and next-base selection. They also allow a
15
+ simple literal delete or move whose every operand is outside the closed repository,
16
+ its member checkouts, and the pipeline state roots, and is no ancestor of them.
17
+ Branch switching remains router-owned. The [ship contract](skills/gsd-path-ship/SKILL.md) owns integration
16
18
  conflict handling and published-validation recovery.
17
19
 
18
20
  `cat-file` options are restricted by `CLOSED_LISTING_GIT_OPTIONS` in
19
21
  [`scripts/guard_hook.py`](scripts/guard_hook.py); filter and text-conversion forms
20
- are blocked because they can execute external commands.
22
+ are blocked because they can execute external commands. A closed-branch fetch takes
23
+ no global option but `-C`, no command-line refspec or option, and a configured remote
24
+ whose repo config fetches read-only; `_closed_fetch_allowed` in the same file owns the
25
+ exact proof.
21
26
 
22
27
  **Docs:** [DOCS.md](DOCS.md) (hub) · [UPDATE.md](UPDATE.md) (refresh hooks) · [QUICK.md](QUICK.md) (first install with `--hooks`)
23
28
 
@@ -94,6 +99,11 @@ hook.
94
99
  the rename when the directory is tracked.
95
100
  - **`<hook>.gsd-path-chained` already exists beside a foreign hook:** the
96
101
  installer stops. Decide which of the two files you want to keep.
102
+ - **You disabled a hook by renaming it to `<hook>.disabled*`:** while
103
+ `<hook>` itself is absent, the installer and `--member-of` leave it disabled
104
+ (`(left disabled)`), and doctor reports it as deliberately disabled instead
105
+ of missing. To enable the guard again, remove or rename the disabled file
106
+ and rerun the installer.
97
107
 
98
108
  Git hooks are local to a clone. A bot that merges on the server, such as
99
109
  Renovate, never runs them, and its commits on the default branch do not
@@ -195,6 +205,18 @@ See [UPDATE.md](UPDATE.md).
195
205
  target a routing control (`.git`, `.project/STATE.md`, `.project/next`,
196
206
  `.gsd-path`) while `.project/STATE.md` exists, or target a product file
197
207
  outside the routed build phase (same rule as direct file-edit tools)
208
+ - PowerShell expression forms the guard cannot inspect: a .NET static member
209
+ call (`[System.IO.File]::WriteAllText`) outside a small read-only type list
210
+ (`[Math]`, `[string]`, number, date, and similar types), anywhere in the
211
+ command, including script blocks; dynamic-execution commands and their
212
+ aliases (`Invoke-Expression`/`iex`, `Invoke-Command`, `Add-Type`,
213
+ `Start-Process`, `Start-Job`, `New-Object`) as the program or inside a
214
+ script block; the `&` call operator; and the same forms inside wrapped
215
+ `-Command` payloads. A `$name = value` or `$env:NAME = value` assignment
216
+ with only plain literal words is process-local and passes; a bare-word
217
+ value is checked as the command it runs. A richer value keeps the
218
+ unresolvable-executable denial, and `$env:HOME`, `$env:XDG_CONFIG_HOME`,
219
+ and `$env:GIT_*` are denied like the `env` form
198
220
  - repeated assignments to the same shell variable within one command; split
199
221
  these into separate tool calls so write targets can be resolved
200
222
  - copies whose destinations, including nested recursive-copy entries, resolve
@@ -220,6 +242,10 @@ See [UPDATE.md](UPDATE.md).
220
242
  backticks, glob characters (`*`, `?`, `[`, `]`), braces, or `..` path components,
221
243
  and must not start with `~`. Quoted spaces in paths and quoted punctuation
222
244
  within arguments, such as the shipment event's semicolon, are allowed
245
+ - deletion or move commands, in every phase, that target or contain a
246
+ pipeline state root (`~/.gsd-path`, `GSD_PATH_WORKTREE_ROOT`, or a pinned
247
+ workspace root). Deletes inside the agent's own managed worktree under such
248
+ a root stay allowed. Workspace receipts that cannot be read deny the command
223
249
  - deletion or move commands outside a single simple segment, including command
224
250
  chains, pipes, newlines, grouping, directory changes, and command substitution
225
251
  - deletion or move commands with any argument outside the literal-path character
@@ -229,6 +255,10 @@ See [UPDATE.md](UPDATE.md).
229
255
  tilde paths are denied even when quoted. This also applies to
230
256
  `find -delete` and supported destructive aliases
231
257
  - destructive Git commands nested in supported shell and command wrappers
258
+ - the operator-only `isolation.py runtime-hotfix` recovery, and every Git
259
+ command with a word that contains `refs/gsd-path/` or `runtime-hotfix`;
260
+ see [runtime selection](skills/gsd-path/references/runtime-selection.md)
261
+ for the receipt, the covered spellings, and the limits
232
262
  - Git arguments that contain a shell parameter. Only plain `$name` and
233
263
  `${name}` expansions pass, and only inside plain double-quoted spans of
234
264
  read-only subcommands (`status`, `log`, `diff`, `show`, and the other
@@ -339,7 +369,11 @@ such as `rm -rf scratch` passes the archive check; other guard rules still apply
339
369
  `files:`, and a `Files:` list of exactly the staged paths. Outside build the
340
370
  reviewed HEAD is frozen; product fixes reopen through the patch plan.
341
371
  The validated integration merge is allowed to complete before publication
342
- proof exists; an ordinary commit with an integration subject is not exempt
372
+ proof exists; an ordinary commit with an integration subject is not exempt.
373
+ A landing commit may also stage `.gsd-path/runtime.json` only when a
374
+ runtime-hotfix receipt exists for that task and base and the staged pin
375
+ names exactly the receipt's runtime; see
376
+ [runtime selection](skills/gsd-path/references/runtime-selection.md)
343
377
  - deleting the recorded branch or switching to another branch while carrying
344
378
  unfinished state does not release product commit protection
345
379
  - pushes (to any remote) that move a `gsd-path/M###` ref anywhere but its
package/QUICK.md CHANGED
@@ -14,7 +14,7 @@ One router skill drives a gated pipeline: inspect → define → research → de
14
14
  plan → build → ship. Every handoff lives in `.project/` so any session can resume from disk.
15
15
 
16
16
  **Hosts:** Codex, Claude Code, Grok, OpenCode, Antigravity, Cursor, and Kimi
17
- Code have a passing live milestone for release 1.6.2. Copilot CLI, Qwen, Zed,
17
+ Code have a passing live milestone for release 1.7.1. Copilot CLI, Qwen, Zed,
18
18
  Kiro, and Muse Code install, but have no live milestone for that release. See
19
19
  [host tiers](README.md).
20
20
 
package/README.md CHANGED
@@ -9,11 +9,11 @@ milestone loop. While a milestone builds, the next one can be planned in
9
9
  parallel under `.project/next/` (lookahead); a building milestone can also be
10
10
  abandoned on an explicit ruling, archiving its partial work for a re-slice.
11
11
 
12
- **Hosts with a passing full live milestone for release 1.6.2:** Codex, Claude
12
+ **Hosts with a passing full live milestone for release 1.7.1:** Codex, Claude
13
13
  Code, Grok, OpenCode, Antigravity CLI, Cursor, and Kimi Code.
14
14
 
15
15
  **Hosts with an installer and a dispatch contract, but no live milestone for
16
- release 1.6.2:** GitHub Copilot CLI (account quota exhausted), Qwen Code, Zed,
16
+ release 1.7.1:** GitHub Copilot CLI (account quota exhausted), Qwen Code, Zed,
17
17
  and Kiro (API credits), and Muse Code (no live release evidence recorded yet).
18
18
  These hosts are excluded from the release evaluation. Exclusion is not a pass.
19
19
 
@@ -24,16 +24,16 @@ The [host trust matrix](https://github.com/open-gsd/gsd-path/blob/main/docs/trus
24
24
  is the source of truth for live milestone proof and each host's guard tier.
25
25
 
26
26
  <!-- release-docs -->
27
- ## What's new in 1.6.2 — 2026-10-17
27
+ ## What's new in 1.7.1 — 2026-10-31
28
28
 
29
- **Latest npm release:** [@opengsd/gsd-path@1.6.2](https://www.npmjs.com/package/@opengsd/gsd-path/v/1.6.2) — [release notes](CHANGELOG.md#162---2026-10-17)
29
+ **Latest npm release:** [@opengsd/gsd-path@1.7.1](https://www.npmjs.com/package/@opengsd/gsd-path/v/1.7.1) — [release notes](CHANGELOG.md#171---2026-10-31)
30
30
 
31
31
  **Recent highlights**
32
- - keep the README What's new section current from release notes
33
- - read quoted STATE values in the ship checkpoint guard
34
- - refuse a build checkpoint while STATE is ship/active
35
- - keep evidence-line citations when final review reuses a wave
36
- - record 1.6.2 live host release evidence
32
+ - scope reviews to agreed acceptance criteria
33
+ - accept a resumed patch planner's re-entry checkpoint
34
+ - Point current-release docs at 1.7.1 evidence
35
+ - complete 1.7.1 release trust evidence
36
+ - record Cursor proof and pending 1.7.1 release checks
37
37
 
38
38
  <!-- /release-docs -->
39
39
 
@@ -196,7 +196,8 @@ What the app does:
196
196
  - **Project setup:** runtime version, guard hooks, health check, and, for a
197
197
  multi-repo coordinator, member hooks and marker repair.
198
198
  - **Settings:** updates, Path settings (shipping mode, models, review panel),
199
- watched folders, model prices, launch at login, and appearance.
199
+ watched folders, model prices, launch at login, appearance, and language
200
+ (English or Simplified Chinese).
200
201
  - **Stats** and the per-project **Environment** editor.
201
202
  - **Tray:** a compact project list, items that need attention, and controls
202
203
  to start, restart, and quit.
@@ -379,7 +380,7 @@ default branch of the coordinator and of the other members. See
379
380
  [ADR 0002](docs/adr/0002-multi-repo-coordinator.md) for the design.
380
381
 
381
382
  `MEMBERS.md` has one section per member, in ship order. `members.py` writes
382
- it; do not edit it by hand:
383
+ it; do not edit it by hand (except `Prepare`, below):
383
384
 
384
385
  ```markdown
385
386
  ## web
@@ -394,6 +395,13 @@ records it one time, from the member's local `origin/HEAD`; run
394
395
  `git remote set-head origin --auto` in the member first. A section without
395
396
  the field means `main`. See [default branch](WORKFLOW.md#default-branch).
396
397
 
398
+ `Prepare` is optional. It is a shell command that the runtime runs inside
399
+ each fresh member isolate before any Verify, to provision what a bare
400
+ worktree lacks (submodules, gitignored build inputs). It must copy, never
401
+ link. `members.py` has no subcommand for it: add it to the member's section
402
+ through a reviewed change. The `gsd-path` skill describes the rules and an
403
+ offline submodule recipe.
404
+
397
405
  - Add members at a milestone boundary: ask the router to add an existing repo
398
406
  (`members.py add`) or to create a new one (`members.py add --create`, after
399
407
  you approve the exact owner, name, visibility, and checkout; a new
package/RUNTIME.md CHANGED
@@ -33,8 +33,10 @@ never changes a recorded base, retries a failed mutation, or repairs state itsel
33
33
  Checkpoint inputs must use the top-level `.project` entries defined by
34
34
  [`isolation.PROJECT_ENTRIES`](scripts/isolation.py); ship verification uses the
35
35
  same allowlist. `isolation.py checkpoint` refuses pending paths under any other
36
- top-level entry and asks for their removal before checkpointing. This check does
37
- not inspect nested entries inside an allowed directory.
36
+ top-level entry that still exist in the worktree and asks for their removal; a
37
+ pending removal (`git rm`, `git mv`, or a deleted file) passes and the checkpoint
38
+ commits it. This check does not inspect nested entries inside an allowed
39
+ directory.
38
40
 
39
41
  For serial work, keep the returned verification sidecar until the coder finishes.
40
42
  Validate the full task diff, reproduce only that patch in the sidecar, compare the
@@ -68,7 +70,7 @@ skeptic attempts under `skeptics/`.
68
70
  | `skeptics --repo <root> --wave <N> --cycle <C> --child-command '<cmd>' [--wait <s>] [--child-timeout <s>]` | Step 7 skeptic branch: one read-only skeptic per `skeptic_groups` locator in its own sidecar at the recorded review base, briefed with the criterion, every observation, and the cited tasks verbatim; validate with `review_findings.py`, collect, retire, then rerun the helper. `none` without skeptic groups; `escalate` for structural blockers or the cycle cap. The skeptic files stay uncommitted for the fix-task or ruling checkpoint. |
69
71
  | `fix-tasks --repo <root> --wave <N> --cycle <C>` | Step 7 batching: one fix task per `review_findings.py collect` batch with the failed criteria and observations verbatim, lint-checked; `escalate` for structural blockers, skeptic groups, cycle cap, or all-refuted. |
70
72
  | `complete --repo <root>` | Completion: clean primary at `build/active` (or a crash-left `build/done`), no unfinished wave, the archive-time review cycle check (`discussion_validate.review_cycle_counts`: contiguous cycles, passing last cycle, panel and skeptic artifacts present); `workflow_run.py build-evidence` at HEAD; `pipeline_state.py transition` to `ship/active` with event `build done; final review pending`; checkpoint `build: complete milestone`. |
71
- | `finish --repo <root> --task-id <id>` | Recover first; return a proven landing without repeating Verify, or verify, land, record, and retire one returned task. Without a dispatch record, or when the newest record is settled and its base differs from the task's current in-progress base (a native retry after a driver block), read the primary task's in-progress state; if it lacks a recorded base, use the parallel task isolate's state only if that worktree is authorized. A blocked record at the same base still refuses the retry, except a host-dispatched record (no child command) while the task is still in progress at that base: `finish` may run again after a failed Verify. A deactivated isolate stays blocked. |
73
+ | `finish --repo <root> --task-id <id>` | Recover first; return a proven landing without repeating Verify, or verify, land, record, and retire one returned task. Without a dispatch record, or when the newest record is settled and its base differs from the task's current in-progress base (a native retry after a driver block), read the primary task's in-progress state; if it lacks a recorded base, use the parallel task isolate's state only if that worktree is authorized. A blocked record at the same base still refuses the retry, except a host-dispatched record (no child command) while the task is still in progress at that base: `finish` may run again after a failed Verify. A deactivated isolate stays blocked. Receipt status `landed`, `question` (the child ended in a question), or `blocked`; a blocked receipt without a blocked entry carries a `reason`. |
72
74
  | `answer --repo <root> --task-id <id> --answer '<text>'` | Append `Orchestrator answer:` to the isolate's task Log; the next `round` redispatches that isolate. |
73
75
  | `status --repo <root>` | Every task's newest dispatch record. |
74
76
 
package/WORKFLOW.md CHANGED
@@ -232,8 +232,10 @@ the user's ruling per applicable edge in INTENT.md `## Edge coverage`:
232
232
  `criterion SCn` (stated in a success criterion), `held-out` (a named test
233
233
  pins it), or `dismissed` with a reason. The prohibition probe asks what each
234
234
  criterion must never silently become and records kept items in
235
- `## Prohibitions` as `criterion SCn`, `judgment`, or `dismissed`. Define
236
- never rules on an edge itself. `scripts/check_handoffs.py intent` checks
235
+ `## Prohibitions` as `criterion SCn`, `judgment`, or `dismissed`. An
236
+ absence criterion ("X does not exist") must state its search space, its
237
+ check, and the result that counts as absent; reviewers judge it over that
238
+ space only. Define never rules on an edge itself. `scripts/check_handoffs.py intent` checks
237
239
  both tables before every approval question, and the `define/done` transition
238
240
  refuses an intent whose tables are incomplete. An INTENT.md without these
239
241
  sections predates the probes and is not rechecked.
@@ -334,6 +336,11 @@ exists).
334
336
  `.project/research/SYNTHESIS.md`, plus relevant existing code. **Output:**
335
337
  `.project/plan/PLAN.md` and one full task file per task in `.project/tasks/`.
336
338
 
339
+ A failed plan revision records `plan/blocked`. The router stops there and asks
340
+ for the correction or user decision named by the plan gate. It does not
341
+ automatically restart planning. After resolving the blocker, invoke
342
+ `$gsd-path-plan` explicitly; its resume records `planning resumed`.
343
+
337
344
  Order work by risk and dependency:
338
345
 
339
346
  1. Wave one burns down assumptions that could invalidate the plan.
@@ -366,8 +373,8 @@ require a verdict per owned SC id. Acceptance criteria,
366
373
  owned SCs, and Verify are the contract; the coder owns
367
374
  implementation decisions inside the stated constraints. The planner reads
368
375
  `.project/LESSONS.md` when present and assigns each wave a `Review depth` —
369
- `full`, `verify-only` for low-risk waves, or sparingly `deep` for
370
- irreversible or security-critical waves; wave 1 and any wave touching
376
+ `full`, `verify-only` for low-risk waves, or `deep` only when INTENT.md
377
+ records an explicit user request for adversarial review; wave 1 and any wave touching
371
378
  authentication, payments, data migration, or concurrency stays `full` or
372
379
  `deep`. PLAN.md Config may name an optional `review_panel` (`off` by
373
380
  default, or `detected` / a named family list). In program flow CHARTER.md
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@opengsd/gsd-path",
3
- "version": "1.6.2",
3
+ "version": "1.7.1",
4
4
  "description": "Disk-backed AI agent pipeline. Docs: DOCS.md (start here), QUICK.md, FULL.md, UPDATE.md",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -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: