@hunter-harness/workflow-harness 0.2.40 → 0.2.42

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/harness-archive/SKILL.md +1 -1
  3. package/harness/bundles/general/claude-code/harness-archive/checklist.md +3 -0
  4. package/harness/bundles/general/claude-code/harness-archive/templates/render-summary.mjs +22 -0
  5. package/harness/bundles/general/claude-code/harness-codebase-map/SKILL.md +1 -1
  6. package/harness/bundles/general/claude-code/harness-knowledge-ingest/SKILL.md +42 -16
  7. package/harness/bundles/general/claude-code/harness-knowledge-ingest/reference.md +57 -6
  8. package/harness/bundles/general/claude-code/harness-knowledge-ingest/scripts/harness_knowledge.py +1097 -174
  9. package/harness/bundles/general/claude-code/harness-knowledge-query/SKILL.md +1 -1
  10. package/harness/bundles/general/claude-code/harness-plan/SKILL.md +1 -1
  11. package/harness/bundles/general/claude-code/harness-plan/reference.md +16 -16
  12. package/harness/bundles/general/claude-code/harness-review/SKILL.md +1 -1
  13. package/harness/bundles/general/claude-code/harness-run/SKILL.md +1 -1
  14. package/harness/bundles/general/claude-code/harness-run/reference.md +1 -1
  15. package/harness/bundles/general/claude-code/harness-submit/SKILL.md +1 -1
  16. package/harness/bundles/general/claude-code/harness-sync/SKILL.md +16 -10
  17. package/harness/bundles/general/claude-code/harness-sync/reference.md +34 -14
  18. package/harness/bundles/general/claude-code/harness-test/SKILL.md +3 -1
  19. package/harness/bundles/general/claude-code/protocols/archive-report-protocol.md +10 -0
  20. package/harness/bundles/general/claude-code/protocols/ledger-protocol.md +12 -0
  21. package/harness/bundles/general/claude-code/protocols/powershell-protocol.md +21 -0
  22. package/harness/bundles/general/claude-code/protocols/state-layout-protocol.md +11 -0
  23. package/harness/bundles/general/claude-code/scripts/harness_archive.py +413 -2
  24. package/harness/bundles/general/claude-code/scripts/harness_environment.py +556 -39
  25. package/harness/bundles/general/claude-code/scripts/harness_gate.py +145 -0
  26. package/harness/bundles/general/claude-code/scripts/harness_ledger.py +581 -55
  27. package/harness/bundles/general/claude-code/scripts/harness_plan_finalize.py +85 -6
  28. package/harness/bundles/general/claude-code/scripts/harness_service.py +291 -22
  29. package/harness/bundles/general/claude-code/scripts/harness_test_runner.py +405 -25
  30. package/harness/bundles/general/codebuddy/.harness-build.json +1 -1
  31. package/harness/bundles/general/codebuddy/harness-archive/SKILL.md +1 -1
  32. package/harness/bundles/general/codebuddy/harness-archive/checklist.md +3 -0
  33. package/harness/bundles/general/codebuddy/harness-archive/templates/render-summary.mjs +22 -0
  34. package/harness/bundles/general/codebuddy/harness-codebase-map/SKILL.md +1 -1
  35. package/harness/bundles/general/codebuddy/harness-knowledge-ingest/SKILL.md +41 -15
  36. package/harness/bundles/general/codebuddy/harness-knowledge-ingest/reference.md +57 -6
  37. package/harness/bundles/general/codebuddy/harness-knowledge-ingest/scripts/harness_knowledge.py +1097 -174
  38. package/harness/bundles/general/codebuddy/harness-knowledge-query/SKILL.md +1 -1
  39. package/harness/bundles/general/codebuddy/harness-plan/SKILL.md +1 -1
  40. package/harness/bundles/general/codebuddy/harness-plan/reference.md +16 -16
  41. package/harness/bundles/general/codebuddy/harness-review/SKILL.md +1 -1
  42. package/harness/bundles/general/codebuddy/harness-run/SKILL.md +1 -1
  43. package/harness/bundles/general/codebuddy/harness-run/reference.md +1 -1
  44. package/harness/bundles/general/codebuddy/harness-submit/SKILL.md +1 -1
  45. package/harness/bundles/general/codebuddy/harness-sync/SKILL.md +16 -10
  46. package/harness/bundles/general/codebuddy/harness-sync/reference.md +34 -14
  47. package/harness/bundles/general/codebuddy/harness-test/SKILL.md +3 -1
  48. package/harness/bundles/general/codebuddy/protocols/archive-report-protocol.md +10 -0
  49. package/harness/bundles/general/codebuddy/protocols/ledger-protocol.md +12 -0
  50. package/harness/bundles/general/codebuddy/protocols/powershell-protocol.md +21 -0
  51. package/harness/bundles/general/codebuddy/protocols/state-layout-protocol.md +11 -0
  52. package/harness/bundles/general/codebuddy/scripts/harness_archive.py +413 -2
  53. package/harness/bundles/general/codebuddy/scripts/harness_environment.py +556 -39
  54. package/harness/bundles/general/codebuddy/scripts/harness_gate.py +145 -0
  55. package/harness/bundles/general/codebuddy/scripts/harness_ledger.py +581 -55
  56. package/harness/bundles/general/codebuddy/scripts/harness_plan_finalize.py +85 -6
  57. package/harness/bundles/general/codebuddy/scripts/harness_service.py +291 -22
  58. package/harness/bundles/general/codebuddy/scripts/harness_test_runner.py +405 -25
  59. package/harness/bundles/general/codex/.harness-build.json +1 -1
  60. package/harness/bundles/general/codex/harness-archive/SKILL.md +1 -1
  61. package/harness/bundles/general/codex/harness-archive/checklist.md +3 -0
  62. package/harness/bundles/general/codex/harness-archive/templates/render-summary.mjs +22 -0
  63. package/harness/bundles/general/codex/harness-codebase-map/SKILL.md +1 -1
  64. package/harness/bundles/general/codex/harness-knowledge-ingest/SKILL.md +41 -15
  65. package/harness/bundles/general/codex/harness-knowledge-ingest/reference.md +57 -6
  66. package/harness/bundles/general/codex/harness-knowledge-ingest/scripts/harness_knowledge.py +1097 -174
  67. package/harness/bundles/general/codex/harness-knowledge-query/SKILL.md +1 -1
  68. package/harness/bundles/general/codex/harness-plan/SKILL.md +1 -1
  69. package/harness/bundles/general/codex/harness-plan/reference.md +16 -16
  70. package/harness/bundles/general/codex/harness-review/SKILL.md +1 -1
  71. package/harness/bundles/general/codex/harness-run/SKILL.md +1 -1
  72. package/harness/bundles/general/codex/harness-run/reference.md +1 -1
  73. package/harness/bundles/general/codex/harness-submit/SKILL.md +1 -1
  74. package/harness/bundles/general/codex/harness-sync/SKILL.md +16 -10
  75. package/harness/bundles/general/codex/harness-sync/reference.md +34 -14
  76. package/harness/bundles/general/codex/harness-test/SKILL.md +3 -1
  77. package/harness/bundles/general/codex/protocols/archive-report-protocol.md +10 -0
  78. package/harness/bundles/general/codex/protocols/ledger-protocol.md +12 -0
  79. package/harness/bundles/general/codex/protocols/powershell-protocol.md +21 -0
  80. package/harness/bundles/general/codex/protocols/state-layout-protocol.md +11 -0
  81. package/harness/bundles/general/codex/scripts/harness_archive.py +413 -2
  82. package/harness/bundles/general/codex/scripts/harness_environment.py +556 -39
  83. package/harness/bundles/general/codex/scripts/harness_gate.py +145 -0
  84. package/harness/bundles/general/codex/scripts/harness_ledger.py +581 -55
  85. package/harness/bundles/general/codex/scripts/harness_plan_finalize.py +85 -6
  86. package/harness/bundles/general/codex/scripts/harness_service.py +291 -22
  87. package/harness/bundles/general/codex/scripts/harness_test_runner.py +405 -25
  88. package/harness/bundles/general/cursor/.harness-build.json +1 -1
  89. package/harness/bundles/general/cursor/harness-archive/SKILL.md +1 -1
  90. package/harness/bundles/general/cursor/harness-archive/checklist.md +3 -0
  91. package/harness/bundles/general/cursor/harness-archive/templates/render-summary.mjs +22 -0
  92. package/harness/bundles/general/cursor/harness-codebase-map/SKILL.md +1 -1
  93. package/harness/bundles/general/cursor/harness-knowledge-ingest/SKILL.md +41 -15
  94. package/harness/bundles/general/cursor/harness-knowledge-ingest/reference.md +57 -6
  95. package/harness/bundles/general/cursor/harness-knowledge-ingest/scripts/harness_knowledge.py +1097 -174
  96. package/harness/bundles/general/cursor/harness-knowledge-query/SKILL.md +1 -1
  97. package/harness/bundles/general/cursor/harness-plan/SKILL.md +1 -1
  98. package/harness/bundles/general/cursor/harness-plan/reference.md +16 -16
  99. package/harness/bundles/general/cursor/harness-review/SKILL.md +1 -1
  100. package/harness/bundles/general/cursor/harness-run/SKILL.md +1 -1
  101. package/harness/bundles/general/cursor/harness-run/reference.md +1 -1
  102. package/harness/bundles/general/cursor/harness-submit/SKILL.md +1 -1
  103. package/harness/bundles/general/cursor/harness-sync/SKILL.md +16 -10
  104. package/harness/bundles/general/cursor/harness-sync/reference.md +34 -14
  105. package/harness/bundles/general/cursor/harness-test/SKILL.md +3 -1
  106. package/harness/bundles/general/cursor/protocols/archive-report-protocol.md +10 -0
  107. package/harness/bundles/general/cursor/protocols/ledger-protocol.md +12 -0
  108. package/harness/bundles/general/cursor/protocols/powershell-protocol.md +21 -0
  109. package/harness/bundles/general/cursor/protocols/state-layout-protocol.md +11 -0
  110. package/harness/bundles/general/cursor/scripts/harness_archive.py +413 -2
  111. package/harness/bundles/general/cursor/scripts/harness_environment.py +556 -39
  112. package/harness/bundles/general/cursor/scripts/harness_gate.py +145 -0
  113. package/harness/bundles/general/cursor/scripts/harness_ledger.py +581 -55
  114. package/harness/bundles/general/cursor/scripts/harness_plan_finalize.py +85 -6
  115. package/harness/bundles/general/cursor/scripts/harness_service.py +291 -22
  116. package/harness/bundles/general/cursor/scripts/harness_test_runner.py +405 -25
  117. package/harness/bundles/java/claude-code/.harness-build.json +1 -1
  118. package/harness/bundles/java/claude-code/harness-apidoc/SKILL.md +1 -1
  119. package/harness/bundles/java/claude-code/harness-archive/SKILL.md +1 -1
  120. package/harness/bundles/java/claude-code/harness-archive/checklist.md +3 -0
  121. package/harness/bundles/java/claude-code/harness-archive/templates/render-summary.mjs +22 -0
  122. package/harness/bundles/java/claude-code/harness-codebase-map/SKILL.md +1 -1
  123. package/harness/bundles/java/claude-code/harness-knowledge-ingest/SKILL.md +42 -16
  124. package/harness/bundles/java/claude-code/harness-knowledge-ingest/reference.md +57 -6
  125. package/harness/bundles/java/claude-code/harness-knowledge-ingest/scripts/harness_knowledge.py +1097 -174
  126. package/harness/bundles/java/claude-code/harness-knowledge-query/SKILL.md +1 -1
  127. package/harness/bundles/java/claude-code/harness-package/SKILL.md +1 -1
  128. package/harness/bundles/java/claude-code/harness-plan/SKILL.md +1 -1
  129. package/harness/bundles/java/claude-code/harness-plan/reference.md +16 -16
  130. package/harness/bundles/java/claude-code/harness-review/SKILL.md +1 -1
  131. package/harness/bundles/java/claude-code/harness-run/SKILL.md +1 -1
  132. package/harness/bundles/java/claude-code/harness-submit/SKILL.md +1 -1
  133. package/harness/bundles/java/claude-code/harness-sync/SKILL.md +16 -10
  134. package/harness/bundles/java/claude-code/harness-sync/reference.md +34 -14
  135. package/harness/bundles/java/claude-code/harness-test/SKILL.md +3 -1
  136. package/harness/bundles/java/claude-code/protocols/archive-report-protocol.md +10 -0
  137. package/harness/bundles/java/claude-code/protocols/ledger-protocol.md +12 -0
  138. package/harness/bundles/java/claude-code/protocols/powershell-protocol.md +21 -0
  139. package/harness/bundles/java/claude-code/protocols/state-layout-protocol.md +11 -0
  140. package/harness/bundles/java/claude-code/scripts/harness_archive.py +413 -2
  141. package/harness/bundles/java/claude-code/scripts/harness_environment.py +556 -39
  142. package/harness/bundles/java/claude-code/scripts/harness_gate.py +145 -0
  143. package/harness/bundles/java/claude-code/scripts/harness_ledger.py +581 -55
  144. package/harness/bundles/java/claude-code/scripts/harness_plan_finalize.py +85 -6
  145. package/harness/bundles/java/claude-code/scripts/harness_service.py +291 -22
  146. package/harness/bundles/java/claude-code/scripts/harness_test_runner.py +405 -25
  147. package/harness/bundles/java/codebuddy/.harness-build.json +1 -1
  148. package/harness/bundles/java/codebuddy/harness-apidoc/SKILL.md +1 -1
  149. package/harness/bundles/java/codebuddy/harness-archive/SKILL.md +1 -1
  150. package/harness/bundles/java/codebuddy/harness-archive/checklist.md +3 -0
  151. package/harness/bundles/java/codebuddy/harness-archive/templates/render-summary.mjs +22 -0
  152. package/harness/bundles/java/codebuddy/harness-codebase-map/SKILL.md +1 -1
  153. package/harness/bundles/java/codebuddy/harness-knowledge-ingest/SKILL.md +41 -15
  154. package/harness/bundles/java/codebuddy/harness-knowledge-ingest/reference.md +57 -6
  155. package/harness/bundles/java/codebuddy/harness-knowledge-ingest/scripts/harness_knowledge.py +1097 -174
  156. package/harness/bundles/java/codebuddy/harness-knowledge-query/SKILL.md +1 -1
  157. package/harness/bundles/java/codebuddy/harness-package/SKILL.md +1 -1
  158. package/harness/bundles/java/codebuddy/harness-plan/SKILL.md +1 -1
  159. package/harness/bundles/java/codebuddy/harness-plan/reference.md +16 -16
  160. package/harness/bundles/java/codebuddy/harness-review/SKILL.md +1 -1
  161. package/harness/bundles/java/codebuddy/harness-run/SKILL.md +1 -1
  162. package/harness/bundles/java/codebuddy/harness-submit/SKILL.md +1 -1
  163. package/harness/bundles/java/codebuddy/harness-sync/SKILL.md +16 -10
  164. package/harness/bundles/java/codebuddy/harness-sync/reference.md +34 -14
  165. package/harness/bundles/java/codebuddy/harness-test/SKILL.md +3 -1
  166. package/harness/bundles/java/codebuddy/protocols/archive-report-protocol.md +10 -0
  167. package/harness/bundles/java/codebuddy/protocols/ledger-protocol.md +12 -0
  168. package/harness/bundles/java/codebuddy/protocols/powershell-protocol.md +21 -0
  169. package/harness/bundles/java/codebuddy/protocols/state-layout-protocol.md +11 -0
  170. package/harness/bundles/java/codebuddy/scripts/harness_archive.py +413 -2
  171. package/harness/bundles/java/codebuddy/scripts/harness_environment.py +556 -39
  172. package/harness/bundles/java/codebuddy/scripts/harness_gate.py +145 -0
  173. package/harness/bundles/java/codebuddy/scripts/harness_ledger.py +581 -55
  174. package/harness/bundles/java/codebuddy/scripts/harness_plan_finalize.py +85 -6
  175. package/harness/bundles/java/codebuddy/scripts/harness_service.py +291 -22
  176. package/harness/bundles/java/codebuddy/scripts/harness_test_runner.py +405 -25
  177. package/harness/bundles/java/codex/.harness-build.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 +1 -1
  180. package/harness/bundles/java/codex/harness-archive/checklist.md +3 -0
  181. package/harness/bundles/java/codex/harness-archive/templates/render-summary.mjs +22 -0
  182. package/harness/bundles/java/codex/harness-codebase-map/SKILL.md +1 -1
  183. package/harness/bundles/java/codex/harness-knowledge-ingest/SKILL.md +41 -15
  184. package/harness/bundles/java/codex/harness-knowledge-ingest/reference.md +57 -6
  185. package/harness/bundles/java/codex/harness-knowledge-ingest/scripts/harness_knowledge.py +1097 -174
  186. package/harness/bundles/java/codex/harness-knowledge-query/SKILL.md +1 -1
  187. package/harness/bundles/java/codex/harness-package/SKILL.md +1 -1
  188. package/harness/bundles/java/codex/harness-plan/SKILL.md +1 -1
  189. package/harness/bundles/java/codex/harness-plan/reference.md +16 -16
  190. package/harness/bundles/java/codex/harness-review/SKILL.md +1 -1
  191. package/harness/bundles/java/codex/harness-run/SKILL.md +1 -1
  192. package/harness/bundles/java/codex/harness-submit/SKILL.md +1 -1
  193. package/harness/bundles/java/codex/harness-sync/SKILL.md +16 -10
  194. package/harness/bundles/java/codex/harness-sync/reference.md +34 -14
  195. package/harness/bundles/java/codex/harness-test/SKILL.md +3 -1
  196. package/harness/bundles/java/codex/protocols/archive-report-protocol.md +10 -0
  197. package/harness/bundles/java/codex/protocols/ledger-protocol.md +12 -0
  198. package/harness/bundles/java/codex/protocols/powershell-protocol.md +21 -0
  199. package/harness/bundles/java/codex/protocols/state-layout-protocol.md +11 -0
  200. package/harness/bundles/java/codex/scripts/harness_archive.py +413 -2
  201. package/harness/bundles/java/codex/scripts/harness_environment.py +556 -39
  202. package/harness/bundles/java/codex/scripts/harness_gate.py +145 -0
  203. package/harness/bundles/java/codex/scripts/harness_ledger.py +581 -55
  204. package/harness/bundles/java/codex/scripts/harness_plan_finalize.py +85 -6
  205. package/harness/bundles/java/codex/scripts/harness_service.py +291 -22
  206. package/harness/bundles/java/codex/scripts/harness_test_runner.py +405 -25
  207. package/harness/bundles/java/cursor/.harness-build.json +1 -1
  208. package/harness/bundles/java/cursor/harness-apidoc/SKILL.md +1 -1
  209. package/harness/bundles/java/cursor/harness-archive/SKILL.md +1 -1
  210. package/harness/bundles/java/cursor/harness-archive/checklist.md +3 -0
  211. package/harness/bundles/java/cursor/harness-archive/templates/render-summary.mjs +22 -0
  212. package/harness/bundles/java/cursor/harness-codebase-map/SKILL.md +1 -1
  213. package/harness/bundles/java/cursor/harness-knowledge-ingest/SKILL.md +41 -15
  214. package/harness/bundles/java/cursor/harness-knowledge-ingest/reference.md +57 -6
  215. package/harness/bundles/java/cursor/harness-knowledge-ingest/scripts/harness_knowledge.py +1097 -174
  216. package/harness/bundles/java/cursor/harness-knowledge-query/SKILL.md +1 -1
  217. package/harness/bundles/java/cursor/harness-package/SKILL.md +1 -1
  218. package/harness/bundles/java/cursor/harness-plan/SKILL.md +1 -1
  219. package/harness/bundles/java/cursor/harness-plan/reference.md +16 -16
  220. package/harness/bundles/java/cursor/harness-review/SKILL.md +1 -1
  221. package/harness/bundles/java/cursor/harness-run/SKILL.md +1 -1
  222. package/harness/bundles/java/cursor/harness-submit/SKILL.md +1 -1
  223. package/harness/bundles/java/cursor/harness-sync/SKILL.md +16 -10
  224. package/harness/bundles/java/cursor/harness-sync/reference.md +34 -14
  225. package/harness/bundles/java/cursor/harness-test/SKILL.md +3 -1
  226. package/harness/bundles/java/cursor/protocols/archive-report-protocol.md +10 -0
  227. package/harness/bundles/java/cursor/protocols/ledger-protocol.md +12 -0
  228. package/harness/bundles/java/cursor/protocols/powershell-protocol.md +21 -0
  229. package/harness/bundles/java/cursor/protocols/state-layout-protocol.md +11 -0
  230. package/harness/bundles/java/cursor/scripts/harness_archive.py +413 -2
  231. package/harness/bundles/java/cursor/scripts/harness_environment.py +556 -39
  232. package/harness/bundles/java/cursor/scripts/harness_gate.py +145 -0
  233. package/harness/bundles/java/cursor/scripts/harness_ledger.py +581 -55
  234. package/harness/bundles/java/cursor/scripts/harness_plan_finalize.py +85 -6
  235. package/harness/bundles/java/cursor/scripts/harness_service.py +291 -22
  236. package/harness/bundles/java/cursor/scripts/harness_test_runner.py +405 -25
  237. package/harness/manifests/general/claude-code.json +34 -34
  238. package/harness/manifests/general/codebuddy.json +34 -34
  239. package/harness/manifests/general/codex.json +34 -34
  240. package/harness/manifests/general/cursor.json +34 -34
  241. package/harness/manifests/java/claude-code.json +35 -35
  242. package/harness/manifests/java/codebuddy.json +35 -35
  243. package/harness/manifests/java/codex.json +35 -35
  244. package/harness/manifests/java/cursor.json +35 -35
  245. package/hunter-workflow-family.json +10 -10
  246. package/package.json +1 -1
@@ -3864,6 +3864,7 @@ def collect_summary_data(
3864
3864
  "timing",
3865
3865
  "skillCalls",
3866
3866
  "verification",
3867
+ "scenarioCoverage",
3867
3868
  "timeline",
3868
3869
  "changedFiles",
3869
3870
  "artifacts",
@@ -3887,6 +3888,7 @@ def collect_summary_data(
3887
3888
  "changeIdentity",
3888
3889
  "artifactStorage",
3889
3890
  "retention",
3891
+ "archiveDurability",
3890
3892
  "remoteCost",
3891
3893
  "projection",
3892
3894
  ):
@@ -3977,6 +3979,9 @@ def collect_summary_data(
3977
3979
  ver.setdefault("dbCompatibilityEvidence", {})
3978
3980
  ver.setdefault("coverageDisplay", ver.get("coverageDisplay", NOT_AVAILABLE))
3979
3981
 
3982
+ if not for_replay or not isinstance(data.get("scenarioCoverage"), dict):
3983
+ data["scenarioCoverage"] = hgate._validate_scenario_coverage(change_dir)
3984
+
3980
3985
  # stageStatus / finalStatus
3981
3986
  if not for_replay or not isinstance(data.get("stageStatus"), dict):
3982
3987
  data["stageStatus"] = _stage_status_from_sources(events, ledger, change_dir)
@@ -4801,6 +4806,18 @@ def render_fallback_html(summary: dict[str, Any]) -> str:
4801
4806
  remote_totals = rec(remote.get("totals"))
4802
4807
  projection = rec(summary.get("projection"))
4803
4808
  archive_integrity = rec(summary.get("archiveIntegrity"))
4809
+ durability = rec(summary.get("archiveDurability"))
4810
+ durability_status = str(
4811
+ durability.get("status") or "ARCHIVED_LOCAL_ONLY"
4812
+ )
4813
+ scenario_coverage = rec(summary.get("scenarioCoverage"))
4814
+ unexecuted_scenarios = [
4815
+ str(item) for item in seq(scenario_coverage.get("unexecuted"))
4816
+ ]
4817
+ scenario_status = str(
4818
+ scenario_coverage.get("code")
4819
+ or ("SCENARIO_COVERAGE_OK" if scenario_coverage.get("ok") else "NOT_RUN")
4820
+ )
4804
4821
  total_files = rec(summary.get("archiveManifest")).get("totalArchiveFiles")
4805
4822
  total_files_text = "" if total_files is None else str(total_files)
4806
4823
 
@@ -4823,6 +4840,9 @@ def render_fallback_html(summary: dict[str, Any]) -> str:
4823
4840
  <article class="metric"><small>全流程耗时</small><strong>{esc(timing.get("workflowWallClockMs") or "N/A")}</strong></article>
4824
4841
  <article class="metric"><small>归档文件</small><strong>{esc(total_files_text or "N/A")}</strong></article></section>
4825
4842
  <article class="card"><h2>验证概览</h2><div class="groups">{"".join(groups)}</div></article>
4843
+ <article class="card"><h2>场景执行闭环</h2>
4844
+ <p>结构化运行回执:{esc(scenario_status)}</p>
4845
+ <p>未执行 / 未通过:{esc(", ".join(unexecuted_scenarios) or "无")}</p></article>
4826
4846
  <article class="card"><h2>风险与动作</h2><div class="risk"><section><h3>当前风险</h3><ul>{risk_rows}</ul></section><section><h3>人工动作</h3><ul>{action_rows}</ul></section></div></article>
4827
4847
  {release_card}
4828
4848
  <details><summary>技术证据 · 时间守恒</summary><div><p>conservationDeltaMs={esc(timing.get("conservationDeltaMs") if "conservationDeltaMs" in timing else "N/A")}</p>
@@ -4833,8 +4853,10 @@ def render_fallback_html(summary: dict[str, Any]) -> str:
4833
4853
  <p>远端 runner 成本:{esc(measurement_label(remote_totals.get("runnerMinutes") or remote.get("runnerMinutes")))}</p>
4834
4854
  <p>新增制品字节:{esc(measurement_label(storage.get("bytesAdded")))}</p></div></details>
4835
4855
  <details><summary>技术证据 · 发布与归档治理</summary><div>
4836
- <p>归档完整性:{esc(archive_integrity.get("code") or rec(summary.get("archiveManifest")).get("checksumStatus") or "N/A")}</p>
4837
- <p>历史质量:未闭合尝试 {esc(timing.get("unclosedAttemptCount") if "unclosedAttemptCount" in timing else "N/A")}</p>
4856
+ <p>归档完整性:{esc(archive_integrity.get("code") or rec(summary.get("archiveManifest")).get("checksumStatus") or "N/A")}</p>
4857
+ <p>归档持久性:{esc(durability_status)} · {esc(durability.get("risk") or "已通过持久介质回读校验")}</p>
4858
+ <p>保留策略:{esc(durability.get("retentionPolicy") or "N/A")}</p>
4859
+ <p>历史质量:未闭合尝试 {esc(timing.get("unclosedAttemptCount") if "unclosedAttemptCount" in timing else "N/A")}</p>
4838
4860
  <p>投影状态:{esc(projection.get("code") or projection.get("mode") or "N/A")}</p></div></details>
4839
4861
  <details><summary>技术证据 · 变更文件</summary><div><table><tbody>{file_rows}</tbody></table></div></details>
4840
4862
  <details><summary>技术证据 · 命令</summary><div><table><tbody>{command_rows}</tbody></table></div></details>
@@ -6896,6 +6918,262 @@ def run_service_stop(change_dir: Path) -> dict[str, Any]:
6896
6918
  return {"ran": True, "skipped": False, "ok": False, "warning": str(exc)}
6897
6919
 
6898
6920
 
6921
+ def _path_is_within(path: Path, root: Path) -> bool:
6922
+ try:
6923
+ path.resolve().relative_to(root.resolve())
6924
+ return True
6925
+ except ValueError:
6926
+ return False
6927
+
6928
+
6929
+ def _archive_tree_digest(root: Path) -> str:
6930
+ """Hash a complete archive tree without following links.
6931
+
6932
+ Directory entries are included so an empty directory cannot disappear
6933
+ unnoticed. Durable archives reject links rather than copying data from
6934
+ outside the finalized archive boundary.
6935
+ """
6936
+ root = root.resolve()
6937
+ if not root.is_dir():
6938
+ raise OSError(f"archive payload directory not found: {root}")
6939
+ digest = hashlib.sha256()
6940
+ for directory, dir_names, file_names in os.walk(root, followlinks=False):
6941
+ current = Path(directory)
6942
+ dir_names.sort()
6943
+ file_names.sort()
6944
+ for name in dir_names:
6945
+ child = current / name
6946
+ if child.is_symlink():
6947
+ raise OSError(f"durable archive refuses directory link: {child}")
6948
+ relative = child.relative_to(root).as_posix()
6949
+ digest.update(b"D\0")
6950
+ digest.update(relative.encode("utf-8"))
6951
+ digest.update(b"\0")
6952
+ for name in file_names:
6953
+ child = current / name
6954
+ if child.is_symlink():
6955
+ raise OSError(f"durable archive refuses file link: {child}")
6956
+ relative = child.relative_to(root).as_posix()
6957
+ file_hash = hashlib.sha256()
6958
+ size = 0
6959
+ with child.open("rb") as handle:
6960
+ while True:
6961
+ chunk = handle.read(1024 * 1024)
6962
+ if not chunk:
6963
+ break
6964
+ size += len(chunk)
6965
+ file_hash.update(chunk)
6966
+ digest.update(b"F\0")
6967
+ digest.update(relative.encode("utf-8"))
6968
+ digest.update(b"\0")
6969
+ digest.update(str(size).encode("ascii"))
6970
+ digest.update(b"\0")
6971
+ digest.update(file_hash.digest())
6972
+ digest.update(b"\0")
6973
+ return "sha256:" + digest.hexdigest()
6974
+
6975
+
6976
+ def _atomic_write_json(path: Path, data: dict[str, Any]) -> None:
6977
+ path.parent.mkdir(parents=True, exist_ok=True)
6978
+ temporary = path.with_name(f".{path.name}.{os.getpid()}.{uuid.uuid4().hex}.tmp")
6979
+ encoded = (
6980
+ json.dumps(data, ensure_ascii=False, indent=2, sort_keys=True) + "\n"
6981
+ ).encode("utf-8")
6982
+ try:
6983
+ with temporary.open("wb") as handle:
6984
+ handle.write(encoded)
6985
+ handle.flush()
6986
+ os.fsync(handle.fileno())
6987
+ os.replace(temporary, path)
6988
+ except BaseException:
6989
+ temporary.unlink(missing_ok=True)
6990
+ raise
6991
+
6992
+
6993
+ def write_durable_archive(
6994
+ archive_payload: Path,
6995
+ durable_root: Path,
6996
+ *,
6997
+ archive_id: str,
6998
+ retention_policy: str,
6999
+ ) -> dict[str, Any]:
7000
+ """Publish and read back a content-addressed durable archive.
7001
+
7002
+ The receipt is outside the payload so the payload hash is not
7003
+ self-referential. An existing object is reusable only when its full tree
7004
+ hash matches the requested content.
7005
+ """
7006
+ source = archive_payload.resolve()
7007
+ root = durable_root.resolve()
7008
+ if not retention_policy.strip():
7009
+ raise ValueError("retention policy must not be empty")
7010
+ if (
7011
+ not archive_id
7012
+ or archive_id in {".", ".."}
7013
+ or Path(archive_id).name != archive_id
7014
+ ):
7015
+ raise ValueError(f"invalid durable archive id: {archive_id!r}")
7016
+
7017
+ payload_hash = _archive_tree_digest(source)
7018
+ digest_hex = payload_hash.removeprefix("sha256:")
7019
+ object_dir = root / "objects" / "sha256" / digest_hex
7020
+ durable_payload = object_dir / "payload"
7021
+ receipt_path = root / "receipts" / f"{archive_id}.json"
7022
+ staging_root = root / ".staging" / f"d-{uuid.uuid4().hex}"
7023
+ staged_payload = staging_root / "payload"
7024
+ object_reused = False
7025
+
7026
+ try:
7027
+ if durable_payload.exists():
7028
+ if not durable_payload.is_dir():
7029
+ raise OSError(f"durable object path is not a directory: {durable_payload}")
7030
+ readback_hash = _archive_tree_digest(durable_payload)
7031
+ if readback_hash != payload_hash:
7032
+ raise OSError(
7033
+ "existing durable object failed readback verification: "
7034
+ f"expected {payload_hash}, got {readback_hash}"
7035
+ )
7036
+ object_reused = True
7037
+ else:
7038
+ staging_root.mkdir(parents=True, exist_ok=False)
7039
+ shutil.copytree(source, staged_payload, copy_function=shutil.copy2)
7040
+ staged_hash = _archive_tree_digest(staged_payload)
7041
+ if staged_hash != payload_hash:
7042
+ raise OSError(
7043
+ "durable staging readback mismatch: "
7044
+ f"expected {payload_hash}, got {staged_hash}"
7045
+ )
7046
+ object_dir.mkdir(parents=True, exist_ok=True)
7047
+ try:
7048
+ os.replace(staged_payload, durable_payload)
7049
+ except OSError:
7050
+ # A concurrent writer may have published the same object.
7051
+ if not durable_payload.is_dir():
7052
+ raise
7053
+ readback_hash = _archive_tree_digest(durable_payload)
7054
+ if readback_hash != payload_hash:
7055
+ raise
7056
+ object_reused = True
7057
+
7058
+ verified_hash = _archive_tree_digest(durable_payload)
7059
+ if verified_hash != payload_hash:
7060
+ raise OSError(
7061
+ "durable object verification failed: "
7062
+ f"expected {payload_hash}, got {verified_hash}"
7063
+ )
7064
+
7065
+ receipt = {
7066
+ "schemaVersion": 1,
7067
+ "archiveId": archive_id,
7068
+ "status": "ARCHIVED_DURABLE",
7069
+ "payloadHash": payload_hash,
7070
+ "payloadPath": str(durable_payload),
7071
+ "retentionPolicy": retention_policy,
7072
+ "verifiedAt": now_iso(),
7073
+ "verification": {
7074
+ "algorithm": "sha256-tree-v1",
7075
+ "readBack": True,
7076
+ },
7077
+ "sourceDeletion": {
7078
+ "authorizedOnlyAfterVerification": True,
7079
+ },
7080
+ }
7081
+ if receipt_path.exists():
7082
+ existing = read_json(receipt_path)
7083
+ if (
7084
+ not isinstance(existing, dict)
7085
+ or existing.get("payloadHash") != payload_hash
7086
+ or existing.get("archiveId") != archive_id
7087
+ ):
7088
+ raise OSError(
7089
+ f"durable receipt already exists for different content: {receipt_path}"
7090
+ )
7091
+ receipt = existing
7092
+ else:
7093
+ _atomic_write_json(receipt_path, receipt)
7094
+
7095
+ return {
7096
+ "status": "ARCHIVED_DURABLE",
7097
+ "payloadHash": payload_hash,
7098
+ "payloadPath": str(durable_payload),
7099
+ "receiptPath": str(receipt_path),
7100
+ "retentionPolicy": retention_policy,
7101
+ "readBackVerified": True,
7102
+ "objectReused": object_reused,
7103
+ }
7104
+ finally:
7105
+ shutil.rmtree(staging_root, ignore_errors=True)
7106
+ staging_parent = root / ".staging"
7107
+ try:
7108
+ staging_parent.rmdir()
7109
+ except OSError:
7110
+ pass
7111
+
7112
+
7113
+ def restore_durable_archive(receipt_path: Path, target_root: Path) -> dict[str, Any]:
7114
+ """Restore a verified durable object without overwriting an existing archive."""
7115
+ receipt_file = receipt_path.resolve()
7116
+ receipt = read_json(receipt_file)
7117
+ if not isinstance(receipt, dict) or receipt.get("schemaVersion") != 1:
7118
+ raise ValueError(f"invalid durable archive receipt: {receipt_file}")
7119
+ if receipt.get("status") != "ARCHIVED_DURABLE":
7120
+ raise ValueError(f"receipt is not durable: {receipt_file}")
7121
+
7122
+ archive_id = str(receipt.get("archiveId") or "")
7123
+ payload_hash = str(receipt.get("payloadHash") or "")
7124
+ if (
7125
+ not archive_id
7126
+ or Path(archive_id).name != archive_id
7127
+ or not re.fullmatch(r"sha256:[0-9a-f]{64}", payload_hash)
7128
+ ):
7129
+ raise ValueError(f"invalid durable archive receipt fields: {receipt_file}")
7130
+
7131
+ durable_root = receipt_file.parent.parent.resolve()
7132
+ expected_payload = (
7133
+ durable_root
7134
+ / "objects"
7135
+ / "sha256"
7136
+ / payload_hash.removeprefix("sha256:")
7137
+ / "payload"
7138
+ ).resolve()
7139
+ declared_payload = Path(str(receipt.get("payloadPath") or "")).resolve()
7140
+ if declared_payload != expected_payload or not _path_is_within(
7141
+ expected_payload, durable_root / "objects"
7142
+ ):
7143
+ raise ValueError("durable receipt payload path is outside its object store")
7144
+ actual_hash = _archive_tree_digest(expected_payload)
7145
+ if actual_hash != payload_hash:
7146
+ raise OSError(
7147
+ f"durable payload hash mismatch: expected {payload_hash}, got {actual_hash}"
7148
+ )
7149
+
7150
+ restore_root = target_root.resolve()
7151
+ destination = restore_root / archive_id
7152
+ if destination.exists():
7153
+ raise FileExistsError(f"restore destination already exists: {destination}")
7154
+ restore_root.mkdir(parents=True, exist_ok=True)
7155
+ staging = restore_root / f".restore-{uuid.uuid4().hex}"
7156
+ try:
7157
+ shutil.copytree(expected_payload, staging, copy_function=shutil.copy2)
7158
+ restored_hash = _archive_tree_digest(staging)
7159
+ if restored_hash != payload_hash:
7160
+ raise OSError(
7161
+ f"restored payload hash mismatch: expected {payload_hash}, got {restored_hash}"
7162
+ )
7163
+ os.replace(staging, destination)
7164
+ finally:
7165
+ shutil.rmtree(staging, ignore_errors=True)
7166
+
7167
+ return {
7168
+ "ok": True,
7169
+ "action": "restore-durable",
7170
+ "archiveId": archive_id,
7171
+ "payloadHash": payload_hash,
7172
+ "readBackVerified": True,
7173
+ "restoredArchive": str(destination),
7174
+ }
7175
+
7176
+
6899
7177
  # ---------------------------------------------------------------------------
6900
7178
  # finalize
6901
7179
  # ---------------------------------------------------------------------------
@@ -6905,6 +7183,8 @@ def cmd_finalize(
6905
7183
  change_dir: Path,
6906
7184
  archive_root: Path,
6907
7185
  *,
7186
+ durable_root: Path | None = None,
7187
+ retention_policy: str = "unspecified",
6908
7188
  skip_ingest: bool = False,
6909
7189
  allow_missing_review: bool = False,
6910
7190
  archive_intent: str = "release-candidate",
@@ -6923,6 +7203,26 @@ def cmd_finalize(
6923
7203
  archive_root.mkdir(parents=True, exist_ok=True)
6924
7204
  archive_dir = archive_root / f"{today_date()}-{change_name}"
6925
7205
  project_root = find_project_root(original_change_dir)
7206
+ if durable_root is not None:
7207
+ durable_root = durable_root.resolve()
7208
+ if _path_is_within(durable_root, project_root):
7209
+ return 1, {
7210
+ "ok": False,
7211
+ "action": "finalize",
7212
+ "change_dir": str(original_change_dir),
7213
+ "archive_dir": str(archive_dir),
7214
+ "issues": [
7215
+ {
7216
+ "code": "DURABLE_ROOT_NOT_INDEPENDENT",
7217
+ "severity": "error",
7218
+ "message": (
7219
+ "durable root must be outside the project so project-local "
7220
+ "state loss cannot delete both copies"
7221
+ ),
7222
+ }
7223
+ ],
7224
+ "error": "durable root is inside the project",
7225
+ }
6926
7226
  try:
6927
7227
  resolved_state_dir = hp.resolve_state_dir_for_contract(
6928
7228
  original_change_dir, project_root
@@ -7281,6 +7581,29 @@ def cmd_finalize(
7281
7581
  for_replay=False,
7282
7582
  )
7283
7583
  summary_path = work_dir / "reports" / "final" / "summary-data.json"
7584
+ summary["archiveDurability"] = {
7585
+ "status": (
7586
+ "ARCHIVED_DURABLE"
7587
+ if durable_root is not None
7588
+ else "ARCHIVED_LOCAL_ONLY"
7589
+ ),
7590
+ "retentionPolicy": (
7591
+ retention_policy if durable_root is not None else "project-local"
7592
+ ),
7593
+ "readBackRequiredBeforeSourceDeletion": durable_root is not None,
7594
+ "risk": (
7595
+ None
7596
+ if durable_root is not None
7597
+ else (
7598
+ "The archive exists only under the project-local .harness tree "
7599
+ "and may be lost with that workspace."
7600
+ )
7601
+ ),
7602
+ }
7603
+ payload["archiveDurability"] = _deepcopy_json(
7604
+ summary["archiveDurability"]
7605
+ )
7606
+ write_json(summary_path, summary)
7284
7607
  if allow_missing_review:
7285
7608
  reasons = list(summary.get("finalStatusReasons") or [])
7286
7609
  reason = "review missing on full tier (allowed by user)"
@@ -7606,6 +7929,43 @@ def cmd_finalize(
7606
7929
  payload["ok"] = False
7607
7930
  return 1, payload
7608
7931
 
7932
+ # A durable sink, when requested, is committed and read back before either
7933
+ # publishing the local archive or deleting the source change directory.
7934
+ if durable_root is not None:
7935
+ try:
7936
+ durability = write_durable_archive(
7937
+ operation_temp_dir,
7938
+ durable_root,
7939
+ archive_id=archive_dir.name,
7940
+ retention_policy=retention_policy,
7941
+ )
7942
+ payload["archiveDurability"] = durability
7943
+ payload["steps"]["durable_archive"] = {
7944
+ "ok": True,
7945
+ **durability,
7946
+ }
7947
+ except (OSError, ValueError, json.JSONDecodeError) as exc:
7948
+ payload["error"] = f"durable archive write failed: {exc}"
7949
+ payload["issues"] = [
7950
+ {
7951
+ "code": "DURABLE_ARCHIVE_WRITE_FAILED",
7952
+ "severity": "error",
7953
+ "message": str(exc),
7954
+ }
7955
+ ]
7956
+ payload["steps"]["durable_archive"] = {
7957
+ "ok": False,
7958
+ "error": str(exc),
7959
+ }
7960
+ payload["steps"]["delete_original"] = {
7961
+ "ok": False,
7962
+ "deleted": False,
7963
+ }
7964
+ _restore_finalize_failure()
7965
+ payload["warnings"] = warnings
7966
+ payload["ok"] = False
7967
+ return 1, payload
7968
+
7609
7969
  # Publish only after every validator passes. The archive path is never used
7610
7970
  # as mutable staging, so a failed attempt cannot poison the next retry.
7611
7971
  try:
@@ -7679,6 +8039,7 @@ def cmd_finalize(
7679
8039
  "manifestSha256": sha256_file(
7680
8040
  archive_dir / "evidence" / "archive-manifest-after.json"
7681
8041
  ),
8042
+ "archiveDurability": payload.get("archiveDurability"),
7682
8043
  },
7683
8044
  )
7684
8045
  except OSError as exc:
@@ -7854,9 +8215,18 @@ def cmd_certify_local_cli(args: argparse.Namespace) -> int:
7854
8215
  def cmd_finalize_cli(args: argparse.Namespace) -> int:
7855
8216
  change_dir = resolve_path(args.change_dir)
7856
8217
  archive_root = resolve_path(args.archive_root)
8218
+ durable_root = (
8219
+ resolve_path(args.durable_root)
8220
+ if getattr(args, "durable_root", None)
8221
+ else None
8222
+ )
7857
8223
  code, payload = cmd_finalize(
7858
8224
  change_dir,
7859
8225
  archive_root,
8226
+ durable_root=durable_root,
8227
+ retention_policy=str(
8228
+ getattr(args, "retention_policy", "unspecified")
8229
+ ),
7860
8230
  skip_ingest=bool(args.skip_ingest),
7861
8231
  allow_missing_review=bool(getattr(args, "allow_missing_review", False)),
7862
8232
  archive_intent=str(getattr(args, "intent", "release-candidate")),
@@ -7865,6 +8235,25 @@ def cmd_finalize_cli(args: argparse.Namespace) -> int:
7865
8235
  return code
7866
8236
 
7867
8237
 
8238
+ def cmd_restore_durable_cli(args: argparse.Namespace) -> int:
8239
+ receipt_path = resolve_path(args.receipt)
8240
+ target_root = resolve_path(args.target_root)
8241
+ try:
8242
+ payload = restore_durable_archive(receipt_path, target_root)
8243
+ code = 0
8244
+ except (OSError, ValueError, json.JSONDecodeError) as exc:
8245
+ code = 1
8246
+ payload = {
8247
+ "ok": False,
8248
+ "action": "restore-durable",
8249
+ "receipt": str(receipt_path),
8250
+ "targetRoot": str(target_root),
8251
+ "error": str(exc),
8252
+ }
8253
+ emit_json(payload)
8254
+ return code
8255
+
8256
+
7868
8257
  def cmd_replay_cli(args: argparse.Namespace) -> int:
7869
8258
  archive_dir = resolve_path(args.archive_dir)
7870
8259
  out_path = resolve_path(args.out) if getattr(args, "out", None) else None
@@ -8087,6 +8476,19 @@ def build_parser() -> argparse.ArgumentParser:
8087
8476
  p_fin = sub.add_parser("finalize", help="single-process archive finalize")
8088
8477
  p_fin.add_argument("--change-dir", required=True)
8089
8478
  p_fin.add_argument("--archive-root", required=True)
8479
+ p_fin.add_argument(
8480
+ "--durable-root",
8481
+ default=None,
8482
+ help=(
8483
+ "independent content-addressed archive store; it must be outside "
8484
+ "the project and is verified before source deletion"
8485
+ ),
8486
+ )
8487
+ p_fin.add_argument(
8488
+ "--retention-policy",
8489
+ default="unspecified",
8490
+ help="retention policy recorded in the durable archive receipt",
8491
+ )
8090
8492
  p_fin.add_argument("--skip-ingest", action="store_true")
8091
8493
  p_fin.add_argument(
8092
8494
  "--allow-missing-review",
@@ -8102,6 +8504,15 @@ def build_parser() -> argparse.ArgumentParser:
8102
8504
  p_fin.add_argument("--json", action="store_true", default=True)
8103
8505
  p_fin.set_defaults(func=cmd_finalize_cli)
8104
8506
 
8507
+ p_restore = sub.add_parser(
8508
+ "restore-durable",
8509
+ help="verify a durable receipt and restore its archive without overwriting",
8510
+ )
8511
+ p_restore.add_argument("--receipt", required=True)
8512
+ p_restore.add_argument("--target-root", required=True)
8513
+ p_restore.add_argument("--json", action="store_true", default=True)
8514
+ p_restore.set_defaults(func=cmd_restore_durable_cli)
8515
+
8105
8516
  p_rep = sub.add_parser("replay", help="read-only re-collect + validate")
8106
8517
  p_rep.add_argument("--archive-dir", required=True)
8107
8518
  p_rep.add_argument("--out", default=None, help="write summary-data JSON outside archive")