@hunter-harness/workflow-harness 0.2.55 → 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 (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 +108 -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 +108 -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 +108 -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 +108 -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 +108 -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 +108 -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 +108 -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 +108 -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
@@ -11,6 +11,7 @@ import os
11
11
  import re
12
12
  import tempfile
13
13
  import sys
14
+ import uuid
14
15
  from pathlib import Path
15
16
  from typing import Any
16
17
 
@@ -19,6 +20,7 @@ if str(SCRIPTS_DIR) not in sys.path:
19
20
  sys.path.insert(0, str(SCRIPTS_DIR))
20
21
 
21
22
  from harness_paths import resolve_state_dir_for_contract
23
+ import harness_events as he
22
24
 
23
25
 
24
26
  SCHEMA_VERSION = 1
@@ -96,6 +98,171 @@ def _all_batches(change_dir: Path) -> list[dict[str, Any]]:
96
98
  return batches
97
99
 
98
100
 
101
+ def _session_path(change_dir: Path) -> Path:
102
+ return _state_root(change_dir) / "runtime" / "fixback-session.json"
103
+
104
+
105
+ def _next_step(batch: dict[str, Any]) -> str:
106
+ issues = [item for item in batch.get("issues", []) if isinstance(item, dict)]
107
+ if not issues:
108
+ return "add-issues"
109
+ if any(item.get("status") != "RESOLVED" for item in issues):
110
+ return "resolve-issues"
111
+ receipts = batch.get("receipts") if isinstance(batch.get("receipts"), dict) else {}
112
+ if not receipts.get("affected"):
113
+ return "verify-affected"
114
+ if not receipts.get("review"):
115
+ return "review"
116
+ return "close"
117
+
118
+
119
+ def resume_batch(
120
+ change_dir: Path,
121
+ *,
122
+ batch_id: str | None,
123
+ product_identity: str | None,
124
+ root_cause: str | None,
125
+ run_id: str | None,
126
+ attempt: int | None,
127
+ ) -> dict[str, Any]:
128
+ """Open once or resume the active Fixback session with a stable run identity."""
129
+ opened = [item for item in _all_batches(change_dir) if item.get("status") == "OPEN"]
130
+ resumed = bool(opened)
131
+ if opened:
132
+ batch = sorted(opened, key=lambda item: str(item.get("openedAt") or ""))[0]
133
+ else:
134
+ if not batch_id or not product_identity or not root_cause:
135
+ raise ValueError(
136
+ "FIXBACK_RESUME_INPUT_REQUIRED: new sessions require batch-id, "
137
+ "product-identity and root-cause"
138
+ )
139
+ batch = open_batch(
140
+ change_dir,
141
+ batch_id=batch_id,
142
+ product_identity=product_identity,
143
+ root_cause=root_cause,
144
+ )
145
+ session_path = _session_path(change_dir)
146
+ existing: dict[str, Any] = {}
147
+ if session_path.is_file():
148
+ try:
149
+ loaded = json.loads(session_path.read_text(encoding="utf-8-sig"))
150
+ existing = loaded if isinstance(loaded, dict) else {}
151
+ except (OSError, json.JSONDecodeError):
152
+ existing = {}
153
+ same_open_session = (
154
+ existing.get("status") == "ACTIVE"
155
+ and existing.get("batchId") == batch.get("batchId")
156
+ )
157
+ effective_run_id = (
158
+ str(existing.get("runId"))
159
+ if same_open_session and existing.get("runId")
160
+ else str(run_id or "fixback-" + uuid.uuid4().hex)
161
+ )
162
+ effective_attempt = (
163
+ int(existing.get("attempt"))
164
+ if same_open_session and isinstance(existing.get("attempt"), int)
165
+ else max(1, int(attempt or 1))
166
+ )
167
+ next_step = _next_step(batch)
168
+ session = {
169
+ "schemaVersion": 1,
170
+ "status": "ACTIVE",
171
+ "batchId": batch.get("batchId"),
172
+ "runId": effective_run_id,
173
+ "attempt": effective_attempt,
174
+ "nextStep": next_step,
175
+ "updatedAt": now_iso(),
176
+ }
177
+ _write_json(session_path, session)
178
+ he.append_event(
179
+ change_dir,
180
+ phase="run",
181
+ type_="decision",
182
+ note=(
183
+ "已恢复现有修复批次,将从未完成步骤继续。"
184
+ if resumed
185
+ else "已创建修复批次,将按问题逐项执行 RED/GREEN。"
186
+ ),
187
+ run_id=effective_run_id,
188
+ )
189
+ return {
190
+ "ok": True,
191
+ "code": "FIXBACK_RESUMED" if resumed else "FIXBACK_OPENED",
192
+ "resumed": resumed,
193
+ "batchId": batch.get("batchId"),
194
+ "runId": effective_run_id,
195
+ "attempt": effective_attempt,
196
+ "nextStep": next_step,
197
+ "requiredVerifications": batch.get("requiredVerifications", []),
198
+ "changedFiles": batch.get("changedFiles", []),
199
+ "sessionPath": str(session_path),
200
+ }
201
+
202
+
203
+ def invalidate_affected_evidence(
204
+ change_dir: Path,
205
+ *,
206
+ changed_files: list[str],
207
+ batch_id: str,
208
+ ) -> dict[str, Any]:
209
+ """Invalidate only verification targets whose declared inputs were changed."""
210
+ normalized = {
211
+ str(path).replace("\\", "/").lstrip("./")
212
+ for path in changed_files
213
+ if str(path).strip()
214
+ }
215
+ ledger_path = _state_root(change_dir) / "evidence" / "verification-ledger.json"
216
+ if not normalized or not ledger_path.is_file():
217
+ return {"ok": True, "targetIds": [], "validations": []}
218
+ try:
219
+ ledger = json.loads(ledger_path.read_text(encoding="utf-8-sig"))
220
+ except (OSError, json.JSONDecodeError):
221
+ return {"ok": False, "code": "FIXBACK_LEDGER_INVALID"}
222
+ if not isinstance(ledger, dict):
223
+ return {"ok": False, "code": "FIXBACK_LEDGER_INVALID"}
224
+
225
+ def affected(entry: dict[str, Any]) -> bool:
226
+ inputs = entry.get("inputsFiles")
227
+ if not isinstance(inputs, list):
228
+ return False
229
+ input_set = {
230
+ str(path).replace("\\", "/").lstrip("./")
231
+ for path in inputs
232
+ if str(path).strip()
233
+ }
234
+ return bool(normalized & input_set)
235
+
236
+ invalidation = {
237
+ "code": "FIXBACK_AFFECTED_INPUT_CHANGED",
238
+ "batchId": batch_id,
239
+ "changedFiles": sorted(normalized),
240
+ "invalidatedAt": now_iso(),
241
+ }
242
+ target_ids: list[str] = []
243
+ targets = ledger.get("verificationTargets")
244
+ if isinstance(targets, dict):
245
+ for target_id, target in targets.items():
246
+ if isinstance(target, dict) and affected(target):
247
+ target["reusable"] = False
248
+ target["invalidation"] = dict(invalidation)
249
+ target_ids.append(str(target_id))
250
+ validation_names: list[str] = []
251
+ validations = ledger.get("validations")
252
+ if isinstance(validations, dict):
253
+ for name, entry in validations.items():
254
+ if isinstance(entry, dict) and affected(entry):
255
+ entry["reusable"] = False
256
+ entry["invalidation"] = dict(invalidation)
257
+ validation_names.append(str(name))
258
+ _write_json(ledger_path, ledger)
259
+ return {
260
+ "ok": True,
261
+ "targetIds": sorted(target_ids),
262
+ "validations": sorted(validation_names),
263
+ }
264
+
265
+
99
266
  def _required_verifications(issues: list[dict[str, Any]]) -> list[str]:
100
267
  required = {"affected", "review"}
101
268
  for issue in issues:
@@ -506,6 +673,11 @@ def resolve_issue(
506
673
  }
507
674
  )
508
675
  batch["updatedAt"] = resolved_at
676
+ batch["invalidation"] = invalidate_affected_evidence(
677
+ change_dir,
678
+ changed_files=[record["path"] for record in changed_records],
679
+ batch_id=batch_id,
680
+ )
509
681
  _write_json(_batch_path(change_dir, batch_id), batch)
510
682
  return issue
511
683
 
@@ -652,6 +824,13 @@ def _emit(value: dict[str, Any]) -> int:
652
824
  def build_parser() -> argparse.ArgumentParser:
653
825
  parser = argparse.ArgumentParser(prog="harness_fixback.py")
654
826
  sub = parser.add_subparsers(dest="command", required=True)
827
+ resume = sub.add_parser("resume")
828
+ resume.add_argument("--change-dir", required=True)
829
+ resume.add_argument("--batch-id")
830
+ resume.add_argument("--product-identity")
831
+ resume.add_argument("--root-cause")
832
+ resume.add_argument("--run-id")
833
+ resume.add_argument("--attempt", type=int)
655
834
  opened = sub.add_parser("open")
656
835
  opened.add_argument("--change-dir", required=True)
657
836
  opened.add_argument("--batch-id", required=True)
@@ -692,7 +871,16 @@ def main(argv: list[str] | None = None) -> int:
692
871
  args = build_parser().parse_args(argv)
693
872
  change_dir = Path(args.change_dir)
694
873
  try:
695
- if args.command == "open":
874
+ if args.command == "resume":
875
+ value = resume_batch(
876
+ change_dir,
877
+ batch_id=args.batch_id,
878
+ product_identity=args.product_identity,
879
+ root_cause=args.root_cause,
880
+ run_id=args.run_id,
881
+ attempt=args.attempt,
882
+ )
883
+ elif args.command == "open":
696
884
  value = open_batch(
697
885
  change_dir,
698
886
  batch_id=args.batch_id,
@@ -693,11 +693,12 @@ def validate_ledger_for_phase_close(
693
693
  try:
694
694
  current_repository = hp.repository_identity(root)
695
695
  current_ownership = hl.ownership_hash(contract)
696
- current_diff = hl.compute_ownership_diff(
696
+ current_diff_detail = hl.compute_ownership_diff(
697
697
  root,
698
698
  base=str(ledger["baseCommit"]),
699
699
  change_dir=change_dir,
700
- )["diffHash"]
700
+ )
701
+ current_diff = current_diff_detail["diffHash"]
701
702
  current_head = _git_text(root, "rev-parse", "--verify", "HEAD")
702
703
  except (OSError, ValueError, RuntimeError) as exc:
703
704
  return {
@@ -727,6 +728,18 @@ def validate_ledger_for_phase_close(
727
728
  "storedHead": ledger.get("currentHead"),
728
729
  "currentHead": current_head,
729
730
  "ledgerPath": str(ledger_path) if ledger_path else None,
731
+ "pathSummary": {
732
+ "ownedFiles": list(current_diff_detail.get("files") or [])[:20],
733
+ "foreignPaths": list(current_diff_detail.get("foreignPaths") or [])[:20],
734
+ "ownedFileCount": current_diff_detail.get("ownedFileCount", 0),
735
+ "excludedRuntimeCount": current_diff_detail.get(
736
+ "excludedRuntimeCount", 0
737
+ ),
738
+ },
739
+ "recoveryAction": (
740
+ "先确认新增、删除或越界路径是否属于当前变更;更新所有权后,"
741
+ "只重新执行受影响的验证,再原样重试 close。"
742
+ ),
730
743
  }
731
744
 
732
745
  problems: list[dict[str, Any]] = []
@@ -1713,6 +1726,98 @@ def record_blocked_attempt(
1713
1726
  )
1714
1727
 
1715
1728
 
1729
+ def record_gate_blocked(
1730
+ change_dir: Path,
1731
+ *,
1732
+ phase: str,
1733
+ code: str,
1734
+ message: str,
1735
+ run_id: str | None = None,
1736
+ ) -> dict[str, Any]:
1737
+ """Persist a recoverable gate rejection without manufacturing a phase run."""
1738
+ return append_phase_event(
1739
+ change_dir,
1740
+ phase=phase,
1741
+ type_="gate.blocked",
1742
+ status="BLOCKED",
1743
+ note=message,
1744
+ run_id=run_id or "blocked-" + uuid.uuid4().hex[:12],
1745
+ code=code,
1746
+ )
1747
+
1748
+
1749
+ def record_gate_recovered(
1750
+ change_dir: Path,
1751
+ *,
1752
+ phase: str,
1753
+ run_id: str,
1754
+ ) -> dict[str, Any]:
1755
+ """Close the latest unresolved gate.blocked record for this phase."""
1756
+ blocked: dict[str, Any] | None = None
1757
+ for event in reversed(he.load_events(he.events_path(change_dir))):
1758
+ if event.get("phase") != phase:
1759
+ continue
1760
+ if event.get("type") == "gate.recovered":
1761
+ break
1762
+ if event.get("type") == "gate.blocked":
1763
+ blocked = event
1764
+ break
1765
+ if blocked is None:
1766
+ return {"ok": True, "skipped": True, "reason": "no-unresolved-block"}
1767
+ return append_phase_event(
1768
+ change_dir,
1769
+ phase=phase,
1770
+ type_="gate.recovered",
1771
+ status="OK",
1772
+ note="阶段交接已恢复,门禁现已成功启动。",
1773
+ run_id=run_id,
1774
+ code=str(blocked.get("code") or "GATE_BLOCKED"),
1775
+ )
1776
+
1777
+
1778
+ def validate_context_for_gate_begin(
1779
+ project: Path,
1780
+ change_id: str,
1781
+ phase: str,
1782
+ ) -> dict[str, Any]:
1783
+ """Require a completed Context handoff whenever modern Context state exists."""
1784
+ view = hctx.context_view(project, change_id)
1785
+ if not isinstance(view, dict) or not view.get("ok"):
1786
+ return {"ok": True, "code": "CONTEXT_LEGACY_COMPATIBLE"}
1787
+ current = view.get("current")
1788
+ transitions = view.get("transitions")
1789
+ begins = (view.get("attemptHistory") or {}).get("begins")
1790
+ if not isinstance(current, dict):
1791
+ return {"ok": True, "code": "CONTEXT_LEGACY_COMPATIBLE"}
1792
+ current_phase = str(current.get("phase") or view.get("currentPhase") or "")
1793
+ if current_phase != phase:
1794
+ return {
1795
+ "ok": False,
1796
+ "code": "CONTEXT_HANDOFF_REQUIRED",
1797
+ "message": "阶段交接尚未完成,门禁未启动;请先完成上下文交接。",
1798
+ "expectedPhase": current_phase or None,
1799
+ "requestedPhase": phase,
1800
+ }
1801
+ if isinstance(transitions, list) and transitions:
1802
+ latest = transitions[-1] if isinstance(transitions[-1], dict) else {}
1803
+ receipt_hash = latest.get("receiptHash")
1804
+ begin_items = begins if isinstance(begins, list) else []
1805
+ acknowledged = any(
1806
+ isinstance(item, dict)
1807
+ and item.get("phase") == phase
1808
+ and item.get("receiptHash") == receipt_hash
1809
+ for item in begin_items
1810
+ )
1811
+ if latest.get("toPhase") != phase or not acknowledged:
1812
+ return {
1813
+ "ok": False,
1814
+ "code": "CONTEXT_BEGIN_REQUIRED",
1815
+ "message": "阶段交接收据尚未确认,门禁未启动;请先执行上下文 begin。",
1816
+ "requestedPhase": phase,
1817
+ }
1818
+ return {"ok": True, "code": "CONTEXT_READY"}
1819
+
1820
+
1716
1821
  def _phase_event_exists(change_dir: Path, phase: str, type_: str, run_id: str) -> bool:
1717
1822
  return any(
1718
1823
  event.get("phase") == phase
@@ -2065,6 +2170,32 @@ def cmd_begin(args: argparse.Namespace) -> int:
2065
2170
  extra={k: v for k, v in resolved.items() if k not in {"ok", "message"}},
2066
2171
  )
2067
2172
  change_dir = Path(resolved["changeDir"])
2173
+ context_check = validate_context_for_gate_begin(
2174
+ project, str(resolved["changeId"]), args.phase
2175
+ )
2176
+ if not context_check.get("ok"):
2177
+ code = str(context_check.get("code") or "CONTEXT_HANDOFF_REQUIRED")
2178
+ message = str(
2179
+ context_check.get("message")
2180
+ or "阶段交接尚未完成,门禁未启动。"
2181
+ )
2182
+ record_gate_blocked(
2183
+ change_dir,
2184
+ phase=args.phase,
2185
+ code=code,
2186
+ message=message,
2187
+ run_id=args.run_id or os.environ.get("HUNTER_HARNESS_RUN_ID"),
2188
+ )
2189
+ return emit_error(
2190
+ code,
2191
+ message,
2192
+ as_json=as_json,
2193
+ extra={
2194
+ key: value
2195
+ for key, value in context_check.items()
2196
+ if key not in {"ok", "code", "message"}
2197
+ },
2198
+ )
2068
2199
  severity_mode = gate_severity_mode(project, change_dir)
2069
2200
  gate_warnings: list[dict[str, Any]] = []
2070
2201
  plan_verification: dict[str, Any] | None = None
@@ -2236,6 +2367,16 @@ def cmd_begin(args: argparse.Namespace) -> int:
2236
2367
  message="test guard begin failed (downgraded to WARN)",
2237
2368
  ))
2238
2369
  else:
2370
+ record_gate_blocked(
2371
+ change_dir,
2372
+ phase=args.phase,
2373
+ code=str(
2374
+ guard_result.get("code")
2375
+ or "TEST_GUARD_BEGIN_FAILED"
2376
+ ),
2377
+ message="测试保护快照建立失败,已停止阶段启动。",
2378
+ run_id=run_id,
2379
+ )
2239
2380
  hc.release_lease(
2240
2381
  project,
2241
2382
  change_id=resolved["changeId"],
@@ -2252,6 +2393,11 @@ def cmd_begin(args: argparse.Namespace) -> int:
2252
2393
  current_head = _git_text(execution_root, "rev-parse", "--verify", "HEAD")
2253
2394
  ledger, _ = hl.load_ledger(change_dir)
2254
2395
  base_commit = ledger.get("baseCommit") if isinstance(ledger, dict) else None
2396
+ try:
2397
+ begin_contract = hp.load_change_contract(change_dir)
2398
+ begin_ownership_hash = hl.ownership_hash(begin_contract)
2399
+ except (OSError, ValueError, json.JSONDecodeError):
2400
+ begin_ownership_hash = None
2255
2401
  capsule = {
2256
2402
  "schemaVersion": 1,
2257
2403
  "changeId": resolved["changeId"],
@@ -2264,6 +2410,7 @@ def cmd_begin(args: argparse.Namespace) -> int:
2264
2410
  "repositoryId": hp.repository_identity(execution_root),
2265
2411
  "baseCommit": base_commit or current_head,
2266
2412
  "currentHead": current_head,
2413
+ "ownershipHash": begin_ownership_hash,
2267
2414
  "createdAt": he.now_iso(),
2268
2415
  "projectionReceipt": projection,
2269
2416
  }
@@ -2281,6 +2428,11 @@ def cmd_begin(args: argparse.Namespace) -> int:
2281
2428
  executor_agent=args.executor_agent or os.environ.get("HUNTER_HARNESS_AGENT"),
2282
2429
  executor_model=args.executor_model or os.environ.get("HUNTER_HARNESS_MODEL"),
2283
2430
  )
2431
+ recovery_result = record_gate_recovered(
2432
+ change_dir,
2433
+ phase=args.phase,
2434
+ run_id=run_id,
2435
+ )
2284
2436
  except BaseException:
2285
2437
  hc.release_lease(
2286
2438
  project, change_id=resolved["changeId"], phase=args.phase, run_id=run_id
@@ -2300,6 +2452,7 @@ def cmd_begin(args: argparse.Namespace) -> int:
2300
2452
  "lease": claim.get("lease"),
2301
2453
  "identity": identity,
2302
2454
  "event": event_result,
2455
+ "gateRecovery": recovery_result,
2303
2456
  "testGuard": guard_result,
2304
2457
  "policySchemaVersion": policy.get("schemaVersion"),
2305
2458
  "planVerification": plan_verification,
@@ -2568,6 +2721,16 @@ def cmd_close(args: argparse.Namespace) -> int:
2568
2721
  status="LEDGER_VALIDATION_FAILED",
2569
2722
  error=ledger_result,
2570
2723
  )
2724
+ record_gate_blocked(
2725
+ change_dir,
2726
+ phase=args.phase,
2727
+ code=str(ledger_result.get("code") or "LEDGER_INVALID"),
2728
+ message=(
2729
+ "验证记录与当前变更身份不一致,已保留现场;"
2730
+ "请按返回的路径摘要重新验证受影响范围。"
2731
+ ),
2732
+ run_id=run_id,
2733
+ )
2571
2734
  return emit_error(
2572
2735
  str(ledger_result.get("code", "LEDGER_INVALID")),
2573
2736
  str(ledger_result.get("message", "ledger validation failed")),
@@ -2656,6 +2819,16 @@ def cmd_close(args: argparse.Namespace) -> int:
2656
2819
  "updatedAt": he.now_iso(),
2657
2820
  })
2658
2821
  write_phase_capsule(change_dir, args.phase, run_id, capsule)
2822
+ record_gate_blocked(
2823
+ change_dir,
2824
+ phase=args.phase,
2825
+ code=str(
2826
+ guard_result.get("code")
2827
+ or "TEST_GUARD_CLOSE_FAILED"
2828
+ ),
2829
+ message="测试保护关门失败,已保留现场并等待同一命令恢复。",
2830
+ run_id=run_id,
2831
+ )
2659
2832
  return emit_error(
2660
2833
  str(guard_result.get("code", "TEST_GUARD_CLOSE_FAILED")),
2661
2834
  "test guard close failed",
@@ -2741,6 +2914,13 @@ def cmd_close(args: argparse.Namespace) -> int:
2741
2914
  )
2742
2915
  platform_monitor = _sync_after_phase_close(project, change_dir)
2743
2916
  if isinstance(context_handoff, dict) and not context_handoff.get("ok"):
2917
+ record_gate_blocked(
2918
+ change_dir,
2919
+ phase=args.phase,
2920
+ code=str(context_handoff.get("code") or "PHASE_HANDOFF_PENDING"),
2921
+ message="阶段本地关门已完成,但上下文交接尚待恢复。",
2922
+ run_id=run_id,
2923
+ )
2744
2924
  return emit_error(
2745
2925
  "PHASE_HANDOFF_PENDING",
2746
2926
  "phase gate closed locally, but context handoff failed; retry the same close command",
@@ -2756,6 +2936,11 @@ def cmd_close(args: argparse.Namespace) -> int:
2756
2936
  },
2757
2937
  )
2758
2938
 
2939
+ recovery_result = record_gate_recovered(
2940
+ change_dir,
2941
+ phase=args.phase,
2942
+ run_id=run_id,
2943
+ )
2759
2944
  payload = {
2760
2945
  "ok": True,
2761
2946
  "code": close_code,
@@ -2773,6 +2958,7 @@ def cmd_close(args: argparse.Namespace) -> int:
2773
2958
  "gateWarnings": gate_warnings,
2774
2959
  "contextHandoff": context_handoff,
2775
2960
  "platformMonitor": platform_monitor,
2961
+ "gateRecovery": recovery_result,
2776
2962
  }
2777
2963
  emit(payload, as_json=as_json)
2778
2964
  return 0