@hunter-harness/workflow-harness 0.2.55 → 0.2.57

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 (246) hide show
  1. package/harness/bundles/general/claude-code/.harness-build.json +1 -1
  2. package/harness/bundles/general/claude-code/README.md +3 -3
  3. package/harness/bundles/general/claude-code/contracts/workflow-policy.json +1 -1
  4. package/harness/bundles/general/claude-code/harness-archive/SKILL.md +21 -33
  5. package/harness/bundles/general/claude-code/harness-archive/checklist.md +6 -10
  6. package/harness/bundles/general/claude-code/harness-codebase-map/SKILL.md +1 -1
  7. package/harness/bundles/general/claude-code/harness-knowledge-ingest/SKILL.md +1 -1
  8. package/harness/bundles/general/claude-code/harness-knowledge-query/SKILL.md +1 -1
  9. package/harness/bundles/general/claude-code/harness-plan/SKILL.md +7 -3
  10. package/harness/bundles/general/claude-code/harness-review/SKILL.md +6 -4
  11. package/harness/bundles/general/claude-code/harness-run/SKILL.md +7 -5
  12. package/harness/bundles/general/claude-code/harness-run/checklist.md +1 -1
  13. package/harness/bundles/general/claude-code/harness-run/reference.md +2 -2
  14. package/harness/bundles/general/claude-code/harness-submit/SKILL.md +6 -6
  15. package/harness/bundles/general/claude-code/harness-submit/checklist.md +1 -1
  16. package/harness/bundles/general/claude-code/harness-sync/SKILL.md +1 -1
  17. package/harness/bundles/general/claude-code/harness-test/SKILL.md +6 -6
  18. package/harness/bundles/general/claude-code/harness-test/checklist.md +1 -1
  19. package/harness/bundles/general/claude-code/harness-test/reference.md +2 -2
  20. package/harness/bundles/general/claude-code/protocols/ledger-protocol.md +3 -3
  21. package/harness/bundles/general/claude-code/scripts/harness_archive.py +276 -67
  22. package/harness/bundles/general/claude-code/scripts/harness_context.py +165 -41
  23. package/harness/bundles/general/claude-code/scripts/harness_events.py +46 -4
  24. package/harness/bundles/general/claude-code/scripts/harness_events_sync.py +109 -3
  25. package/harness/bundles/general/claude-code/scripts/harness_fixback.py +189 -1
  26. package/harness/bundles/general/claude-code/scripts/harness_gate.py +188 -2
  27. package/harness/bundles/general/claude-code/scripts/harness_ledger.py +193 -11
  28. package/harness/bundles/general/claude-code/scripts/harness_plan_finalize.py +80 -0
  29. package/harness/bundles/general/codebuddy/.harness-build.json +1 -1
  30. package/harness/bundles/general/codebuddy/README.md +3 -3
  31. package/harness/bundles/general/codebuddy/contracts/workflow-policy.json +1 -1
  32. package/harness/bundles/general/codebuddy/harness-archive/SKILL.md +21 -33
  33. package/harness/bundles/general/codebuddy/harness-archive/checklist.md +6 -10
  34. package/harness/bundles/general/codebuddy/harness-codebase-map/SKILL.md +1 -1
  35. package/harness/bundles/general/codebuddy/harness-knowledge-ingest/SKILL.md +1 -1
  36. package/harness/bundles/general/codebuddy/harness-knowledge-query/SKILL.md +1 -1
  37. package/harness/bundles/general/codebuddy/harness-plan/SKILL.md +7 -3
  38. package/harness/bundles/general/codebuddy/harness-review/SKILL.md +6 -4
  39. package/harness/bundles/general/codebuddy/harness-run/SKILL.md +7 -5
  40. package/harness/bundles/general/codebuddy/harness-run/checklist.md +1 -1
  41. package/harness/bundles/general/codebuddy/harness-run/reference.md +2 -2
  42. package/harness/bundles/general/codebuddy/harness-submit/SKILL.md +6 -6
  43. package/harness/bundles/general/codebuddy/harness-submit/checklist.md +1 -1
  44. package/harness/bundles/general/codebuddy/harness-sync/SKILL.md +1 -1
  45. package/harness/bundles/general/codebuddy/harness-test/SKILL.md +6 -6
  46. package/harness/bundles/general/codebuddy/harness-test/checklist.md +1 -1
  47. package/harness/bundles/general/codebuddy/harness-test/reference.md +2 -2
  48. package/harness/bundles/general/codebuddy/protocols/ledger-protocol.md +3 -3
  49. package/harness/bundles/general/codebuddy/scripts/harness_archive.py +276 -67
  50. package/harness/bundles/general/codebuddy/scripts/harness_context.py +165 -41
  51. package/harness/bundles/general/codebuddy/scripts/harness_events.py +46 -4
  52. package/harness/bundles/general/codebuddy/scripts/harness_events_sync.py +109 -3
  53. package/harness/bundles/general/codebuddy/scripts/harness_fixback.py +189 -1
  54. package/harness/bundles/general/codebuddy/scripts/harness_gate.py +188 -2
  55. package/harness/bundles/general/codebuddy/scripts/harness_ledger.py +193 -11
  56. package/harness/bundles/general/codebuddy/scripts/harness_plan_finalize.py +80 -0
  57. package/harness/bundles/general/codex/.harness-build.json +1 -1
  58. package/harness/bundles/general/codex/README.md +3 -3
  59. package/harness/bundles/general/codex/contracts/workflow-policy.json +1 -1
  60. package/harness/bundles/general/codex/harness-archive/SKILL.md +21 -33
  61. package/harness/bundles/general/codex/harness-archive/checklist.md +6 -10
  62. package/harness/bundles/general/codex/harness-codebase-map/SKILL.md +1 -1
  63. package/harness/bundles/general/codex/harness-knowledge-ingest/SKILL.md +1 -1
  64. package/harness/bundles/general/codex/harness-knowledge-query/SKILL.md +1 -1
  65. package/harness/bundles/general/codex/harness-plan/SKILL.md +7 -3
  66. package/harness/bundles/general/codex/harness-review/SKILL.md +4 -4
  67. package/harness/bundles/general/codex/harness-run/SKILL.md +7 -5
  68. package/harness/bundles/general/codex/harness-run/checklist.md +1 -1
  69. package/harness/bundles/general/codex/harness-run/reference.md +2 -2
  70. package/harness/bundles/general/codex/harness-submit/SKILL.md +6 -6
  71. package/harness/bundles/general/codex/harness-submit/checklist.md +1 -1
  72. package/harness/bundles/general/codex/harness-sync/SKILL.md +1 -1
  73. package/harness/bundles/general/codex/harness-test/SKILL.md +6 -6
  74. package/harness/bundles/general/codex/harness-test/checklist.md +1 -1
  75. package/harness/bundles/general/codex/harness-test/reference.md +2 -2
  76. package/harness/bundles/general/codex/protocols/ledger-protocol.md +3 -3
  77. package/harness/bundles/general/codex/scripts/harness_archive.py +276 -67
  78. package/harness/bundles/general/codex/scripts/harness_context.py +165 -41
  79. package/harness/bundles/general/codex/scripts/harness_events.py +46 -4
  80. package/harness/bundles/general/codex/scripts/harness_events_sync.py +109 -3
  81. package/harness/bundles/general/codex/scripts/harness_fixback.py +189 -1
  82. package/harness/bundles/general/codex/scripts/harness_gate.py +188 -2
  83. package/harness/bundles/general/codex/scripts/harness_ledger.py +193 -11
  84. package/harness/bundles/general/codex/scripts/harness_plan_finalize.py +80 -0
  85. package/harness/bundles/general/cursor/.harness-build.json +1 -1
  86. package/harness/bundles/general/cursor/README.md +3 -3
  87. package/harness/bundles/general/cursor/contracts/workflow-policy.json +1 -1
  88. package/harness/bundles/general/cursor/harness-archive/SKILL.md +21 -33
  89. package/harness/bundles/general/cursor/harness-archive/checklist.md +6 -10
  90. package/harness/bundles/general/cursor/harness-codebase-map/SKILL.md +1 -1
  91. package/harness/bundles/general/cursor/harness-knowledge-ingest/SKILL.md +1 -1
  92. package/harness/bundles/general/cursor/harness-knowledge-query/SKILL.md +1 -1
  93. package/harness/bundles/general/cursor/harness-plan/SKILL.md +7 -3
  94. package/harness/bundles/general/cursor/harness-review/SKILL.md +4 -4
  95. package/harness/bundles/general/cursor/harness-run/SKILL.md +7 -5
  96. package/harness/bundles/general/cursor/harness-run/checklist.md +1 -1
  97. package/harness/bundles/general/cursor/harness-run/reference.md +2 -2
  98. package/harness/bundles/general/cursor/harness-submit/SKILL.md +6 -6
  99. package/harness/bundles/general/cursor/harness-submit/checklist.md +1 -1
  100. package/harness/bundles/general/cursor/harness-sync/SKILL.md +1 -1
  101. package/harness/bundles/general/cursor/harness-test/SKILL.md +6 -6
  102. package/harness/bundles/general/cursor/harness-test/checklist.md +1 -1
  103. package/harness/bundles/general/cursor/harness-test/reference.md +2 -2
  104. package/harness/bundles/general/cursor/protocols/ledger-protocol.md +3 -3
  105. package/harness/bundles/general/cursor/scripts/harness_archive.py +276 -67
  106. package/harness/bundles/general/cursor/scripts/harness_context.py +165 -41
  107. package/harness/bundles/general/cursor/scripts/harness_events.py +46 -4
  108. package/harness/bundles/general/cursor/scripts/harness_events_sync.py +109 -3
  109. package/harness/bundles/general/cursor/scripts/harness_fixback.py +189 -1
  110. package/harness/bundles/general/cursor/scripts/harness_gate.py +188 -2
  111. package/harness/bundles/general/cursor/scripts/harness_ledger.py +193 -11
  112. package/harness/bundles/general/cursor/scripts/harness_plan_finalize.py +80 -0
  113. package/harness/bundles/java/claude-code/.harness-build.json +1 -1
  114. package/harness/bundles/java/claude-code/README.md +3 -3
  115. package/harness/bundles/java/claude-code/contracts/workflow-policy.json +1 -1
  116. package/harness/bundles/java/claude-code/harness-apidoc/SKILL.md +1 -1
  117. package/harness/bundles/java/claude-code/harness-archive/SKILL.md +21 -33
  118. package/harness/bundles/java/claude-code/harness-archive/checklist.md +6 -10
  119. package/harness/bundles/java/claude-code/harness-codebase-map/SKILL.md +1 -1
  120. package/harness/bundles/java/claude-code/harness-knowledge-ingest/SKILL.md +1 -1
  121. package/harness/bundles/java/claude-code/harness-knowledge-query/SKILL.md +1 -1
  122. package/harness/bundles/java/claude-code/harness-package/SKILL.md +1 -1
  123. package/harness/bundles/java/claude-code/harness-package/checklist.md +1 -1
  124. package/harness/bundles/java/claude-code/harness-plan/SKILL.md +7 -3
  125. package/harness/bundles/java/claude-code/harness-review/SKILL.md +6 -4
  126. package/harness/bundles/java/claude-code/harness-run/SKILL.md +7 -5
  127. package/harness/bundles/java/claude-code/harness-run/checklist.md +1 -1
  128. package/harness/bundles/java/claude-code/harness-run/reference.md +2 -2
  129. package/harness/bundles/java/claude-code/harness-submit/SKILL.md +6 -6
  130. package/harness/bundles/java/claude-code/harness-submit/checklist.md +1 -1
  131. package/harness/bundles/java/claude-code/harness-sync/SKILL.md +1 -1
  132. package/harness/bundles/java/claude-code/harness-test/SKILL.md +6 -6
  133. package/harness/bundles/java/claude-code/harness-test/checklist.md +1 -1
  134. package/harness/bundles/java/claude-code/harness-test/reference.md +1 -1
  135. package/harness/bundles/java/claude-code/protocols/ledger-protocol.md +3 -3
  136. package/harness/bundles/java/claude-code/scripts/harness_archive.py +276 -67
  137. package/harness/bundles/java/claude-code/scripts/harness_context.py +165 -41
  138. package/harness/bundles/java/claude-code/scripts/harness_events.py +46 -4
  139. package/harness/bundles/java/claude-code/scripts/harness_events_sync.py +109 -3
  140. package/harness/bundles/java/claude-code/scripts/harness_fixback.py +189 -1
  141. package/harness/bundles/java/claude-code/scripts/harness_gate.py +188 -2
  142. package/harness/bundles/java/claude-code/scripts/harness_ledger.py +193 -11
  143. package/harness/bundles/java/claude-code/scripts/harness_plan_finalize.py +80 -0
  144. package/harness/bundles/java/codebuddy/.harness-build.json +1 -1
  145. package/harness/bundles/java/codebuddy/README.md +3 -3
  146. package/harness/bundles/java/codebuddy/contracts/workflow-policy.json +1 -1
  147. package/harness/bundles/java/codebuddy/harness-apidoc/SKILL.md +1 -1
  148. package/harness/bundles/java/codebuddy/harness-archive/SKILL.md +21 -33
  149. package/harness/bundles/java/codebuddy/harness-archive/checklist.md +6 -10
  150. package/harness/bundles/java/codebuddy/harness-codebase-map/SKILL.md +1 -1
  151. package/harness/bundles/java/codebuddy/harness-knowledge-ingest/SKILL.md +1 -1
  152. package/harness/bundles/java/codebuddy/harness-knowledge-query/SKILL.md +1 -1
  153. package/harness/bundles/java/codebuddy/harness-package/SKILL.md +1 -1
  154. package/harness/bundles/java/codebuddy/harness-package/checklist.md +1 -1
  155. package/harness/bundles/java/codebuddy/harness-plan/SKILL.md +7 -3
  156. package/harness/bundles/java/codebuddy/harness-review/SKILL.md +6 -4
  157. package/harness/bundles/java/codebuddy/harness-run/SKILL.md +7 -5
  158. package/harness/bundles/java/codebuddy/harness-run/checklist.md +1 -1
  159. package/harness/bundles/java/codebuddy/harness-run/reference.md +2 -2
  160. package/harness/bundles/java/codebuddy/harness-submit/SKILL.md +6 -6
  161. package/harness/bundles/java/codebuddy/harness-submit/checklist.md +1 -1
  162. package/harness/bundles/java/codebuddy/harness-sync/SKILL.md +1 -1
  163. package/harness/bundles/java/codebuddy/harness-test/SKILL.md +6 -6
  164. package/harness/bundles/java/codebuddy/harness-test/checklist.md +1 -1
  165. package/harness/bundles/java/codebuddy/harness-test/reference.md +1 -1
  166. package/harness/bundles/java/codebuddy/protocols/ledger-protocol.md +3 -3
  167. package/harness/bundles/java/codebuddy/scripts/harness_archive.py +276 -67
  168. package/harness/bundles/java/codebuddy/scripts/harness_context.py +165 -41
  169. package/harness/bundles/java/codebuddy/scripts/harness_events.py +46 -4
  170. package/harness/bundles/java/codebuddy/scripts/harness_events_sync.py +109 -3
  171. package/harness/bundles/java/codebuddy/scripts/harness_fixback.py +189 -1
  172. package/harness/bundles/java/codebuddy/scripts/harness_gate.py +188 -2
  173. package/harness/bundles/java/codebuddy/scripts/harness_ledger.py +193 -11
  174. package/harness/bundles/java/codebuddy/scripts/harness_plan_finalize.py +80 -0
  175. package/harness/bundles/java/codex/.harness-build.json +1 -1
  176. package/harness/bundles/java/codex/README.md +3 -3
  177. package/harness/bundles/java/codex/contracts/workflow-policy.json +1 -1
  178. package/harness/bundles/java/codex/harness-apidoc/SKILL.md +1 -1
  179. package/harness/bundles/java/codex/harness-archive/SKILL.md +21 -33
  180. package/harness/bundles/java/codex/harness-archive/checklist.md +6 -10
  181. package/harness/bundles/java/codex/harness-codebase-map/SKILL.md +1 -1
  182. package/harness/bundles/java/codex/harness-knowledge-ingest/SKILL.md +1 -1
  183. package/harness/bundles/java/codex/harness-knowledge-query/SKILL.md +1 -1
  184. package/harness/bundles/java/codex/harness-package/SKILL.md +1 -1
  185. package/harness/bundles/java/codex/harness-package/checklist.md +1 -1
  186. package/harness/bundles/java/codex/harness-plan/SKILL.md +7 -3
  187. package/harness/bundles/java/codex/harness-review/SKILL.md +4 -4
  188. package/harness/bundles/java/codex/harness-run/SKILL.md +7 -5
  189. package/harness/bundles/java/codex/harness-run/checklist.md +1 -1
  190. package/harness/bundles/java/codex/harness-run/reference.md +2 -2
  191. package/harness/bundles/java/codex/harness-submit/SKILL.md +6 -6
  192. package/harness/bundles/java/codex/harness-submit/checklist.md +1 -1
  193. package/harness/bundles/java/codex/harness-sync/SKILL.md +1 -1
  194. package/harness/bundles/java/codex/harness-test/SKILL.md +6 -6
  195. package/harness/bundles/java/codex/harness-test/checklist.md +1 -1
  196. package/harness/bundles/java/codex/harness-test/reference.md +1 -1
  197. package/harness/bundles/java/codex/protocols/ledger-protocol.md +3 -3
  198. package/harness/bundles/java/codex/scripts/harness_archive.py +276 -67
  199. package/harness/bundles/java/codex/scripts/harness_context.py +165 -41
  200. package/harness/bundles/java/codex/scripts/harness_events.py +46 -4
  201. package/harness/bundles/java/codex/scripts/harness_events_sync.py +109 -3
  202. package/harness/bundles/java/codex/scripts/harness_fixback.py +189 -1
  203. package/harness/bundles/java/codex/scripts/harness_gate.py +188 -2
  204. package/harness/bundles/java/codex/scripts/harness_ledger.py +193 -11
  205. package/harness/bundles/java/codex/scripts/harness_plan_finalize.py +80 -0
  206. package/harness/bundles/java/cursor/.harness-build.json +1 -1
  207. package/harness/bundles/java/cursor/README.md +3 -3
  208. package/harness/bundles/java/cursor/contracts/workflow-policy.json +1 -1
  209. package/harness/bundles/java/cursor/harness-apidoc/SKILL.md +1 -1
  210. package/harness/bundles/java/cursor/harness-archive/SKILL.md +21 -33
  211. package/harness/bundles/java/cursor/harness-archive/checklist.md +6 -10
  212. package/harness/bundles/java/cursor/harness-codebase-map/SKILL.md +1 -1
  213. package/harness/bundles/java/cursor/harness-knowledge-ingest/SKILL.md +1 -1
  214. package/harness/bundles/java/cursor/harness-knowledge-query/SKILL.md +1 -1
  215. package/harness/bundles/java/cursor/harness-package/SKILL.md +1 -1
  216. package/harness/bundles/java/cursor/harness-package/checklist.md +1 -1
  217. package/harness/bundles/java/cursor/harness-plan/SKILL.md +7 -3
  218. package/harness/bundles/java/cursor/harness-review/SKILL.md +4 -4
  219. package/harness/bundles/java/cursor/harness-run/SKILL.md +7 -5
  220. package/harness/bundles/java/cursor/harness-run/checklist.md +1 -1
  221. package/harness/bundles/java/cursor/harness-run/reference.md +2 -2
  222. package/harness/bundles/java/cursor/harness-submit/SKILL.md +6 -6
  223. package/harness/bundles/java/cursor/harness-submit/checklist.md +1 -1
  224. package/harness/bundles/java/cursor/harness-sync/SKILL.md +1 -1
  225. package/harness/bundles/java/cursor/harness-test/SKILL.md +6 -6
  226. package/harness/bundles/java/cursor/harness-test/checklist.md +1 -1
  227. package/harness/bundles/java/cursor/harness-test/reference.md +1 -1
  228. package/harness/bundles/java/cursor/protocols/ledger-protocol.md +3 -3
  229. package/harness/bundles/java/cursor/scripts/harness_archive.py +276 -67
  230. package/harness/bundles/java/cursor/scripts/harness_context.py +165 -41
  231. package/harness/bundles/java/cursor/scripts/harness_events.py +46 -4
  232. package/harness/bundles/java/cursor/scripts/harness_events_sync.py +109 -3
  233. package/harness/bundles/java/cursor/scripts/harness_fixback.py +189 -1
  234. package/harness/bundles/java/cursor/scripts/harness_gate.py +188 -2
  235. package/harness/bundles/java/cursor/scripts/harness_ledger.py +193 -11
  236. package/harness/bundles/java/cursor/scripts/harness_plan_finalize.py +80 -0
  237. package/harness/manifests/general/claude-code.json +30 -30
  238. package/harness/manifests/general/codebuddy.json +30 -30
  239. package/harness/manifests/general/codex.json +30 -30
  240. package/harness/manifests/general/cursor.json +30 -30
  241. package/harness/manifests/java/claude-code.json +33 -33
  242. package/harness/manifests/java/codebuddy.json +33 -33
  243. package/harness/manifests/java/codex.json +33 -33
  244. package/harness/manifests/java/cursor.json +33 -33
  245. package/hunter-workflow-family.json +5 -5
  246. package/package.json +1 -1
@@ -160,6 +160,39 @@ def infer_change_name(dir_path: Path) -> str:
160
160
  return name
161
161
 
162
162
 
163
+ def resolve_archive_state_root(change_dir: Path) -> Path:
164
+ """Resolve the dynamic state view shared by status, certify and finalize."""
165
+ change_dir = change_dir.resolve()
166
+ return hp.resolve_state_dir_for_contract(
167
+ change_dir,
168
+ find_project_root(change_dir),
169
+ ).resolve()
170
+
171
+
172
+ def archive_read_roots(change_dir: Path) -> list[Path]:
173
+ """Return dynamic state first and the immutable contract as fallback."""
174
+ contract = change_dir.resolve()
175
+ state = resolve_archive_state_root(contract)
176
+ return [state] if state == contract else [state, contract]
177
+
178
+
179
+ def _first_archive_path(change_dir: Path, relative: str) -> Path | None:
180
+ for root in archive_read_roots(change_dir):
181
+ candidate = root / relative
182
+ if candidate.is_file():
183
+ return candidate
184
+ return None
185
+
186
+
187
+ def _archive_path_label(change_dir: Path, path: Path) -> str:
188
+ for root in archive_read_roots(change_dir):
189
+ try:
190
+ return path.resolve().relative_to(root).as_posix()
191
+ except ValueError:
192
+ continue
193
+ return path.name
194
+
195
+
163
196
  def load_template() -> dict[str, Any]:
164
197
  if SUMMARY_TEMPLATE.is_file():
165
198
  data = read_json(SUMMARY_TEMPLATE)
@@ -218,6 +251,8 @@ def generate_manifest(root: Path, output_path: Path) -> dict[str, Any]:
218
251
  for path in sorted(root.rglob("*")):
219
252
  if not path.is_file():
220
253
  continue
254
+ if path.name.endswith(".lock"):
255
+ continue
221
256
  if exclude is not None and path.resolve() == exclude:
222
257
  continue
223
258
  rel = path.relative_to(root).as_posix()
@@ -314,12 +349,14 @@ def compare_manifests(
314
349
 
315
350
 
316
351
  def load_ledger(change_dir: Path) -> dict[str, Any] | None:
317
- for rel in (
318
- "evidence/verification-ledger.json",
319
- "verification-ledger.json",
320
- ):
321
- path = change_dir / rel
322
- if path.is_file():
352
+ for root in archive_read_roots(change_dir):
353
+ for rel in (
354
+ "evidence/verification-ledger.json",
355
+ "verification-ledger.json",
356
+ ):
357
+ path = root / rel
358
+ if not path.is_file():
359
+ continue
323
360
  try:
324
361
  data = read_json(path)
325
362
  return data if isinstance(data, dict) else None
@@ -330,14 +367,15 @@ def load_ledger(change_dir: Path) -> dict[str, Any] | None:
330
367
 
331
368
  def load_ci_metrics(change_dir: Path) -> tuple[dict[str, Any] | None, str | None]:
332
369
  """Load schema-versioned runner metrics without parsing human CI logs."""
333
- for relative in ("evidence/ci-metrics.json", "runtime/ci-metrics.json"):
334
- path = change_dir / relative
335
- if not path.is_file():
336
- continue
337
- value = read_json(path)
338
- if value.get("schemaVersion") != 1:
339
- raise ValueError(f"unsupported ci-metrics schema: {path}")
340
- return value, relative
370
+ for root in archive_read_roots(change_dir):
371
+ for relative in ("evidence/ci-metrics.json", "runtime/ci-metrics.json"):
372
+ path = root / relative
373
+ if not path.is_file():
374
+ continue
375
+ value = read_json(path)
376
+ if value.get("schemaVersion") != 1:
377
+ raise ValueError(f"unsupported ci-metrics schema: {path}")
378
+ return value, relative
341
379
  return None, None
342
380
 
343
381
 
@@ -451,8 +489,8 @@ def load_product_candidate_ci(change_dir: Path) -> dict[str, Any] | None:
451
489
  "meta/product-candidate-verification.json",
452
490
  "runtime/product-candidate-verification.json",
453
491
  ):
454
- path = change_dir / relative
455
- if not path.is_file():
492
+ path = _first_archive_path(change_dir, relative)
493
+ if path is None:
456
494
  continue
457
495
  try:
458
496
  data = read_json(path)
@@ -857,14 +895,21 @@ def _candidate_value_hash(value: Any) -> str:
857
895
  def _candidate_evidence_hash(change_dir: Path, value: Any) -> str:
858
896
  if isinstance(value, str) and value.strip():
859
897
  raw = Path(value.strip())
860
- candidate = raw if raw.is_absolute() else change_dir / raw
861
- try:
862
- resolved = candidate.resolve()
863
- resolved.relative_to(change_dir.resolve())
864
- except (OSError, ValueError):
865
- resolved = None
866
- if resolved is not None and resolved.is_file():
867
- return f"sha256:{sha256_file(resolved)}"
898
+ candidates = [raw] if raw.is_absolute() else [
899
+ root / raw for root in archive_read_roots(change_dir)
900
+ ]
901
+ for candidate in candidates:
902
+ try:
903
+ resolved = candidate.resolve()
904
+ if not any(
905
+ resolved.is_relative_to(root.resolve())
906
+ for root in archive_read_roots(change_dir)
907
+ ):
908
+ continue
909
+ except OSError:
910
+ continue
911
+ if resolved.is_file():
912
+ return f"sha256:{sha256_file(resolved)}"
868
913
  return _candidate_value_hash(value)
869
914
 
870
915
 
@@ -980,8 +1025,6 @@ def certify_local_candidate(
980
1025
  "command",
981
1026
  "evidence",
982
1027
  "inputsHash",
983
- "toolchainHash",
984
- "environmentHash",
985
1028
  ):
986
1029
  if entry.get(field) in (None, "", [], {}):
987
1030
  missing.append(field)
@@ -994,25 +1037,24 @@ def certify_local_candidate(
994
1037
  "name": name,
995
1038
  "command": entry.get("command"),
996
1039
  "inputsHash": entry.get("inputsHash"),
997
- "toolchainHash": entry.get("toolchainHash"),
998
- "environmentHash": entry.get("environmentHash"),
1040
+ "toolchainHash": (
1041
+ entry.get("toolchainHash")
1042
+ or hl.default_toolchain_hash(project_root)
1043
+ ),
1044
+ "environmentHash": (
1045
+ entry.get("environmentHash")
1046
+ or hl.default_environment_hash(project_root)
1047
+ ),
1048
+ "inputsFiles": list(entry.get("inputsFiles") or []),
999
1049
  "logHash": _candidate_evidence_hash(
1000
1050
  change_dir, entry.get("evidence")
1001
1051
  ),
1002
1052
  }
1003
1053
  )
1004
1054
 
1005
- ledger_path = next(
1006
- (
1007
- change_dir / relative
1008
- for relative in (
1009
- "evidence/verification-ledger.json",
1010
- "verification-ledger.json",
1011
- )
1012
- if (change_dir / relative).is_file()
1013
- ),
1014
- None,
1015
- )
1055
+ ledger_path = _first_archive_path(
1056
+ change_dir, "evidence/verification-ledger.json"
1057
+ ) or _first_archive_path(change_dir, "verification-ledger.json")
1016
1058
  if ledger_path is None:
1017
1059
  raise ValueError("verification ledger path missing")
1018
1060
 
@@ -1023,15 +1065,29 @@ def certify_local_candidate(
1023
1065
  or ""
1024
1066
  ).strip()
1025
1067
  rc, current_head, _stderr = git_run(project_root, "rev-parse", "HEAD")
1068
+ rebound_from_commit: str | None = None
1026
1069
  if rc == 0 and current_head:
1027
1070
  if product_commit and not (
1028
1071
  current_head.startswith(product_commit)
1029
1072
  or product_commit.startswith(current_head)
1030
1073
  ):
1031
- raise ValueError(
1032
- "product commit is stale: "
1033
- f"ledger={product_commit} current={current_head}"
1034
- )
1074
+ for item in selected:
1075
+ input_files = [str(path) for path in item.get("inputsFiles") or []]
1076
+ if not input_files:
1077
+ raise ValueError(
1078
+ "product commit is stale and validation inputs are unavailable: "
1079
+ f"ledger={product_commit} current={current_head}"
1080
+ )
1081
+ current_inputs_hash, _paths = hl.compute_inputs_hash(
1082
+ hl.resolve_input_files(input_files, project_root),
1083
+ project_root=project_root,
1084
+ )
1085
+ if current_inputs_hash != item.get("inputsHash"):
1086
+ raise ValueError(
1087
+ "product commit is stale and validated inputs changed: "
1088
+ f"ledger={product_commit} current={current_head}"
1089
+ )
1090
+ rebound_from_commit = product_commit
1035
1091
  product_commit = current_head
1036
1092
  if not product_commit:
1037
1093
  raise ValueError("product commit unavailable")
@@ -1045,7 +1101,7 @@ def certify_local_candidate(
1045
1101
  )
1046
1102
  current_tree_hash = f"sha256:{detail['hash']}"
1047
1103
  recorded_tree_hash = str(ledger.get("productTreeHash") or "").strip()
1048
- if recorded_tree_hash and recorded_tree_hash.removeprefix(
1104
+ if recorded_tree_hash and rebound_from_commit is None and recorded_tree_hash.removeprefix(
1049
1105
  "sha256:"
1050
1106
  ) != current_tree_hash.removeprefix("sha256:"):
1051
1107
  raise ValueError(
@@ -1090,8 +1146,13 @@ def certify_local_candidate(
1090
1146
  "logHashes": sorted({str(item["logHash"]) for item in selected}),
1091
1147
  },
1092
1148
  }
1149
+ if rebound_from_commit is not None:
1150
+ receipt["subject"]["reboundFromCommit"] = rebound_from_commit
1151
+ receipt["verification"]["commitReboundWithoutRerun"] = True
1093
1152
  write_json(
1094
- change_dir / "evidence" / "product-candidate-verification.json",
1153
+ resolve_archive_state_root(change_dir)
1154
+ / "evidence"
1155
+ / "product-candidate-verification.json",
1095
1156
  receipt,
1096
1157
  )
1097
1158
  return receipt
@@ -1811,9 +1872,11 @@ def verify_manifest_byte_coverage(
1811
1872
  }
1812
1873
 
1813
1874
  def load_execution_log(change_dir: Path) -> str:
1814
- for rel in ("logs/execution-log.md", "execution-log.md"):
1815
- path = change_dir / rel
1816
- if path.is_file():
1875
+ for root in archive_read_roots(change_dir):
1876
+ for rel in ("logs/execution-log.md", "execution-log.md"):
1877
+ path = root / rel
1878
+ if not path.is_file():
1879
+ continue
1817
1880
  try:
1818
1881
  return path.read_text(encoding="utf-8-sig")
1819
1882
  except OSError:
@@ -1822,12 +1885,14 @@ def load_execution_log(change_dir: Path) -> str:
1822
1885
 
1823
1886
 
1824
1887
  def load_existing_summary(change_dir: Path) -> dict[str, Any] | None:
1825
- for rel in (
1826
- "reports/final/summary-data.json",
1827
- "summary-data.json",
1828
- ):
1829
- path = change_dir / rel
1830
- if path.is_file():
1888
+ for root in archive_read_roots(change_dir):
1889
+ for rel in (
1890
+ "reports/final/summary-data.json",
1891
+ "summary-data.json",
1892
+ ):
1893
+ path = root / rel
1894
+ if not path.is_file():
1895
+ continue
1831
1896
  try:
1832
1897
  data = read_json(path)
1833
1898
  return data if isinstance(data, dict) else None
@@ -2074,15 +2139,36 @@ def check_status(
2074
2139
  *,
2075
2140
  allow_missing_review: bool = False,
2076
2141
  archive_intent: str = "release-candidate",
2142
+ closure_disposition: str = "completed",
2143
+ closure_reason: str = "",
2077
2144
  ) -> dict[str, Any]:
2078
2145
  """Read-only archive preconditions. Never mutates."""
2079
2146
  if archive_intent not in {"release-candidate", "record-only"}:
2080
2147
  raise ValueError(
2081
2148
  "archive_intent must be release-candidate or record-only"
2082
2149
  )
2150
+ if closure_disposition not in {"completed", "abandoned", "superseded"}:
2151
+ raise ValueError(
2152
+ "closure_disposition must be completed, abandoned, or superseded"
2153
+ )
2154
+ closure_reason = closure_reason.strip()
2083
2155
  blockers: list[dict[str, str]] = []
2084
2156
  warnings: list[dict[str, str]] = []
2085
2157
  checks: dict[str, Any] = {}
2158
+ if closure_disposition != "completed" and not closure_reason:
2159
+ blockers.append(
2160
+ {
2161
+ "code": "closure-reason-required",
2162
+ "message": "ending an unfinished change requires a closure reason",
2163
+ }
2164
+ )
2165
+ if closure_disposition != "completed" and archive_intent == "release-candidate":
2166
+ blockers.append(
2167
+ {
2168
+ "code": "closure-not-release-candidate",
2169
+ "message": "abandoned or superseded changes can be archived but cannot be release candidates",
2170
+ }
2171
+ )
2086
2172
 
2087
2173
  if not change_dir.is_dir():
2088
2174
  return {
@@ -2097,6 +2183,8 @@ def check_status(
2097
2183
 
2098
2184
  project = find_project_root(change_dir)
2099
2185
  checks["project_root"] = str(project)
2186
+ state_root = resolve_archive_state_root(change_dir)
2187
+ checks["state_root"] = str(state_root)
2100
2188
 
2101
2189
  exact_byte = check_archive_exact_byte_policy(project)
2102
2190
  checks["archive_exact_byte"] = exact_byte
@@ -2138,7 +2226,7 @@ def check_status(
2138
2226
  }
2139
2227
  )
2140
2228
 
2141
- events_path = change_dir / "events.ndjson"
2229
+ events_path = state_root / "events.ndjson"
2142
2230
  events_present = events_path.is_file() and events_path.stat().st_size > 0
2143
2231
  checks["events_ndjson"] = events_present
2144
2232
  if not events_present:
@@ -2152,10 +2240,19 @@ def check_status(
2152
2240
  ledger = load_ledger(change_dir)
2153
2241
  checks["verification_ledger"] = ledger is not None
2154
2242
  if ledger is None:
2155
- blockers.append(
2243
+ target = warnings if closure_disposition != "completed" else blockers
2244
+ target.append(
2156
2245
  {
2157
- "code": "missing-verification-ledger",
2158
- "message": "evidence/verification-ledger.json is required before archive",
2246
+ "code": (
2247
+ "closure-ledger-not-required"
2248
+ if closure_disposition != "completed"
2249
+ else "missing-verification-ledger"
2250
+ ),
2251
+ "message": (
2252
+ "verification was not completed; the closure records it as NOT_RUN"
2253
+ if closure_disposition != "completed"
2254
+ else "evidence/verification-ledger.json is required before archive"
2255
+ ),
2159
2256
  }
2160
2257
  )
2161
2258
 
@@ -2425,6 +2522,8 @@ def check_status(
2425
2522
  change_dir, write=False, for_replay=False
2426
2523
  )
2427
2524
  release_summary["archiveIntent"] = archive_intent
2525
+ release_summary["closureDisposition"] = closure_disposition
2526
+ release_summary["closureReason"] = closure_reason
2428
2527
  report_adequacy = validate_report_adequacy(release_summary)
2429
2528
  release_decision = evaluate_release_eligibility(
2430
2529
  change_dir,
@@ -2448,12 +2547,19 @@ def check_status(
2448
2547
  "ok": True,
2449
2548
  "archivable": archivable,
2450
2549
  "archiveIntent": archive_intent,
2550
+ "releaseIntent": "candidate" if archive_intent == "release-candidate" else "none",
2551
+ "closureDisposition": closure_disposition,
2552
+ "closureReason": closure_reason or None,
2451
2553
  "archiveIntegrity": archive_integrity,
2452
2554
  "candidateVerification": release_decision["checks"][
2453
2555
  "candidateVerification"
2454
2556
  ],
2455
2557
  "releaseDecision": release_decision,
2456
- "releaseEligible": release_decision["releaseEligible"],
2558
+ "releaseEligible": (
2559
+ release_decision["releaseEligible"]
2560
+ if closure_disposition == "completed"
2561
+ else False
2562
+ ),
2457
2563
  "change_dir": str(change_dir),
2458
2564
  "change_name": infer_change_name(change_dir),
2459
2565
  "blockers": blockers,
@@ -2484,7 +2590,10 @@ def archive_auto_gate(
2484
2590
  "nextAction": "Resolve archive status blockers, then run `harness_archive.py auto-gate` again.",
2485
2591
  }
2486
2592
 
2487
- snapshot_path = change_dir / "meta" / "state-snapshot.json"
2593
+ state_root = resolve_archive_state_root(change_dir)
2594
+ snapshot_path = state_root / "meta" / "state-snapshot.json"
2595
+ if not snapshot_path.is_file():
2596
+ snapshot_path = change_dir / "meta" / "state-snapshot.json"
2488
2597
  try:
2489
2598
  snapshot = read_json(snapshot_path)
2490
2599
  except (OSError, json.JSONDecodeError, TypeError):
@@ -2496,7 +2605,7 @@ def archive_auto_gate(
2496
2605
  "autoArchiveAllowed": False,
2497
2606
  "reasonCode": "ARCHIVE_BOUNDARY_SNAPSHOT_MISSING",
2498
2607
  "status": status,
2499
- "nextAction": "Capture the post-submit/merge archive-boundary state snapshot before auto-archiving.",
2608
+ "nextAction": "Capture the archive-boundary state snapshot before auto-archiving.",
2500
2609
  }
2501
2610
 
2502
2611
  git_state = snapshot.get("git")
@@ -2505,33 +2614,65 @@ def archive_auto_gate(
2505
2614
  if isinstance(git_state, dict)
2506
2615
  else snapshot.get("base") or snapshot.get("baseCommit")
2507
2616
  )
2508
- if not isinstance(snapshot_base, str) or not snapshot_base.strip():
2617
+ content_state = snapshot.get("content")
2618
+ content_hash = (
2619
+ content_state.get("productTreeHash")
2620
+ if isinstance(content_state, dict)
2621
+ else snapshot.get("productTreeHash")
2622
+ )
2623
+ content_boundary_valid = (
2624
+ isinstance(content_hash, str)
2625
+ and re.fullmatch(r"sha256:[0-9a-f]{64}", content_hash.strip()) is not None
2626
+ )
2627
+ git_boundary_valid = isinstance(snapshot_base, str) and bool(snapshot_base.strip())
2628
+ if not git_boundary_valid and not content_boundary_valid:
2509
2629
  return {
2510
2630
  "ok": False,
2511
2631
  "action": "auto-gate",
2512
2632
  "autoArchiveAllowed": False,
2513
2633
  "reasonCode": "ARCHIVE_BOUNDARY_SNAPSHOT_INVALID",
2514
2634
  "status": status,
2515
- "nextAction": "Capture a state snapshot containing the archive-boundary base commit after Submit or Merge.",
2635
+ "nextAction": (
2636
+ "Capture a state snapshot containing either a Git boundary or "
2637
+ "a deterministic productTreeHash content boundary."
2638
+ ),
2516
2639
  }
2517
2640
 
2518
- events_path = change_dir / "events.ndjson"
2641
+ events_path = state_root / "events.ndjson"
2519
2642
  events = he.load_events(events_path) if events_path.is_file() else []
2643
+ policy = load_gate_policy(change_dir) or {}
2644
+ raw_planned = policy.get("plannedPhases")
2645
+ if not isinstance(raw_planned, list) or not raw_planned:
2646
+ raw_planned = policy.get("defaultPhases")
2647
+ planned_phases = (
2648
+ [str(item).strip() for item in raw_planned if str(item).strip()]
2649
+ if isinstance(raw_planned, list)
2650
+ else []
2651
+ )
2652
+ completed_phase: str | None = None
2653
+ if "archive" in planned_phases:
2654
+ archive_index = planned_phases.index("archive")
2655
+ if archive_index > 0:
2656
+ completed_phase = planned_phases[archive_index - 1]
2657
+ expected_phases = {completed_phase} if completed_phase else {"submit", "merge"}
2520
2658
  completed = any(
2521
- event.get("phase") in {"submit", "merge"}
2659
+ event.get("phase") in expected_phases
2522
2660
  and event.get("type") == "phase.end"
2523
2661
  and str(event.get("status") or "").upper() in {"OK", "WARN"}
2524
2662
  for event in events
2525
2663
  if isinstance(event, dict)
2526
2664
  )
2527
2665
  if not completed:
2666
+ phase_label = completed_phase or "Submit or Merge"
2528
2667
  return {
2529
2668
  "ok": False,
2530
2669
  "action": "auto-gate",
2531
2670
  "autoArchiveAllowed": False,
2532
- "reasonCode": "SUBMIT_OR_MERGE_NOT_COMPLETED",
2671
+ "reasonCode": "PLANNED_PREREQUISITE_NOT_COMPLETED",
2533
2672
  "status": status,
2534
- "nextAction": "Complete Submit or Merge and record its terminal phase event before auto-archiving.",
2673
+ "plannedPhases": planned_phases or None,
2674
+ "completedPrerequisitePhase": completed_phase,
2675
+ "nextAction": f"Complete {phase_label} and record its terminal phase event before auto-archiving.",
2535
2676
  }
2536
2677
 
2537
2678
  return {
@@ -2539,6 +2680,8 @@ def archive_auto_gate(
2539
2680
  "action": "auto-gate",
2540
2681
  "autoArchiveAllowed": True,
2541
2682
  "reasonCode": "ARCHIVE_AUTO_GATE_SATISFIED",
2683
+ "plannedPhases": planned_phases or None,
2684
+ "completedPrerequisitePhase": completed_phase,
2542
2685
  "status": status,
2543
2686
  "snapshotPath": str(snapshot_path),
2544
2687
  "snapshotBase": snapshot_base,
@@ -4026,6 +4169,9 @@ def collect_summary_data(
4026
4169
  "knownRisks",
4027
4170
  "manualActions",
4028
4171
  "archiveIntent",
4172
+ "releaseIntent",
4173
+ "closureDisposition",
4174
+ "closureReason",
4029
4175
  "archiveIntegrity",
4030
4176
  "candidateVerification",
4031
4177
  "releaseDecision",
@@ -4164,6 +4310,19 @@ def collect_summary_data(
4164
4310
  data["archiveIntent"] = str(
4165
4311
  intent_data.get("intent") or "release-candidate"
4166
4312
  )
4313
+ data["releaseIntent"] = str(
4314
+ intent_data.get("releaseIntent")
4315
+ or ("candidate" if data["archiveIntent"] == "release-candidate" else "none")
4316
+ )
4317
+ data["closureDisposition"] = str(
4318
+ intent_data.get("closureDisposition") or "completed"
4319
+ )
4320
+ raw_closure_reason = intent_data.get("closureReason")
4321
+ data["closureReason"] = (
4322
+ str(raw_closure_reason).strip()
4323
+ if isinstance(raw_closure_reason, str) and raw_closure_reason.strip()
4324
+ else None
4325
+ )
4167
4326
  data["archiveIntegrity"] = {"ok": True}
4168
4327
  data["candidateVerification"] = candidate_gate
4169
4328
 
@@ -5199,6 +5358,9 @@ def _freeze_evidence_cutoff(work_dir: Path) -> dict[str, Any]:
5199
5358
  "path": "events.ndjson",
5200
5359
  }
5201
5360
  write_json(work_dir / "evidence" / "evidence-cutoff.json", cutoff)
5361
+ # The staged archive is immutable after this point. The event lock is a
5362
+ # runtime coordination file, not evidence, and must not survive publish.
5363
+ events_file.with_name(events_file.name + ".lock").unlink(missing_ok=True)
5202
5364
  return cutoff
5203
5365
 
5204
5366
 
@@ -7302,6 +7464,8 @@ def cmd_finalize(
7302
7464
  skip_ingest: bool = False,
7303
7465
  allow_missing_review: bool = False,
7304
7466
  archive_intent: str = "release-candidate",
7467
+ closure_disposition: str = "completed",
7468
+ closure_reason: str = "",
7305
7469
  ) -> tuple[int, dict[str, Any]]:
7306
7470
  """Execute the 9-step finalize pipeline. Returns (exit_code, payload)."""
7307
7471
  if archive_intent not in {"release-candidate", "record-only"}:
@@ -7310,6 +7474,25 @@ def cmd_finalize(
7310
7474
  "action": "finalize",
7311
7475
  "error": "archive_intent must be release-candidate or record-only",
7312
7476
  }
7477
+ if closure_disposition not in {"completed", "abandoned", "superseded"}:
7478
+ return 1, {
7479
+ "ok": False,
7480
+ "action": "finalize",
7481
+ "error": "invalid closure disposition",
7482
+ }
7483
+ closure_reason = closure_reason.strip()
7484
+ if closure_disposition != "completed" and not closure_reason:
7485
+ return 1, {
7486
+ "ok": False,
7487
+ "action": "finalize",
7488
+ "error": "closure reason is required for abandoned or superseded changes",
7489
+ }
7490
+ if closure_disposition != "completed" and archive_intent == "release-candidate":
7491
+ return 1, {
7492
+ "ok": False,
7493
+ "action": "finalize",
7494
+ "error": "abandoned or superseded changes cannot be release candidates",
7495
+ }
7313
7496
  warnings: list[str] = []
7314
7497
  original_change_dir = change_dir.resolve()
7315
7498
  change_name = original_change_dir.name
@@ -7396,6 +7579,9 @@ def cmd_finalize(
7396
7579
  "operationTempDir": str(operation_temp_dir),
7397
7580
  "operationRecord": str(operation_record),
7398
7581
  "archiveIntent": archive_intent,
7582
+ "releaseIntent": "candidate" if archive_intent == "release-candidate" else "none",
7583
+ "closureDisposition": closure_disposition,
7584
+ "closureReason": closure_reason or None,
7399
7585
  "releaseEligible": False,
7400
7586
  "warnings": warnings,
7401
7587
  "steps": {},
@@ -7787,6 +7973,11 @@ def cmd_finalize(
7787
7973
  {
7788
7974
  "schemaVersion": 1,
7789
7975
  "intent": archive_intent,
7976
+ "releaseIntent": (
7977
+ "candidate" if archive_intent == "release-candidate" else "none"
7978
+ ),
7979
+ "closureDisposition": closure_disposition,
7980
+ "closureReason": closure_reason or None,
7790
7981
  "candidateVerificationCode": ci_gate.get("code"),
7791
7982
  "releaseEligible": False,
7792
7983
  "releaseDecisionCode": "PENDING_IDENTITY_AND_REPORT_GATES",
@@ -9219,6 +9410,8 @@ def cmd_status_cli(args: argparse.Namespace) -> int:
9219
9410
  change_dir,
9220
9411
  allow_missing_review=bool(getattr(args, "allow_missing_review", False)),
9221
9412
  archive_intent=str(getattr(args, "intent", "release-candidate")),
9413
+ closure_disposition=str(getattr(args, "closure", "completed")),
9414
+ closure_reason=str(getattr(args, "closure_reason", "")),
9222
9415
  )
9223
9416
  emit_json(result)
9224
9417
  # Checks completed → exit 0; archivable flag conveys the gate result.
@@ -9289,6 +9482,8 @@ def cmd_finalize_cli(args: argparse.Namespace) -> int:
9289
9482
  skip_ingest=bool(args.skip_ingest),
9290
9483
  allow_missing_review=bool(getattr(args, "allow_missing_review", False)),
9291
9484
  archive_intent=str(getattr(args, "intent", "release-candidate")),
9485
+ closure_disposition=str(getattr(args, "closure", "completed")),
9486
+ closure_reason=str(getattr(args, "closure_reason", "")),
9292
9487
  )
9293
9488
  emit_json(payload)
9294
9489
  return code
@@ -9497,6 +9692,13 @@ def build_parser() -> argparse.ArgumentParser:
9497
9692
  action="store_true",
9498
9693
  help="downgrade full-tier missing review from blocker to warning",
9499
9694
  )
9695
+ p_status.add_argument(
9696
+ "--closure",
9697
+ choices=("completed", "abandoned", "superseded"),
9698
+ default="completed",
9699
+ help="lifecycle outcome, independent from release intent",
9700
+ )
9701
+ p_status.add_argument("--closure-reason", default="")
9500
9702
  p_status.add_argument(
9501
9703
  "--intent",
9502
9704
  choices=("release-candidate", "record-only"),
@@ -9543,6 +9745,13 @@ def build_parser() -> argparse.ArgumentParser:
9543
9745
  "the project and is verified before source deletion"
9544
9746
  ),
9545
9747
  )
9748
+ p_fin.add_argument(
9749
+ "--closure",
9750
+ choices=("completed", "abandoned", "superseded"),
9751
+ default="completed",
9752
+ help="lifecycle outcome, independent from release intent",
9753
+ )
9754
+ p_fin.add_argument("--closure-reason", default="")
9546
9755
  p_fin.add_argument(
9547
9756
  "--retention-policy",
9548
9757
  default="unspecified",