@hunter-harness/workflow-harness 0.2.54 → 0.2.56

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 (262) 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-plan/checklist.md +2 -1
  11. package/harness/bundles/general/claude-code/harness-review/SKILL.md +8 -4
  12. package/harness/bundles/general/claude-code/harness-run/SKILL.md +9 -7
  13. package/harness/bundles/general/claude-code/harness-run/checklist.md +1 -1
  14. package/harness/bundles/general/claude-code/harness-run/reference.md +3 -3
  15. package/harness/bundles/general/claude-code/harness-submit/SKILL.md +6 -6
  16. package/harness/bundles/general/claude-code/harness-submit/checklist.md +1 -1
  17. package/harness/bundles/general/claude-code/harness-sync/SKILL.md +1 -1
  18. package/harness/bundles/general/claude-code/harness-test/SKILL.md +8 -6
  19. package/harness/bundles/general/claude-code/harness-test/checklist.md +1 -1
  20. package/harness/bundles/general/claude-code/harness-test/reference.md +2 -2
  21. package/harness/bundles/general/claude-code/protocols/ledger-protocol.md +3 -3
  22. package/harness/bundles/general/claude-code/scripts/harness_archive.py +322 -74
  23. package/harness/bundles/general/claude-code/scripts/harness_context.py +236 -63
  24. package/harness/bundles/general/claude-code/scripts/harness_events.py +86 -5
  25. package/harness/bundles/general/claude-code/scripts/harness_events_sync.py +201 -17
  26. package/harness/bundles/general/claude-code/scripts/harness_fixback.py +189 -1
  27. package/harness/bundles/general/claude-code/scripts/harness_gate.py +353 -3
  28. package/harness/bundles/general/claude-code/scripts/harness_ledger.py +195 -11
  29. package/harness/bundles/general/claude-code/scripts/harness_plan_finalize.py +82 -0
  30. package/harness/bundles/general/claude-code/scripts/harness_test_guard.py +37 -44
  31. package/harness/bundles/general/codebuddy/.harness-build.json +1 -1
  32. package/harness/bundles/general/codebuddy/README.md +3 -3
  33. package/harness/bundles/general/codebuddy/contracts/workflow-policy.json +1 -1
  34. package/harness/bundles/general/codebuddy/harness-archive/SKILL.md +21 -33
  35. package/harness/bundles/general/codebuddy/harness-archive/checklist.md +6 -10
  36. package/harness/bundles/general/codebuddy/harness-codebase-map/SKILL.md +1 -1
  37. package/harness/bundles/general/codebuddy/harness-knowledge-ingest/SKILL.md +1 -1
  38. package/harness/bundles/general/codebuddy/harness-knowledge-query/SKILL.md +1 -1
  39. package/harness/bundles/general/codebuddy/harness-plan/SKILL.md +7 -3
  40. package/harness/bundles/general/codebuddy/harness-plan/checklist.md +2 -1
  41. package/harness/bundles/general/codebuddy/harness-review/SKILL.md +8 -4
  42. package/harness/bundles/general/codebuddy/harness-run/SKILL.md +9 -7
  43. package/harness/bundles/general/codebuddy/harness-run/checklist.md +1 -1
  44. package/harness/bundles/general/codebuddy/harness-run/reference.md +3 -3
  45. package/harness/bundles/general/codebuddy/harness-submit/SKILL.md +6 -6
  46. package/harness/bundles/general/codebuddy/harness-submit/checklist.md +1 -1
  47. package/harness/bundles/general/codebuddy/harness-sync/SKILL.md +1 -1
  48. package/harness/bundles/general/codebuddy/harness-test/SKILL.md +8 -6
  49. package/harness/bundles/general/codebuddy/harness-test/checklist.md +1 -1
  50. package/harness/bundles/general/codebuddy/harness-test/reference.md +2 -2
  51. package/harness/bundles/general/codebuddy/protocols/ledger-protocol.md +3 -3
  52. package/harness/bundles/general/codebuddy/scripts/harness_archive.py +322 -74
  53. package/harness/bundles/general/codebuddy/scripts/harness_context.py +236 -63
  54. package/harness/bundles/general/codebuddy/scripts/harness_events.py +86 -5
  55. package/harness/bundles/general/codebuddy/scripts/harness_events_sync.py +201 -17
  56. package/harness/bundles/general/codebuddy/scripts/harness_fixback.py +189 -1
  57. package/harness/bundles/general/codebuddy/scripts/harness_gate.py +353 -3
  58. package/harness/bundles/general/codebuddy/scripts/harness_ledger.py +195 -11
  59. package/harness/bundles/general/codebuddy/scripts/harness_plan_finalize.py +82 -0
  60. package/harness/bundles/general/codebuddy/scripts/harness_test_guard.py +37 -44
  61. package/harness/bundles/general/codex/.harness-build.json +1 -1
  62. package/harness/bundles/general/codex/README.md +3 -3
  63. package/harness/bundles/general/codex/contracts/workflow-policy.json +1 -1
  64. package/harness/bundles/general/codex/harness-archive/SKILL.md +21 -33
  65. package/harness/bundles/general/codex/harness-archive/checklist.md +6 -10
  66. package/harness/bundles/general/codex/harness-codebase-map/SKILL.md +1 -1
  67. package/harness/bundles/general/codex/harness-knowledge-ingest/SKILL.md +1 -1
  68. package/harness/bundles/general/codex/harness-knowledge-query/SKILL.md +1 -1
  69. package/harness/bundles/general/codex/harness-plan/SKILL.md +7 -3
  70. package/harness/bundles/general/codex/harness-plan/checklist.md +2 -1
  71. package/harness/bundles/general/codex/harness-review/SKILL.md +4 -4
  72. package/harness/bundles/general/codex/harness-run/SKILL.md +9 -7
  73. package/harness/bundles/general/codex/harness-run/checklist.md +1 -1
  74. package/harness/bundles/general/codex/harness-run/reference.md +3 -3
  75. package/harness/bundles/general/codex/harness-submit/SKILL.md +6 -6
  76. package/harness/bundles/general/codex/harness-submit/checklist.md +1 -1
  77. package/harness/bundles/general/codex/harness-sync/SKILL.md +1 -1
  78. package/harness/bundles/general/codex/harness-test/SKILL.md +8 -6
  79. package/harness/bundles/general/codex/harness-test/checklist.md +1 -1
  80. package/harness/bundles/general/codex/harness-test/reference.md +2 -2
  81. package/harness/bundles/general/codex/protocols/ledger-protocol.md +3 -3
  82. package/harness/bundles/general/codex/scripts/harness_archive.py +322 -74
  83. package/harness/bundles/general/codex/scripts/harness_context.py +236 -63
  84. package/harness/bundles/general/codex/scripts/harness_events.py +86 -5
  85. package/harness/bundles/general/codex/scripts/harness_events_sync.py +201 -17
  86. package/harness/bundles/general/codex/scripts/harness_fixback.py +189 -1
  87. package/harness/bundles/general/codex/scripts/harness_gate.py +353 -3
  88. package/harness/bundles/general/codex/scripts/harness_ledger.py +195 -11
  89. package/harness/bundles/general/codex/scripts/harness_plan_finalize.py +82 -0
  90. package/harness/bundles/general/codex/scripts/harness_test_guard.py +37 -44
  91. package/harness/bundles/general/cursor/.harness-build.json +1 -1
  92. package/harness/bundles/general/cursor/README.md +3 -3
  93. package/harness/bundles/general/cursor/contracts/workflow-policy.json +1 -1
  94. package/harness/bundles/general/cursor/harness-archive/SKILL.md +21 -33
  95. package/harness/bundles/general/cursor/harness-archive/checklist.md +6 -10
  96. package/harness/bundles/general/cursor/harness-codebase-map/SKILL.md +1 -1
  97. package/harness/bundles/general/cursor/harness-knowledge-ingest/SKILL.md +1 -1
  98. package/harness/bundles/general/cursor/harness-knowledge-query/SKILL.md +1 -1
  99. package/harness/bundles/general/cursor/harness-plan/SKILL.md +7 -3
  100. package/harness/bundles/general/cursor/harness-plan/checklist.md +2 -1
  101. package/harness/bundles/general/cursor/harness-review/SKILL.md +4 -4
  102. package/harness/bundles/general/cursor/harness-run/SKILL.md +9 -7
  103. package/harness/bundles/general/cursor/harness-run/checklist.md +1 -1
  104. package/harness/bundles/general/cursor/harness-run/reference.md +3 -3
  105. package/harness/bundles/general/cursor/harness-submit/SKILL.md +6 -6
  106. package/harness/bundles/general/cursor/harness-submit/checklist.md +1 -1
  107. package/harness/bundles/general/cursor/harness-sync/SKILL.md +1 -1
  108. package/harness/bundles/general/cursor/harness-test/SKILL.md +8 -6
  109. package/harness/bundles/general/cursor/harness-test/checklist.md +1 -1
  110. package/harness/bundles/general/cursor/harness-test/reference.md +2 -2
  111. package/harness/bundles/general/cursor/protocols/ledger-protocol.md +3 -3
  112. package/harness/bundles/general/cursor/scripts/harness_archive.py +322 -74
  113. package/harness/bundles/general/cursor/scripts/harness_context.py +236 -63
  114. package/harness/bundles/general/cursor/scripts/harness_events.py +86 -5
  115. package/harness/bundles/general/cursor/scripts/harness_events_sync.py +201 -17
  116. package/harness/bundles/general/cursor/scripts/harness_fixback.py +189 -1
  117. package/harness/bundles/general/cursor/scripts/harness_gate.py +353 -3
  118. package/harness/bundles/general/cursor/scripts/harness_ledger.py +195 -11
  119. package/harness/bundles/general/cursor/scripts/harness_plan_finalize.py +82 -0
  120. package/harness/bundles/general/cursor/scripts/harness_test_guard.py +37 -44
  121. package/harness/bundles/java/claude-code/.harness-build.json +1 -1
  122. package/harness/bundles/java/claude-code/README.md +3 -3
  123. package/harness/bundles/java/claude-code/contracts/workflow-policy.json +1 -1
  124. package/harness/bundles/java/claude-code/harness-apidoc/SKILL.md +1 -1
  125. package/harness/bundles/java/claude-code/harness-archive/SKILL.md +21 -33
  126. package/harness/bundles/java/claude-code/harness-archive/checklist.md +6 -10
  127. package/harness/bundles/java/claude-code/harness-codebase-map/SKILL.md +1 -1
  128. package/harness/bundles/java/claude-code/harness-knowledge-ingest/SKILL.md +1 -1
  129. package/harness/bundles/java/claude-code/harness-knowledge-query/SKILL.md +1 -1
  130. package/harness/bundles/java/claude-code/harness-package/SKILL.md +1 -1
  131. package/harness/bundles/java/claude-code/harness-package/checklist.md +1 -1
  132. package/harness/bundles/java/claude-code/harness-plan/SKILL.md +7 -3
  133. package/harness/bundles/java/claude-code/harness-plan/checklist.md +2 -1
  134. package/harness/bundles/java/claude-code/harness-review/SKILL.md +8 -4
  135. package/harness/bundles/java/claude-code/harness-run/SKILL.md +9 -7
  136. package/harness/bundles/java/claude-code/harness-run/checklist.md +1 -1
  137. package/harness/bundles/java/claude-code/harness-run/reference.md +2 -2
  138. package/harness/bundles/java/claude-code/harness-submit/SKILL.md +6 -6
  139. package/harness/bundles/java/claude-code/harness-submit/checklist.md +1 -1
  140. package/harness/bundles/java/claude-code/harness-sync/SKILL.md +1 -1
  141. package/harness/bundles/java/claude-code/harness-test/SKILL.md +8 -6
  142. package/harness/bundles/java/claude-code/harness-test/checklist.md +1 -1
  143. package/harness/bundles/java/claude-code/harness-test/reference.md +1 -1
  144. package/harness/bundles/java/claude-code/protocols/ledger-protocol.md +3 -3
  145. package/harness/bundles/java/claude-code/scripts/harness_archive.py +322 -74
  146. package/harness/bundles/java/claude-code/scripts/harness_context.py +236 -63
  147. package/harness/bundles/java/claude-code/scripts/harness_events.py +86 -5
  148. package/harness/bundles/java/claude-code/scripts/harness_events_sync.py +201 -17
  149. package/harness/bundles/java/claude-code/scripts/harness_fixback.py +189 -1
  150. package/harness/bundles/java/claude-code/scripts/harness_gate.py +353 -3
  151. package/harness/bundles/java/claude-code/scripts/harness_ledger.py +195 -11
  152. package/harness/bundles/java/claude-code/scripts/harness_plan_finalize.py +82 -0
  153. package/harness/bundles/java/claude-code/scripts/harness_test_guard.py +37 -44
  154. package/harness/bundles/java/codebuddy/.harness-build.json +1 -1
  155. package/harness/bundles/java/codebuddy/README.md +3 -3
  156. package/harness/bundles/java/codebuddy/contracts/workflow-policy.json +1 -1
  157. package/harness/bundles/java/codebuddy/harness-apidoc/SKILL.md +1 -1
  158. package/harness/bundles/java/codebuddy/harness-archive/SKILL.md +21 -33
  159. package/harness/bundles/java/codebuddy/harness-archive/checklist.md +6 -10
  160. package/harness/bundles/java/codebuddy/harness-codebase-map/SKILL.md +1 -1
  161. package/harness/bundles/java/codebuddy/harness-knowledge-ingest/SKILL.md +1 -1
  162. package/harness/bundles/java/codebuddy/harness-knowledge-query/SKILL.md +1 -1
  163. package/harness/bundles/java/codebuddy/harness-package/SKILL.md +1 -1
  164. package/harness/bundles/java/codebuddy/harness-package/checklist.md +1 -1
  165. package/harness/bundles/java/codebuddy/harness-plan/SKILL.md +7 -3
  166. package/harness/bundles/java/codebuddy/harness-plan/checklist.md +2 -1
  167. package/harness/bundles/java/codebuddy/harness-review/SKILL.md +8 -4
  168. package/harness/bundles/java/codebuddy/harness-run/SKILL.md +9 -7
  169. package/harness/bundles/java/codebuddy/harness-run/checklist.md +1 -1
  170. package/harness/bundles/java/codebuddy/harness-run/reference.md +2 -2
  171. package/harness/bundles/java/codebuddy/harness-submit/SKILL.md +6 -6
  172. package/harness/bundles/java/codebuddy/harness-submit/checklist.md +1 -1
  173. package/harness/bundles/java/codebuddy/harness-sync/SKILL.md +1 -1
  174. package/harness/bundles/java/codebuddy/harness-test/SKILL.md +8 -6
  175. package/harness/bundles/java/codebuddy/harness-test/checklist.md +1 -1
  176. package/harness/bundles/java/codebuddy/harness-test/reference.md +1 -1
  177. package/harness/bundles/java/codebuddy/protocols/ledger-protocol.md +3 -3
  178. package/harness/bundles/java/codebuddy/scripts/harness_archive.py +322 -74
  179. package/harness/bundles/java/codebuddy/scripts/harness_context.py +236 -63
  180. package/harness/bundles/java/codebuddy/scripts/harness_events.py +86 -5
  181. package/harness/bundles/java/codebuddy/scripts/harness_events_sync.py +201 -17
  182. package/harness/bundles/java/codebuddy/scripts/harness_fixback.py +189 -1
  183. package/harness/bundles/java/codebuddy/scripts/harness_gate.py +353 -3
  184. package/harness/bundles/java/codebuddy/scripts/harness_ledger.py +195 -11
  185. package/harness/bundles/java/codebuddy/scripts/harness_plan_finalize.py +82 -0
  186. package/harness/bundles/java/codebuddy/scripts/harness_test_guard.py +37 -44
  187. package/harness/bundles/java/codex/.harness-build.json +1 -1
  188. package/harness/bundles/java/codex/README.md +3 -3
  189. package/harness/bundles/java/codex/contracts/workflow-policy.json +1 -1
  190. package/harness/bundles/java/codex/harness-apidoc/SKILL.md +1 -1
  191. package/harness/bundles/java/codex/harness-archive/SKILL.md +21 -33
  192. package/harness/bundles/java/codex/harness-archive/checklist.md +6 -10
  193. package/harness/bundles/java/codex/harness-codebase-map/SKILL.md +1 -1
  194. package/harness/bundles/java/codex/harness-knowledge-ingest/SKILL.md +1 -1
  195. package/harness/bundles/java/codex/harness-knowledge-query/SKILL.md +1 -1
  196. package/harness/bundles/java/codex/harness-package/SKILL.md +1 -1
  197. package/harness/bundles/java/codex/harness-package/checklist.md +1 -1
  198. package/harness/bundles/java/codex/harness-plan/SKILL.md +7 -3
  199. package/harness/bundles/java/codex/harness-plan/checklist.md +2 -1
  200. package/harness/bundles/java/codex/harness-review/SKILL.md +4 -4
  201. package/harness/bundles/java/codex/harness-run/SKILL.md +9 -7
  202. package/harness/bundles/java/codex/harness-run/checklist.md +1 -1
  203. package/harness/bundles/java/codex/harness-run/reference.md +2 -2
  204. package/harness/bundles/java/codex/harness-submit/SKILL.md +6 -6
  205. package/harness/bundles/java/codex/harness-submit/checklist.md +1 -1
  206. package/harness/bundles/java/codex/harness-sync/SKILL.md +1 -1
  207. package/harness/bundles/java/codex/harness-test/SKILL.md +8 -6
  208. package/harness/bundles/java/codex/harness-test/checklist.md +1 -1
  209. package/harness/bundles/java/codex/harness-test/reference.md +1 -1
  210. package/harness/bundles/java/codex/protocols/ledger-protocol.md +3 -3
  211. package/harness/bundles/java/codex/scripts/harness_archive.py +322 -74
  212. package/harness/bundles/java/codex/scripts/harness_context.py +236 -63
  213. package/harness/bundles/java/codex/scripts/harness_events.py +86 -5
  214. package/harness/bundles/java/codex/scripts/harness_events_sync.py +201 -17
  215. package/harness/bundles/java/codex/scripts/harness_fixback.py +189 -1
  216. package/harness/bundles/java/codex/scripts/harness_gate.py +353 -3
  217. package/harness/bundles/java/codex/scripts/harness_ledger.py +195 -11
  218. package/harness/bundles/java/codex/scripts/harness_plan_finalize.py +82 -0
  219. package/harness/bundles/java/codex/scripts/harness_test_guard.py +37 -44
  220. package/harness/bundles/java/cursor/.harness-build.json +1 -1
  221. package/harness/bundles/java/cursor/README.md +3 -3
  222. package/harness/bundles/java/cursor/contracts/workflow-policy.json +1 -1
  223. package/harness/bundles/java/cursor/harness-apidoc/SKILL.md +1 -1
  224. package/harness/bundles/java/cursor/harness-archive/SKILL.md +21 -33
  225. package/harness/bundles/java/cursor/harness-archive/checklist.md +6 -10
  226. package/harness/bundles/java/cursor/harness-codebase-map/SKILL.md +1 -1
  227. package/harness/bundles/java/cursor/harness-knowledge-ingest/SKILL.md +1 -1
  228. package/harness/bundles/java/cursor/harness-knowledge-query/SKILL.md +1 -1
  229. package/harness/bundles/java/cursor/harness-package/SKILL.md +1 -1
  230. package/harness/bundles/java/cursor/harness-package/checklist.md +1 -1
  231. package/harness/bundles/java/cursor/harness-plan/SKILL.md +7 -3
  232. package/harness/bundles/java/cursor/harness-plan/checklist.md +2 -1
  233. package/harness/bundles/java/cursor/harness-review/SKILL.md +4 -4
  234. package/harness/bundles/java/cursor/harness-run/SKILL.md +9 -7
  235. package/harness/bundles/java/cursor/harness-run/checklist.md +1 -1
  236. package/harness/bundles/java/cursor/harness-run/reference.md +2 -2
  237. package/harness/bundles/java/cursor/harness-submit/SKILL.md +6 -6
  238. package/harness/bundles/java/cursor/harness-submit/checklist.md +1 -1
  239. package/harness/bundles/java/cursor/harness-sync/SKILL.md +1 -1
  240. package/harness/bundles/java/cursor/harness-test/SKILL.md +8 -6
  241. package/harness/bundles/java/cursor/harness-test/checklist.md +1 -1
  242. package/harness/bundles/java/cursor/harness-test/reference.md +1 -1
  243. package/harness/bundles/java/cursor/protocols/ledger-protocol.md +3 -3
  244. package/harness/bundles/java/cursor/scripts/harness_archive.py +322 -74
  245. package/harness/bundles/java/cursor/scripts/harness_context.py +236 -63
  246. package/harness/bundles/java/cursor/scripts/harness_events.py +86 -5
  247. package/harness/bundles/java/cursor/scripts/harness_events_sync.py +201 -17
  248. package/harness/bundles/java/cursor/scripts/harness_fixback.py +189 -1
  249. package/harness/bundles/java/cursor/scripts/harness_gate.py +353 -3
  250. package/harness/bundles/java/cursor/scripts/harness_ledger.py +195 -11
  251. package/harness/bundles/java/cursor/scripts/harness_plan_finalize.py +82 -0
  252. package/harness/bundles/java/cursor/scripts/harness_test_guard.py +37 -44
  253. package/harness/manifests/general/claude-code.json +32 -32
  254. package/harness/manifests/general/codebuddy.json +32 -32
  255. package/harness/manifests/general/codex.json +32 -32
  256. package/harness/manifests/general/cursor.json +32 -32
  257. package/harness/manifests/java/claude-code.json +35 -35
  258. package/harness/manifests/java/codebuddy.json +35 -35
  259. package/harness/manifests/java/codex.json +35 -35
  260. package/harness/manifests/java/cursor.json +35 -35
  261. package/hunter-workflow-family.json +5 -4
  262. package/package.json +1 -1
@@ -17,6 +17,7 @@ import argparse
17
17
  import datetime as dt
18
18
  import hashlib
19
19
  import io
20
+ import ipaddress
20
21
  import json
21
22
  import os
22
23
  import re
@@ -31,6 +32,7 @@ import zipfile
31
32
  from collections.abc import Mapping
32
33
  from pathlib import Path
33
34
  from typing import Any
35
+ from urllib.parse import urlsplit
34
36
 
35
37
 
36
38
  if hasattr(sys.stdout, "reconfigure"):
@@ -158,6 +160,39 @@ def infer_change_name(dir_path: Path) -> str:
158
160
  return name
159
161
 
160
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
+
161
196
  def load_template() -> dict[str, Any]:
162
197
  if SUMMARY_TEMPLATE.is_file():
163
198
  data = read_json(SUMMARY_TEMPLATE)
@@ -192,9 +227,9 @@ def append_event(
192
227
  for key, value in fields.items():
193
228
  if value is not None:
194
229
  event[key] = value
195
- line = json.dumps(event, ensure_ascii=False, separators=(",", ":"))
196
- he.atomic_append_line(path, line)
197
- all_events = existing + [he.normalize_event(event)]
230
+ result = he.append_with_auto_seal(path, event, existing_events=existing)
231
+ event = result["event"]
232
+ all_events = he.load_events(path)
198
233
  he.write_execution_log(change_dir, he.render_execution_log(all_events))
199
234
  return event
200
235
 
@@ -216,6 +251,8 @@ def generate_manifest(root: Path, output_path: Path) -> dict[str, Any]:
216
251
  for path in sorted(root.rglob("*")):
217
252
  if not path.is_file():
218
253
  continue
254
+ if path.name.endswith(".lock"):
255
+ continue
219
256
  if exclude is not None and path.resolve() == exclude:
220
257
  continue
221
258
  rel = path.relative_to(root).as_posix()
@@ -312,12 +349,14 @@ def compare_manifests(
312
349
 
313
350
 
314
351
  def load_ledger(change_dir: Path) -> dict[str, Any] | None:
315
- for rel in (
316
- "evidence/verification-ledger.json",
317
- "verification-ledger.json",
318
- ):
319
- path = change_dir / rel
320
- 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
321
360
  try:
322
361
  data = read_json(path)
323
362
  return data if isinstance(data, dict) else None
@@ -328,14 +367,15 @@ def load_ledger(change_dir: Path) -> dict[str, Any] | None:
328
367
 
329
368
  def load_ci_metrics(change_dir: Path) -> tuple[dict[str, Any] | None, str | None]:
330
369
  """Load schema-versioned runner metrics without parsing human CI logs."""
331
- for relative in ("evidence/ci-metrics.json", "runtime/ci-metrics.json"):
332
- path = change_dir / relative
333
- if not path.is_file():
334
- continue
335
- value = read_json(path)
336
- if value.get("schemaVersion") != 1:
337
- raise ValueError(f"unsupported ci-metrics schema: {path}")
338
- 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
339
379
  return None, None
340
380
 
341
381
 
@@ -449,8 +489,8 @@ def load_product_candidate_ci(change_dir: Path) -> dict[str, Any] | None:
449
489
  "meta/product-candidate-verification.json",
450
490
  "runtime/product-candidate-verification.json",
451
491
  ):
452
- path = change_dir / relative
453
- if not path.is_file():
492
+ path = _first_archive_path(change_dir, relative)
493
+ if path is None:
454
494
  continue
455
495
  try:
456
496
  data = read_json(path)
@@ -855,14 +895,21 @@ def _candidate_value_hash(value: Any) -> str:
855
895
  def _candidate_evidence_hash(change_dir: Path, value: Any) -> str:
856
896
  if isinstance(value, str) and value.strip():
857
897
  raw = Path(value.strip())
858
- candidate = raw if raw.is_absolute() else change_dir / raw
859
- try:
860
- resolved = candidate.resolve()
861
- resolved.relative_to(change_dir.resolve())
862
- except (OSError, ValueError):
863
- resolved = None
864
- if resolved is not None and resolved.is_file():
865
- 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)}"
866
913
  return _candidate_value_hash(value)
867
914
 
868
915
 
@@ -978,8 +1025,6 @@ def certify_local_candidate(
978
1025
  "command",
979
1026
  "evidence",
980
1027
  "inputsHash",
981
- "toolchainHash",
982
- "environmentHash",
983
1028
  ):
984
1029
  if entry.get(field) in (None, "", [], {}):
985
1030
  missing.append(field)
@@ -992,25 +1037,24 @@ def certify_local_candidate(
992
1037
  "name": name,
993
1038
  "command": entry.get("command"),
994
1039
  "inputsHash": entry.get("inputsHash"),
995
- "toolchainHash": entry.get("toolchainHash"),
996
- "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 []),
997
1049
  "logHash": _candidate_evidence_hash(
998
1050
  change_dir, entry.get("evidence")
999
1051
  ),
1000
1052
  }
1001
1053
  )
1002
1054
 
1003
- ledger_path = next(
1004
- (
1005
- change_dir / relative
1006
- for relative in (
1007
- "evidence/verification-ledger.json",
1008
- "verification-ledger.json",
1009
- )
1010
- if (change_dir / relative).is_file()
1011
- ),
1012
- None,
1013
- )
1055
+ ledger_path = _first_archive_path(
1056
+ change_dir, "evidence/verification-ledger.json"
1057
+ ) or _first_archive_path(change_dir, "verification-ledger.json")
1014
1058
  if ledger_path is None:
1015
1059
  raise ValueError("verification ledger path missing")
1016
1060
 
@@ -1021,15 +1065,29 @@ def certify_local_candidate(
1021
1065
  or ""
1022
1066
  ).strip()
1023
1067
  rc, current_head, _stderr = git_run(project_root, "rev-parse", "HEAD")
1068
+ rebound_from_commit: str | None = None
1024
1069
  if rc == 0 and current_head:
1025
1070
  if product_commit and not (
1026
1071
  current_head.startswith(product_commit)
1027
1072
  or product_commit.startswith(current_head)
1028
1073
  ):
1029
- raise ValueError(
1030
- "product commit is stale: "
1031
- f"ledger={product_commit} current={current_head}"
1032
- )
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
1033
1091
  product_commit = current_head
1034
1092
  if not product_commit:
1035
1093
  raise ValueError("product commit unavailable")
@@ -1043,7 +1101,7 @@ def certify_local_candidate(
1043
1101
  )
1044
1102
  current_tree_hash = f"sha256:{detail['hash']}"
1045
1103
  recorded_tree_hash = str(ledger.get("productTreeHash") or "").strip()
1046
- 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(
1047
1105
  "sha256:"
1048
1106
  ) != current_tree_hash.removeprefix("sha256:"):
1049
1107
  raise ValueError(
@@ -1088,8 +1146,13 @@ def certify_local_candidate(
1088
1146
  "logHashes": sorted({str(item["logHash"]) for item in selected}),
1089
1147
  },
1090
1148
  }
1149
+ if rebound_from_commit is not None:
1150
+ receipt["subject"]["reboundFromCommit"] = rebound_from_commit
1151
+ receipt["verification"]["commitReboundWithoutRerun"] = True
1091
1152
  write_json(
1092
- change_dir / "evidence" / "product-candidate-verification.json",
1153
+ resolve_archive_state_root(change_dir)
1154
+ / "evidence"
1155
+ / "product-candidate-verification.json",
1093
1156
  receipt,
1094
1157
  )
1095
1158
  return receipt
@@ -1809,9 +1872,11 @@ def verify_manifest_byte_coverage(
1809
1872
  }
1810
1873
 
1811
1874
  def load_execution_log(change_dir: Path) -> str:
1812
- for rel in ("logs/execution-log.md", "execution-log.md"):
1813
- path = change_dir / rel
1814
- 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
1815
1880
  try:
1816
1881
  return path.read_text(encoding="utf-8-sig")
1817
1882
  except OSError:
@@ -1820,12 +1885,14 @@ def load_execution_log(change_dir: Path) -> str:
1820
1885
 
1821
1886
 
1822
1887
  def load_existing_summary(change_dir: Path) -> dict[str, Any] | None:
1823
- for rel in (
1824
- "reports/final/summary-data.json",
1825
- "summary-data.json",
1826
- ):
1827
- path = change_dir / rel
1828
- 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
1829
1896
  try:
1830
1897
  data = read_json(path)
1831
1898
  return data if isinstance(data, dict) else None
@@ -2072,15 +2139,36 @@ def check_status(
2072
2139
  *,
2073
2140
  allow_missing_review: bool = False,
2074
2141
  archive_intent: str = "release-candidate",
2142
+ closure_disposition: str = "completed",
2143
+ closure_reason: str = "",
2075
2144
  ) -> dict[str, Any]:
2076
2145
  """Read-only archive preconditions. Never mutates."""
2077
2146
  if archive_intent not in {"release-candidate", "record-only"}:
2078
2147
  raise ValueError(
2079
2148
  "archive_intent must be release-candidate or record-only"
2080
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()
2081
2155
  blockers: list[dict[str, str]] = []
2082
2156
  warnings: list[dict[str, str]] = []
2083
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
+ )
2084
2172
 
2085
2173
  if not change_dir.is_dir():
2086
2174
  return {
@@ -2095,6 +2183,8 @@ def check_status(
2095
2183
 
2096
2184
  project = find_project_root(change_dir)
2097
2185
  checks["project_root"] = str(project)
2186
+ state_root = resolve_archive_state_root(change_dir)
2187
+ checks["state_root"] = str(state_root)
2098
2188
 
2099
2189
  exact_byte = check_archive_exact_byte_policy(project)
2100
2190
  checks["archive_exact_byte"] = exact_byte
@@ -2136,7 +2226,7 @@ def check_status(
2136
2226
  }
2137
2227
  )
2138
2228
 
2139
- events_path = change_dir / "events.ndjson"
2229
+ events_path = state_root / "events.ndjson"
2140
2230
  events_present = events_path.is_file() and events_path.stat().st_size > 0
2141
2231
  checks["events_ndjson"] = events_present
2142
2232
  if not events_present:
@@ -2150,10 +2240,19 @@ def check_status(
2150
2240
  ledger = load_ledger(change_dir)
2151
2241
  checks["verification_ledger"] = ledger is not None
2152
2242
  if ledger is None:
2153
- blockers.append(
2243
+ target = warnings if closure_disposition != "completed" else blockers
2244
+ target.append(
2154
2245
  {
2155
- "code": "missing-verification-ledger",
2156
- "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
+ ),
2157
2256
  }
2158
2257
  )
2159
2258
 
@@ -2423,6 +2522,8 @@ def check_status(
2423
2522
  change_dir, write=False, for_replay=False
2424
2523
  )
2425
2524
  release_summary["archiveIntent"] = archive_intent
2525
+ release_summary["closureDisposition"] = closure_disposition
2526
+ release_summary["closureReason"] = closure_reason
2426
2527
  report_adequacy = validate_report_adequacy(release_summary)
2427
2528
  release_decision = evaluate_release_eligibility(
2428
2529
  change_dir,
@@ -2446,12 +2547,19 @@ def check_status(
2446
2547
  "ok": True,
2447
2548
  "archivable": archivable,
2448
2549
  "archiveIntent": archive_intent,
2550
+ "releaseIntent": "candidate" if archive_intent == "release-candidate" else "none",
2551
+ "closureDisposition": closure_disposition,
2552
+ "closureReason": closure_reason or None,
2449
2553
  "archiveIntegrity": archive_integrity,
2450
2554
  "candidateVerification": release_decision["checks"][
2451
2555
  "candidateVerification"
2452
2556
  ],
2453
2557
  "releaseDecision": release_decision,
2454
- "releaseEligible": release_decision["releaseEligible"],
2558
+ "releaseEligible": (
2559
+ release_decision["releaseEligible"]
2560
+ if closure_disposition == "completed"
2561
+ else False
2562
+ ),
2455
2563
  "change_dir": str(change_dir),
2456
2564
  "change_name": infer_change_name(change_dir),
2457
2565
  "blockers": blockers,
@@ -2482,7 +2590,10 @@ def archive_auto_gate(
2482
2590
  "nextAction": "Resolve archive status blockers, then run `harness_archive.py auto-gate` again.",
2483
2591
  }
2484
2592
 
2485
- 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"
2486
2597
  try:
2487
2598
  snapshot = read_json(snapshot_path)
2488
2599
  except (OSError, json.JSONDecodeError, TypeError):
@@ -2494,7 +2605,7 @@ def archive_auto_gate(
2494
2605
  "autoArchiveAllowed": False,
2495
2606
  "reasonCode": "ARCHIVE_BOUNDARY_SNAPSHOT_MISSING",
2496
2607
  "status": status,
2497
- "nextAction": "Capture the post-submit/merge archive-boundary state snapshot before auto-archiving.",
2608
+ "nextAction": "Capture the archive-boundary state snapshot before auto-archiving.",
2498
2609
  }
2499
2610
 
2500
2611
  git_state = snapshot.get("git")
@@ -2503,33 +2614,65 @@ def archive_auto_gate(
2503
2614
  if isinstance(git_state, dict)
2504
2615
  else snapshot.get("base") or snapshot.get("baseCommit")
2505
2616
  )
2506
- 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:
2507
2629
  return {
2508
2630
  "ok": False,
2509
2631
  "action": "auto-gate",
2510
2632
  "autoArchiveAllowed": False,
2511
2633
  "reasonCode": "ARCHIVE_BOUNDARY_SNAPSHOT_INVALID",
2512
2634
  "status": status,
2513
- "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
+ ),
2514
2639
  }
2515
2640
 
2516
- events_path = change_dir / "events.ndjson"
2641
+ events_path = state_root / "events.ndjson"
2517
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"}
2518
2658
  completed = any(
2519
- event.get("phase") in {"submit", "merge"}
2659
+ event.get("phase") in expected_phases
2520
2660
  and event.get("type") == "phase.end"
2521
2661
  and str(event.get("status") or "").upper() in {"OK", "WARN"}
2522
2662
  for event in events
2523
2663
  if isinstance(event, dict)
2524
2664
  )
2525
2665
  if not completed:
2666
+ phase_label = completed_phase or "Submit or Merge"
2526
2667
  return {
2527
2668
  "ok": False,
2528
2669
  "action": "auto-gate",
2529
2670
  "autoArchiveAllowed": False,
2530
- "reasonCode": "SUBMIT_OR_MERGE_NOT_COMPLETED",
2671
+ "reasonCode": "PLANNED_PREREQUISITE_NOT_COMPLETED",
2531
2672
  "status": status,
2532
- "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.",
2533
2676
  }
2534
2677
 
2535
2678
  return {
@@ -2537,6 +2680,8 @@ def archive_auto_gate(
2537
2680
  "action": "auto-gate",
2538
2681
  "autoArchiveAllowed": True,
2539
2682
  "reasonCode": "ARCHIVE_AUTO_GATE_SATISFIED",
2683
+ "plannedPhases": planned_phases or None,
2684
+ "completedPrerequisitePhase": completed_phase,
2540
2685
  "status": status,
2541
2686
  "snapshotPath": str(snapshot_path),
2542
2687
  "snapshotBase": snapshot_base,
@@ -3053,6 +3198,25 @@ def build_verification_projection(
3053
3198
  "apiTests": _ledger_api_tests(effective_ledger, change_dir=change_dir),
3054
3199
  }
3055
3200
  db_compatibility = _ledger_db_compat(effective_ledger)
3201
+ if db_compatibility["status"] == "NOT_RUN" and change_dir is not None:
3202
+ gate_policy_path = change_dir / "meta" / "gate-policy.json"
3203
+ try:
3204
+ gate_policy = read_json(gate_policy_path) if gate_policy_path.is_file() else None
3205
+ except (OSError, json.JSONDecodeError):
3206
+ gate_policy = None
3207
+ if isinstance(gate_policy, dict):
3208
+ capabilities = gate_policy.get("capabilities")
3209
+ required = gate_policy.get("requiredValidations")
3210
+ if (
3211
+ isinstance(capabilities, list)
3212
+ and "database" not in capabilities
3213
+ and (not isinstance(required, list) or "dbCompatibility" not in required)
3214
+ ):
3215
+ db_compatibility = {
3216
+ "status": "NOT_APPLICABLE",
3217
+ "reason": "项目能力画像未声明数据库能力",
3218
+ "source": "capability-profile",
3219
+ }
3056
3220
  projection["dbCompatibility"] = db_compatibility["status"]
3057
3221
  projection["dbCompatibilityEvidence"] = db_compatibility
3058
3222
  api_contract = validations.get("apiContract")
@@ -4005,6 +4169,9 @@ def collect_summary_data(
4005
4169
  "knownRisks",
4006
4170
  "manualActions",
4007
4171
  "archiveIntent",
4172
+ "releaseIntent",
4173
+ "closureDisposition",
4174
+ "closureReason",
4008
4175
  "archiveIntegrity",
4009
4176
  "candidateVerification",
4010
4177
  "releaseDecision",
@@ -4143,6 +4310,19 @@ def collect_summary_data(
4143
4310
  data["archiveIntent"] = str(
4144
4311
  intent_data.get("intent") or "release-candidate"
4145
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
+ )
4146
4326
  data["archiveIntegrity"] = {"ok": True}
4147
4327
  data["candidateVerification"] = candidate_gate
4148
4328
 
@@ -5178,6 +5358,9 @@ def _freeze_evidence_cutoff(work_dir: Path) -> dict[str, Any]:
5178
5358
  "path": "events.ndjson",
5179
5359
  }
5180
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)
5181
5364
  return cutoff
5182
5365
 
5183
5366
 
@@ -7281,6 +7464,8 @@ def cmd_finalize(
7281
7464
  skip_ingest: bool = False,
7282
7465
  allow_missing_review: bool = False,
7283
7466
  archive_intent: str = "release-candidate",
7467
+ closure_disposition: str = "completed",
7468
+ closure_reason: str = "",
7284
7469
  ) -> tuple[int, dict[str, Any]]:
7285
7470
  """Execute the 9-step finalize pipeline. Returns (exit_code, payload)."""
7286
7471
  if archive_intent not in {"release-candidate", "record-only"}:
@@ -7289,6 +7474,25 @@ def cmd_finalize(
7289
7474
  "action": "finalize",
7290
7475
  "error": "archive_intent must be release-candidate or record-only",
7291
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
+ }
7292
7496
  warnings: list[str] = []
7293
7497
  original_change_dir = change_dir.resolve()
7294
7498
  change_name = original_change_dir.name
@@ -7375,6 +7579,9 @@ def cmd_finalize(
7375
7579
  "operationTempDir": str(operation_temp_dir),
7376
7580
  "operationRecord": str(operation_record),
7377
7581
  "archiveIntent": archive_intent,
7582
+ "releaseIntent": "candidate" if archive_intent == "release-candidate" else "none",
7583
+ "closureDisposition": closure_disposition,
7584
+ "closureReason": closure_reason or None,
7378
7585
  "releaseEligible": False,
7379
7586
  "warnings": warnings,
7380
7587
  "steps": {},
@@ -7766,6 +7973,11 @@ def cmd_finalize(
7766
7973
  {
7767
7974
  "schemaVersion": 1,
7768
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,
7769
7981
  "candidateVerificationCode": ci_gate.get("code"),
7770
7982
  "releaseEligible": False,
7771
7983
  "releaseDecisionCode": "PENDING_IDENTITY_AND_REPORT_GATES",
@@ -8377,6 +8589,27 @@ def _read_optional_text(path: Path) -> str:
8377
8589
  return ""
8378
8590
 
8379
8591
 
8592
+ def _archive_server_url_allowed(value: str | None) -> bool:
8593
+ """Require TLS remotely while allowing explicit loopback HTTP for local testing."""
8594
+ if value is None or not value.strip() or re.search(r"\s", value):
8595
+ return False
8596
+ try:
8597
+ parsed = urlsplit(value)
8598
+ host = (parsed.hostname or "").lower()
8599
+ except ValueError:
8600
+ return False
8601
+ if parsed.scheme.lower() == "https":
8602
+ return bool(host)
8603
+ if parsed.scheme.lower() != "http":
8604
+ return False
8605
+ if host == "localhost":
8606
+ return True
8607
+ try:
8608
+ return ipaddress.ip_address(host).is_loopback
8609
+ except ValueError:
8610
+ return False
8611
+
8612
+
8380
8613
  def _resolve_archive_remote_credentials(
8381
8614
  project_root: Path,
8382
8615
  environment: Mapping[str, str],
@@ -8403,10 +8636,7 @@ def _resolve_archive_remote_credentials(
8403
8636
  else ""
8404
8637
  )
8405
8638
  token_available = bool(env_token or local_token)
8406
- url_available = bool(
8407
- server_url
8408
- and re.fullmatch(r"https://[^\s]+", server_url, flags=re.IGNORECASE)
8409
- )
8639
+ url_available = _archive_server_url_allowed(server_url)
8410
8640
  return {
8411
8641
  "configured": bool(url_available and token_available),
8412
8642
  "serverUrl": server_url if url_available else None,
@@ -9180,6 +9410,8 @@ def cmd_status_cli(args: argparse.Namespace) -> int:
9180
9410
  change_dir,
9181
9411
  allow_missing_review=bool(getattr(args, "allow_missing_review", False)),
9182
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", "")),
9183
9415
  )
9184
9416
  emit_json(result)
9185
9417
  # Checks completed → exit 0; archivable flag conveys the gate result.
@@ -9250,6 +9482,8 @@ def cmd_finalize_cli(args: argparse.Namespace) -> int:
9250
9482
  skip_ingest=bool(args.skip_ingest),
9251
9483
  allow_missing_review=bool(getattr(args, "allow_missing_review", False)),
9252
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", "")),
9253
9487
  )
9254
9488
  emit_json(payload)
9255
9489
  return code
@@ -9458,6 +9692,13 @@ def build_parser() -> argparse.ArgumentParser:
9458
9692
  action="store_true",
9459
9693
  help="downgrade full-tier missing review from blocker to warning",
9460
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="")
9461
9702
  p_status.add_argument(
9462
9703
  "--intent",
9463
9704
  choices=("release-candidate", "record-only"),
@@ -9504,6 +9745,13 @@ def build_parser() -> argparse.ArgumentParser:
9504
9745
  "the project and is verified before source deletion"
9505
9746
  ),
9506
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="")
9507
9755
  p_fin.add_argument(
9508
9756
  "--retention-policy",
9509
9757
  default="unspecified",