@hunter-harness/workflow-harness 0.2.11 → 0.2.12

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 (242) hide show
  1. package/harness/bundles/general/claude-code/.harness-build.json +1 -1
  2. package/harness/bundles/general/claude-code/contracts/ci-metrics.schema.json +36 -0
  3. package/harness/bundles/general/claude-code/contracts/workflow-policy.json +30 -0
  4. package/harness/bundles/general/claude-code/harness-archive/SKILL.md +1 -1
  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 +1 -1
  7. package/harness/bundles/general/claude-code/harness-knowledge-query/SKILL.md +1 -1
  8. package/harness/bundles/general/claude-code/harness-plan/SKILL.md +4 -4
  9. package/harness/bundles/general/claude-code/harness-plan/checklist.md +6 -0
  10. package/harness/bundles/general/claude-code/harness-plan/reference.md +7 -2
  11. package/harness/bundles/general/claude-code/harness-review/SKILL.md +1 -1
  12. package/harness/bundles/general/claude-code/harness-run/SKILL.md +1 -1
  13. package/harness/bundles/general/claude-code/harness-run/checklist.md +3 -2
  14. package/harness/bundles/general/claude-code/harness-run/reference.md +6 -4
  15. package/harness/bundles/general/claude-code/harness-submit/SKILL.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 +1 -1
  18. package/harness/bundles/general/claude-code/scripts/harness_archive.py +282 -120
  19. package/harness/bundles/general/claude-code/scripts/harness_events.py +400 -24
  20. package/harness/bundles/general/claude-code/scripts/harness_gate.py +585 -21
  21. package/harness/bundles/general/claude-code/scripts/harness_integration.py +403 -18
  22. package/harness/bundles/general/claude-code/scripts/harness_ledger.py +225 -29
  23. package/harness/bundles/general/claude-code/scripts/harness_phase.py +926 -0
  24. package/harness/bundles/general/claude-code/scripts/harness_plan_finalize.py +361 -0
  25. package/harness/bundles/general/claude-code/scripts/harness_profile.py +50 -0
  26. package/harness/bundles/general/claude-code/scripts/harness_runtime.py +248 -0
  27. package/harness/bundles/general/claude-code/scripts/harness_state.py +15 -2
  28. package/harness/bundles/general/claude-code/scripts/harness_test_guard.py +188 -0
  29. package/harness/bundles/general/claude-code/scripts/harness_workflow_policy.py +19 -0
  30. package/harness/bundles/general/codebuddy/.harness-build.json +1 -1
  31. package/harness/bundles/general/codebuddy/contracts/ci-metrics.schema.json +36 -0
  32. package/harness/bundles/general/codebuddy/contracts/workflow-policy.json +30 -0
  33. package/harness/bundles/general/codebuddy/harness-archive/SKILL.md +1 -1
  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 +4 -4
  38. package/harness/bundles/general/codebuddy/harness-plan/checklist.md +6 -0
  39. package/harness/bundles/general/codebuddy/harness-plan/reference.md +7 -2
  40. package/harness/bundles/general/codebuddy/harness-review/SKILL.md +1 -1
  41. package/harness/bundles/general/codebuddy/harness-run/SKILL.md +1 -1
  42. package/harness/bundles/general/codebuddy/harness-run/checklist.md +3 -2
  43. package/harness/bundles/general/codebuddy/harness-run/reference.md +6 -4
  44. package/harness/bundles/general/codebuddy/harness-submit/SKILL.md +1 -1
  45. package/harness/bundles/general/codebuddy/harness-sync/SKILL.md +1 -1
  46. package/harness/bundles/general/codebuddy/harness-test/SKILL.md +1 -1
  47. package/harness/bundles/general/codebuddy/scripts/harness_archive.py +282 -120
  48. package/harness/bundles/general/codebuddy/scripts/harness_events.py +400 -24
  49. package/harness/bundles/general/codebuddy/scripts/harness_gate.py +585 -21
  50. package/harness/bundles/general/codebuddy/scripts/harness_integration.py +403 -18
  51. package/harness/bundles/general/codebuddy/scripts/harness_ledger.py +225 -29
  52. package/harness/bundles/general/codebuddy/scripts/harness_phase.py +926 -0
  53. package/harness/bundles/general/codebuddy/scripts/harness_plan_finalize.py +361 -0
  54. package/harness/bundles/general/codebuddy/scripts/harness_profile.py +50 -0
  55. package/harness/bundles/general/codebuddy/scripts/harness_runtime.py +248 -0
  56. package/harness/bundles/general/codebuddy/scripts/harness_state.py +15 -2
  57. package/harness/bundles/general/codebuddy/scripts/harness_test_guard.py +188 -0
  58. package/harness/bundles/general/codebuddy/scripts/harness_workflow_policy.py +19 -0
  59. package/harness/bundles/general/codex/.harness-build.json +1 -1
  60. package/harness/bundles/general/codex/contracts/ci-metrics.schema.json +36 -0
  61. package/harness/bundles/general/codex/contracts/workflow-policy.json +30 -0
  62. package/harness/bundles/general/codex/harness-archive/SKILL.md +1 -1
  63. package/harness/bundles/general/codex/harness-codebase-map/SKILL.md +1 -1
  64. package/harness/bundles/general/codex/harness-knowledge-ingest/SKILL.md +1 -1
  65. package/harness/bundles/general/codex/harness-knowledge-query/SKILL.md +1 -1
  66. package/harness/bundles/general/codex/harness-plan/SKILL.md +4 -4
  67. package/harness/bundles/general/codex/harness-plan/checklist.md +6 -0
  68. package/harness/bundles/general/codex/harness-plan/reference.md +7 -2
  69. package/harness/bundles/general/codex/harness-review/SKILL.md +1 -1
  70. package/harness/bundles/general/codex/harness-run/SKILL.md +1 -1
  71. package/harness/bundles/general/codex/harness-run/checklist.md +3 -2
  72. package/harness/bundles/general/codex/harness-run/reference.md +6 -4
  73. package/harness/bundles/general/codex/harness-submit/SKILL.md +1 -1
  74. package/harness/bundles/general/codex/harness-sync/SKILL.md +1 -1
  75. package/harness/bundles/general/codex/harness-test/SKILL.md +1 -1
  76. package/harness/bundles/general/codex/scripts/harness_archive.py +282 -120
  77. package/harness/bundles/general/codex/scripts/harness_events.py +400 -24
  78. package/harness/bundles/general/codex/scripts/harness_gate.py +585 -21
  79. package/harness/bundles/general/codex/scripts/harness_integration.py +403 -18
  80. package/harness/bundles/general/codex/scripts/harness_ledger.py +225 -29
  81. package/harness/bundles/general/codex/scripts/harness_phase.py +926 -0
  82. package/harness/bundles/general/codex/scripts/harness_plan_finalize.py +361 -0
  83. package/harness/bundles/general/codex/scripts/harness_profile.py +50 -0
  84. package/harness/bundles/general/codex/scripts/harness_runtime.py +248 -0
  85. package/harness/bundles/general/codex/scripts/harness_state.py +15 -2
  86. package/harness/bundles/general/codex/scripts/harness_test_guard.py +188 -0
  87. package/harness/bundles/general/codex/scripts/harness_workflow_policy.py +19 -0
  88. package/harness/bundles/general/cursor/.harness-build.json +1 -1
  89. package/harness/bundles/general/cursor/contracts/ci-metrics.schema.json +36 -0
  90. package/harness/bundles/general/cursor/contracts/workflow-policy.json +30 -0
  91. package/harness/bundles/general/cursor/harness-archive/SKILL.md +1 -1
  92. package/harness/bundles/general/cursor/harness-codebase-map/SKILL.md +1 -1
  93. package/harness/bundles/general/cursor/harness-knowledge-ingest/SKILL.md +1 -1
  94. package/harness/bundles/general/cursor/harness-knowledge-query/SKILL.md +1 -1
  95. package/harness/bundles/general/cursor/harness-plan/SKILL.md +4 -4
  96. package/harness/bundles/general/cursor/harness-plan/checklist.md +6 -0
  97. package/harness/bundles/general/cursor/harness-plan/reference.md +7 -2
  98. package/harness/bundles/general/cursor/harness-review/SKILL.md +1 -1
  99. package/harness/bundles/general/cursor/harness-run/SKILL.md +1 -1
  100. package/harness/bundles/general/cursor/harness-run/checklist.md +3 -2
  101. package/harness/bundles/general/cursor/harness-run/reference.md +6 -4
  102. package/harness/bundles/general/cursor/harness-submit/SKILL.md +1 -1
  103. package/harness/bundles/general/cursor/harness-sync/SKILL.md +1 -1
  104. package/harness/bundles/general/cursor/harness-test/SKILL.md +1 -1
  105. package/harness/bundles/general/cursor/scripts/harness_archive.py +282 -120
  106. package/harness/bundles/general/cursor/scripts/harness_events.py +400 -24
  107. package/harness/bundles/general/cursor/scripts/harness_gate.py +585 -21
  108. package/harness/bundles/general/cursor/scripts/harness_integration.py +403 -18
  109. package/harness/bundles/general/cursor/scripts/harness_ledger.py +225 -29
  110. package/harness/bundles/general/cursor/scripts/harness_phase.py +926 -0
  111. package/harness/bundles/general/cursor/scripts/harness_plan_finalize.py +361 -0
  112. package/harness/bundles/general/cursor/scripts/harness_profile.py +50 -0
  113. package/harness/bundles/general/cursor/scripts/harness_runtime.py +248 -0
  114. package/harness/bundles/general/cursor/scripts/harness_state.py +15 -2
  115. package/harness/bundles/general/cursor/scripts/harness_test_guard.py +188 -0
  116. package/harness/bundles/general/cursor/scripts/harness_workflow_policy.py +19 -0
  117. package/harness/bundles/java/claude-code/.harness-build.json +1 -1
  118. package/harness/bundles/java/claude-code/contracts/ci-metrics.schema.json +36 -0
  119. package/harness/bundles/java/claude-code/contracts/workflow-policy.json +30 -0
  120. package/harness/bundles/java/claude-code/harness-apidoc/SKILL.md +1 -1
  121. package/harness/bundles/java/claude-code/harness-archive/SKILL.md +1 -1
  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 +1 -1
  124. package/harness/bundles/java/claude-code/harness-knowledge-query/SKILL.md +1 -1
  125. package/harness/bundles/java/claude-code/harness-package/SKILL.md +1 -1
  126. package/harness/bundles/java/claude-code/harness-plan/SKILL.md +4 -4
  127. package/harness/bundles/java/claude-code/harness-plan/checklist.md +6 -0
  128. package/harness/bundles/java/claude-code/harness-plan/reference.md +7 -2
  129. package/harness/bundles/java/claude-code/harness-review/SKILL.md +1 -1
  130. package/harness/bundles/java/claude-code/harness-run/SKILL.md +1 -1
  131. package/harness/bundles/java/claude-code/harness-submit/SKILL.md +1 -1
  132. package/harness/bundles/java/claude-code/harness-sync/SKILL.md +1 -1
  133. package/harness/bundles/java/claude-code/harness-test/SKILL.md +1 -1
  134. package/harness/bundles/java/claude-code/scripts/harness_archive.py +282 -120
  135. package/harness/bundles/java/claude-code/scripts/harness_events.py +400 -24
  136. package/harness/bundles/java/claude-code/scripts/harness_gate.py +585 -21
  137. package/harness/bundles/java/claude-code/scripts/harness_integration.py +403 -18
  138. package/harness/bundles/java/claude-code/scripts/harness_ledger.py +225 -29
  139. package/harness/bundles/java/claude-code/scripts/harness_phase.py +926 -0
  140. package/harness/bundles/java/claude-code/scripts/harness_plan_finalize.py +361 -0
  141. package/harness/bundles/java/claude-code/scripts/harness_profile.py +50 -0
  142. package/harness/bundles/java/claude-code/scripts/harness_runtime.py +248 -0
  143. package/harness/bundles/java/claude-code/scripts/harness_state.py +15 -2
  144. package/harness/bundles/java/claude-code/scripts/harness_test_guard.py +188 -0
  145. package/harness/bundles/java/claude-code/scripts/harness_workflow_policy.py +19 -0
  146. package/harness/bundles/java/codebuddy/.harness-build.json +1 -1
  147. package/harness/bundles/java/codebuddy/contracts/ci-metrics.schema.json +36 -0
  148. package/harness/bundles/java/codebuddy/contracts/workflow-policy.json +30 -0
  149. package/harness/bundles/java/codebuddy/harness-apidoc/SKILL.md +1 -1
  150. package/harness/bundles/java/codebuddy/harness-archive/SKILL.md +1 -1
  151. package/harness/bundles/java/codebuddy/harness-codebase-map/SKILL.md +1 -1
  152. package/harness/bundles/java/codebuddy/harness-knowledge-ingest/SKILL.md +1 -1
  153. package/harness/bundles/java/codebuddy/harness-knowledge-query/SKILL.md +1 -1
  154. package/harness/bundles/java/codebuddy/harness-package/SKILL.md +1 -1
  155. package/harness/bundles/java/codebuddy/harness-plan/SKILL.md +4 -4
  156. package/harness/bundles/java/codebuddy/harness-plan/checklist.md +6 -0
  157. package/harness/bundles/java/codebuddy/harness-plan/reference.md +7 -2
  158. package/harness/bundles/java/codebuddy/harness-review/SKILL.md +1 -1
  159. package/harness/bundles/java/codebuddy/harness-run/SKILL.md +1 -1
  160. package/harness/bundles/java/codebuddy/harness-submit/SKILL.md +1 -1
  161. package/harness/bundles/java/codebuddy/harness-sync/SKILL.md +1 -1
  162. package/harness/bundles/java/codebuddy/harness-test/SKILL.md +1 -1
  163. package/harness/bundles/java/codebuddy/scripts/harness_archive.py +282 -120
  164. package/harness/bundles/java/codebuddy/scripts/harness_events.py +400 -24
  165. package/harness/bundles/java/codebuddy/scripts/harness_gate.py +585 -21
  166. package/harness/bundles/java/codebuddy/scripts/harness_integration.py +403 -18
  167. package/harness/bundles/java/codebuddy/scripts/harness_ledger.py +225 -29
  168. package/harness/bundles/java/codebuddy/scripts/harness_phase.py +926 -0
  169. package/harness/bundles/java/codebuddy/scripts/harness_plan_finalize.py +361 -0
  170. package/harness/bundles/java/codebuddy/scripts/harness_profile.py +50 -0
  171. package/harness/bundles/java/codebuddy/scripts/harness_runtime.py +248 -0
  172. package/harness/bundles/java/codebuddy/scripts/harness_state.py +15 -2
  173. package/harness/bundles/java/codebuddy/scripts/harness_test_guard.py +188 -0
  174. package/harness/bundles/java/codebuddy/scripts/harness_workflow_policy.py +19 -0
  175. package/harness/bundles/java/codex/.harness-build.json +1 -1
  176. package/harness/bundles/java/codex/contracts/ci-metrics.schema.json +36 -0
  177. package/harness/bundles/java/codex/contracts/workflow-policy.json +30 -0
  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-codebase-map/SKILL.md +1 -1
  181. package/harness/bundles/java/codex/harness-knowledge-ingest/SKILL.md +1 -1
  182. package/harness/bundles/java/codex/harness-knowledge-query/SKILL.md +1 -1
  183. package/harness/bundles/java/codex/harness-package/SKILL.md +1 -1
  184. package/harness/bundles/java/codex/harness-plan/SKILL.md +4 -4
  185. package/harness/bundles/java/codex/harness-plan/checklist.md +6 -0
  186. package/harness/bundles/java/codex/harness-plan/reference.md +7 -2
  187. package/harness/bundles/java/codex/harness-review/SKILL.md +1 -1
  188. package/harness/bundles/java/codex/harness-run/SKILL.md +1 -1
  189. package/harness/bundles/java/codex/harness-submit/SKILL.md +1 -1
  190. package/harness/bundles/java/codex/harness-sync/SKILL.md +1 -1
  191. package/harness/bundles/java/codex/harness-test/SKILL.md +1 -1
  192. package/harness/bundles/java/codex/scripts/harness_archive.py +282 -120
  193. package/harness/bundles/java/codex/scripts/harness_events.py +400 -24
  194. package/harness/bundles/java/codex/scripts/harness_gate.py +585 -21
  195. package/harness/bundles/java/codex/scripts/harness_integration.py +403 -18
  196. package/harness/bundles/java/codex/scripts/harness_ledger.py +225 -29
  197. package/harness/bundles/java/codex/scripts/harness_phase.py +926 -0
  198. package/harness/bundles/java/codex/scripts/harness_plan_finalize.py +361 -0
  199. package/harness/bundles/java/codex/scripts/harness_profile.py +50 -0
  200. package/harness/bundles/java/codex/scripts/harness_runtime.py +248 -0
  201. package/harness/bundles/java/codex/scripts/harness_state.py +15 -2
  202. package/harness/bundles/java/codex/scripts/harness_test_guard.py +188 -0
  203. package/harness/bundles/java/codex/scripts/harness_workflow_policy.py +19 -0
  204. package/harness/bundles/java/cursor/.harness-build.json +1 -1
  205. package/harness/bundles/java/cursor/contracts/ci-metrics.schema.json +36 -0
  206. package/harness/bundles/java/cursor/contracts/workflow-policy.json +30 -0
  207. package/harness/bundles/java/cursor/harness-apidoc/SKILL.md +1 -1
  208. package/harness/bundles/java/cursor/harness-archive/SKILL.md +1 -1
  209. package/harness/bundles/java/cursor/harness-codebase-map/SKILL.md +1 -1
  210. package/harness/bundles/java/cursor/harness-knowledge-ingest/SKILL.md +1 -1
  211. package/harness/bundles/java/cursor/harness-knowledge-query/SKILL.md +1 -1
  212. package/harness/bundles/java/cursor/harness-package/SKILL.md +1 -1
  213. package/harness/bundles/java/cursor/harness-plan/SKILL.md +4 -4
  214. package/harness/bundles/java/cursor/harness-plan/checklist.md +6 -0
  215. package/harness/bundles/java/cursor/harness-plan/reference.md +7 -2
  216. package/harness/bundles/java/cursor/harness-review/SKILL.md +1 -1
  217. package/harness/bundles/java/cursor/harness-run/SKILL.md +1 -1
  218. package/harness/bundles/java/cursor/harness-submit/SKILL.md +1 -1
  219. package/harness/bundles/java/cursor/harness-sync/SKILL.md +1 -1
  220. package/harness/bundles/java/cursor/harness-test/SKILL.md +1 -1
  221. package/harness/bundles/java/cursor/scripts/harness_archive.py +282 -120
  222. package/harness/bundles/java/cursor/scripts/harness_events.py +400 -24
  223. package/harness/bundles/java/cursor/scripts/harness_gate.py +585 -21
  224. package/harness/bundles/java/cursor/scripts/harness_integration.py +403 -18
  225. package/harness/bundles/java/cursor/scripts/harness_ledger.py +225 -29
  226. package/harness/bundles/java/cursor/scripts/harness_phase.py +926 -0
  227. package/harness/bundles/java/cursor/scripts/harness_plan_finalize.py +361 -0
  228. package/harness/bundles/java/cursor/scripts/harness_profile.py +50 -0
  229. package/harness/bundles/java/cursor/scripts/harness_runtime.py +248 -0
  230. package/harness/bundles/java/cursor/scripts/harness_state.py +15 -2
  231. package/harness/bundles/java/cursor/scripts/harness_test_guard.py +188 -0
  232. package/harness/bundles/java/cursor/scripts/harness_workflow_policy.py +19 -0
  233. package/harness/manifests/general/claude-code.json +42 -26
  234. package/harness/manifests/general/codebuddy.json +42 -26
  235. package/harness/manifests/general/codex.json +42 -26
  236. package/harness/manifests/general/cursor.json +42 -26
  237. package/harness/manifests/java/claude-code.json +42 -26
  238. package/harness/manifests/java/codebuddy.json +42 -26
  239. package/harness/manifests/java/codex.json +42 -26
  240. package/harness/manifests/java/cursor.json +42 -26
  241. package/hunter-workflow-family.json +2 -2
  242. package/package.json +1 -1
@@ -12,10 +12,13 @@ Python 3.10+, stdlib only. UTF-8 without BOM. Windows path safe.
12
12
  from __future__ import annotations
13
13
 
14
14
  import argparse
15
+ import copy
15
16
  import contextlib
16
17
  import datetime as dt
18
+ import hashlib
17
19
  import json
18
20
  import os
21
+ import re
19
22
  import sys
20
23
  import tempfile
21
24
  import time
@@ -44,7 +47,9 @@ EVENT_TYPES = frozenset(
44
47
  "verification",
45
48
  "artifact",
46
49
  "issue",
50
+ "issue.resolve",
47
51
  "decision",
52
+ "correction",
48
53
  }
49
54
  )
50
55
 
@@ -69,6 +74,12 @@ OPTIONAL_FIELDS = (
69
74
  "message",
70
75
  "decision",
71
76
  "reason",
77
+ "issue_id",
78
+ "scope",
79
+ "target_event_id",
80
+ "target_field",
81
+ "old_value_hash",
82
+ "new_value_json",
72
83
  "run_id",
73
84
  "attempt",
74
85
  "executor_tool",
@@ -76,6 +87,78 @@ OPTIONAL_FIELDS = (
76
87
  "executor_model",
77
88
  "handoff_from_tool",
78
89
  "handoff_reason",
90
+ "trace_id",
91
+ "span_id",
92
+ "parent_span_id",
93
+ "runner_ms",
94
+ "orchestration_active_ms",
95
+ "wall_clock_ms",
96
+ "user_wait_ms",
97
+ )
98
+
99
+ _PROVENANCE_FIELDS = frozenset(
100
+ {
101
+ "run_id",
102
+ "attempt",
103
+ "executor_tool",
104
+ "executor_agent",
105
+ "executor_model",
106
+ "handoff_from_tool",
107
+ "handoff_reason",
108
+ "trace_id",
109
+ "span_id",
110
+ "parent_span_id",
111
+ "runner_ms",
112
+ "orchestration_active_ms",
113
+ "wall_clock_ms",
114
+ "user_wait_ms",
115
+ }
116
+ )
117
+ _EVENT_ALLOWED_FIELDS = {
118
+ "phase.start": frozenset({"note"}) | _PROVENANCE_FIELDS,
119
+ "phase.end": frozenset(
120
+ {"status", "duration_ms", "note", "reason", "issue_id"}
121
+ )
122
+ | _PROVENANCE_FIELDS,
123
+ "command": frozenset({"command", "exit_code", "duration_ms", "note"})
124
+ | _PROVENANCE_FIELDS,
125
+ "verification": frozenset(
126
+ {"name", "status", "reason", "command", "exit_code", "duration_ms", "note"}
127
+ )
128
+ | _PROVENANCE_FIELDS,
129
+ "artifact": frozenset({"path", "kind", "note"}) | _PROVENANCE_FIELDS,
130
+ "issue": frozenset({"code", "scope", "severity", "message", "reason", "note"})
131
+ | frozenset({"issue_id"})
132
+ | _PROVENANCE_FIELDS,
133
+ "issue.resolve": frozenset({"issue_id", "reason", "note"})
134
+ | _PROVENANCE_FIELDS,
135
+ "decision": frozenset({"decision", "reason", "note"}) | _PROVENANCE_FIELDS,
136
+ "correction": frozenset(
137
+ {
138
+ "target_event_id",
139
+ "target_field",
140
+ "old_value_hash",
141
+ "new_value_json",
142
+ "reason",
143
+ "note",
144
+ }
145
+ )
146
+ | _PROVENANCE_FIELDS,
147
+ }
148
+ _EVENT_REQUIRED_FIELDS = {
149
+ "issue": ("severity",),
150
+ "issue.resolve": ("issue_id", "reason"),
151
+ "verification": ("name", "status"),
152
+ "correction": (
153
+ "target_event_id",
154
+ "target_field",
155
+ "old_value_hash",
156
+ "new_value_json",
157
+ "reason",
158
+ ),
159
+ }
160
+ _CORRECTION_PROTECTED_FIELDS = frozenset(
161
+ {"schema_version", "id", "timestamp", "phase", "type"}
79
162
  )
80
163
 
81
164
 
@@ -92,8 +175,16 @@ def emit_json(payload: dict[str, Any], *, as_json: bool) -> None:
92
175
  sys.stdout.write(f"{msg}\n")
93
176
 
94
177
 
95
- def emit_error(message: str, *, as_json: bool, code: int = 1) -> int:
178
+ def emit_error(
179
+ message: str,
180
+ *,
181
+ as_json: bool,
182
+ code: int = 1,
183
+ error_code: str | None = None,
184
+ ) -> int:
96
185
  payload = {"ok": False, "error": message}
186
+ if error_code:
187
+ payload["code"] = error_code
97
188
  if as_json:
98
189
  sys.stderr.write(json.dumps(payload, ensure_ascii=False) + "\n")
99
190
  else:
@@ -165,11 +256,109 @@ def normalize_event(raw: dict[str, Any]) -> dict[str, Any]:
165
256
  except (TypeError, ValueError):
166
257
  version_int = 1
167
258
  event["schema_version"] = version_int
259
+ if version_int < SCHEMA_VERSION and "schemaValidation" not in event:
260
+ event["schemaValidation"] = "legacy"
168
261
  if "note" not in event or event["note"] is None:
169
262
  event["note"] = ""
170
263
  return event
171
264
 
172
265
 
266
+ def canonical_value_hash(value: Any) -> str:
267
+ """Return the stable optimistic-concurrency hash for a corrected value."""
268
+ payload = json.dumps(
269
+ value,
270
+ ensure_ascii=False,
271
+ sort_keys=True,
272
+ separators=(",", ":"),
273
+ ).encode("utf-8")
274
+ return "sha256:" + hashlib.sha256(payload).hexdigest()
275
+
276
+
277
+ def apply_event_corrections(events: list[dict[str, Any]]) -> list[dict[str, Any]]:
278
+ """Project append-only correction events without mutating event history.
279
+
280
+ Corrections may target only earlier events and use an old-value hash as an
281
+ optimistic-concurrency guard. The correction records remain in the raw
282
+ stream; projections return only the corrected domain events.
283
+ """
284
+ projected: list[dict[str, Any]] = []
285
+ by_id: dict[str, dict[str, Any]] = {}
286
+ for raw in events:
287
+ event = copy.deepcopy(raw)
288
+ if event.get("type") != "correction":
289
+ projected.append(event)
290
+ event_id = str(event.get("id") or "").strip()
291
+ if event_id:
292
+ by_id[event_id] = event
293
+ continue
294
+
295
+ correction_id = str(event.get("id") or "<unknown>")
296
+ target_id = str(event.get("target_event_id") or "").strip()
297
+ target = by_id.get(target_id)
298
+ if target is None:
299
+ raise ValueError(
300
+ f"CORRECTION_TARGET_NOT_FOUND: {correction_id} targets {target_id}"
301
+ )
302
+ field = str(event.get("target_field") or "").strip()
303
+ if not field or field in _CORRECTION_PROTECTED_FIELDS:
304
+ raise ValueError(
305
+ f"CORRECTION_FIELD_NOT_ALLOWED: {correction_id} targets {field}"
306
+ )
307
+ actual_hash = canonical_value_hash(target.get(field))
308
+ expected_hash = str(event.get("old_value_hash") or "").strip()
309
+ if actual_hash != expected_hash:
310
+ raise ValueError(
311
+ "CORRECTION_OLD_VALUE_MISMATCH: "
312
+ f"{correction_id} expected {expected_hash}, found {actual_hash}"
313
+ )
314
+ target[field] = copy.deepcopy(event.get("new_value"))
315
+ return projected
316
+
317
+
318
+ def _issue_identity(event: dict[str, Any]) -> str:
319
+ explicit = str(event.get("issue_id") or "").strip()
320
+ if explicit:
321
+ return explicit
322
+ code = str(event.get("code") or "").strip()
323
+ scope = str(event.get("scope") or "").strip()
324
+ if code:
325
+ return f"code:{code}|scope:{scope}"
326
+ return str(event.get("id") or "").strip()
327
+
328
+
329
+ def current_issues(events: list[dict[str, Any]]) -> list[dict[str, Any]]:
330
+ """Return unresolved issues while preserving the full append-only history."""
331
+ active: dict[str, dict[str, Any]] = {}
332
+ for event in apply_event_corrections(events):
333
+ event_type = event.get("type")
334
+ if event_type == "issue":
335
+ identity = _issue_identity(event)
336
+ if identity:
337
+ item = copy.deepcopy(event)
338
+ item["issue_id"] = identity
339
+ active[identity] = item
340
+ elif event_type == "issue.resolve":
341
+ active.pop(_issue_identity(event), None)
342
+ elif event_type == "phase.end" and str(event.get("status") or "").upper() == "OK":
343
+ identity = str(event.get("issue_id") or "").strip()
344
+ if not identity:
345
+ continue
346
+ phase = event.get("phase")
347
+ attempt = event.get("attempt")
348
+ issue = active.get(identity)
349
+ if issue is None or issue.get("phase") != phase:
350
+ continue
351
+ issue_attempt = issue.get("attempt")
352
+ if (
353
+ isinstance(attempt, int)
354
+ and isinstance(issue_attempt, int)
355
+ and issue_attempt >= attempt
356
+ ):
357
+ continue
358
+ active.pop(identity, None)
359
+ return list(active.values())
360
+
361
+
173
362
  def load_events(path: Path) -> list[dict[str, Any]]:
174
363
  if not path.exists():
175
364
  return []
@@ -329,7 +518,12 @@ def build_event(args: argparse.Namespace, existing: list[dict[str, Any]]) -> dic
329
518
  value = getattr(args, field, None)
330
519
  if value is None:
331
520
  continue
332
- event[field] = value
521
+ if field == "new_value_json":
522
+ event["new_value"] = json.loads(value)
523
+ else:
524
+ event[field] = value
525
+ if args.type == "issue" and not event.get("issue_id"):
526
+ event["issue_id"] = _issue_identity(event)
333
527
  environment_defaults = {
334
528
  "run_id": "HUNTER_HARNESS_RUN_ID",
335
529
  "executor_tool": "HUNTER_HARNESS_TOOL",
@@ -344,6 +538,61 @@ def build_event(args: argparse.Namespace, existing: list[dict[str, Any]]) -> dic
344
538
  return event
345
539
 
346
540
 
541
+ def validate_append_event(args: argparse.Namespace) -> tuple[str, str] | None:
542
+ """Validate type-specific append fields before any file is mutated."""
543
+ event_type = str(args.type)
544
+ for field in _EVENT_REQUIRED_FIELDS.get(event_type, ()):
545
+ value = getattr(args, field, None)
546
+ if value is None or (isinstance(value, str) and not value.strip()):
547
+ return (
548
+ "EVENT_REQUIRED_FIELD",
549
+ f"EVENT_REQUIRED_FIELD: {event_type} requires --{field.replace('_', '-')}",
550
+ )
551
+ allowed = _EVENT_ALLOWED_FIELDS[event_type]
552
+ for field in OPTIONAL_FIELDS:
553
+ value = getattr(args, field, None)
554
+ if value is not None and field not in allowed:
555
+ return (
556
+ "EVENT_FIELD_NOT_ALLOWED",
557
+ "EVENT_FIELD_NOT_ALLOWED: "
558
+ f"{event_type} does not accept --{field.replace('_', '-')}",
559
+ )
560
+ for field, length in (("trace_id", 32), ("span_id", 16), ("parent_span_id", 16)):
561
+ value = getattr(args, field, None)
562
+ if value is not None and not re.fullmatch(rf"[0-9a-f]{{{length}}}", str(value)):
563
+ return (
564
+ "EVENT_TRACE_FIELD_INVALID",
565
+ f"EVENT_TRACE_FIELD_INVALID: --{field.replace('_', '-')} must be {length} lowercase hex characters",
566
+ )
567
+ for field in (
568
+ "runner_ms",
569
+ "orchestration_active_ms",
570
+ "wall_clock_ms",
571
+ "user_wait_ms",
572
+ ):
573
+ value = getattr(args, field, None)
574
+ if value is not None and (not isinstance(value, int) or value < 0):
575
+ return (
576
+ "EVENT_TIMING_FIELD_INVALID",
577
+ f"EVENT_TIMING_FIELD_INVALID: --{field.replace('_', '-')} must be a nonnegative integer",
578
+ )
579
+ if event_type == "correction":
580
+ target_field = str(getattr(args, "target_field", "") or "").strip()
581
+ if target_field in _CORRECTION_PROTECTED_FIELDS:
582
+ return (
583
+ "CORRECTION_FIELD_NOT_ALLOWED",
584
+ f"CORRECTION_FIELD_NOT_ALLOWED: cannot correct {target_field}",
585
+ )
586
+ try:
587
+ json.loads(str(getattr(args, "new_value_json", "")))
588
+ except json.JSONDecodeError as exc:
589
+ return (
590
+ "CORRECTION_VALUE_INVALID_JSON",
591
+ f"CORRECTION_VALUE_INVALID_JSON: {exc.msg}",
592
+ )
593
+ return None
594
+
595
+
347
596
  def status_symbol(status: Any, reason: Any = None) -> str:
348
597
  text = str(status or "").strip().lower()
349
598
  reason_text = str(reason or "").strip()
@@ -475,21 +724,19 @@ def phase_duration_ms(phase_events: list[dict[str, Any]]) -> int | None:
475
724
 
476
725
 
477
726
  def late_event_stats(phase_events: list[dict[str, Any]]) -> dict[str, int]:
478
- """Count events recorded after the closing phase.end (RET-21)."""
479
- end_ts = None
480
- for event in phase_events:
727
+ """Count events recorded after the final closing phase.end (RET-21)."""
728
+ final_end_index = None
729
+ for index, event in enumerate(phase_events):
481
730
  if event.get("type") == "phase.end":
482
- end_ts = event.get("timestamp")
483
- if not end_ts:
731
+ final_end_index = index
732
+ if final_end_index is None:
484
733
  return {"lateEventCount": 0, "lateEventSpanMs": 0}
485
- late_stamps = []
486
- seen_end = False
487
- for event in phase_events:
488
- if event.get("type") == "phase.end" and not seen_end:
489
- seen_end = True
490
- continue
491
- if seen_end and event.get("timestamp"):
492
- late_stamps.append(event["timestamp"])
734
+ end_ts = phase_events[final_end_index].get("timestamp")
735
+ late_stamps = [
736
+ event["timestamp"]
737
+ for event in phase_events[final_end_index + 1 :]
738
+ if event.get("timestamp")
739
+ ]
493
740
  if not late_stamps:
494
741
  return {"lateEventCount": 0, "lateEventSpanMs": 0}
495
742
  span = duration_ms_between(end_ts, late_stamps[-1]) or 0
@@ -523,6 +770,20 @@ def attempt_invocations(phase_events: list[dict[str, Any]]) -> list[dict[str, An
523
770
  return invocations
524
771
 
525
772
 
773
+ def phase_final_state(phase_events: list[dict[str, Any]]) -> dict[str, Any]:
774
+ """Reduce a phase to the state of its latest attempt."""
775
+ invocations = attempt_invocations(phase_events)
776
+ if not invocations:
777
+ return {"attempt": None, "status": None, "durationMs": None, "closed": False}
778
+ latest = invocations[-1]
779
+ return {
780
+ "attempt": latest.get("attempt"),
781
+ "status": latest.get("status"),
782
+ "durationMs": latest.get("durationMs"),
783
+ "closed": latest.get("status") is not None,
784
+ }
785
+
786
+
526
787
  def canonical_phase_timing(phase_events: list[dict[str, Any]]) -> dict[str, Any]:
527
788
  """Single reducer for every duration view (RET-20).
528
789
 
@@ -530,7 +791,12 @@ def canonical_phase_timing(phase_events: list[dict[str, Any]]) -> dict[str, Any]
530
791
  wallClockSpanMs: first → last timestamp in the bucket, late events included.
531
792
  """
532
793
  stamps = [e.get("timestamp") for e in phase_events if e.get("timestamp")]
533
- active = phase_duration_ms(phase_events)
794
+ invocation_durations = [
795
+ invocation["durationMs"]
796
+ for invocation in attempt_invocations(phase_events)
797
+ if invocation.get("durationMs") is not None
798
+ ]
799
+ active = sum(invocation_durations) if invocation_durations else None
534
800
  late = late_event_stats(phase_events)
535
801
  wall = duration_ms_between(stamps[0], stamps[-1]) if len(stamps) >= 2 else active
536
802
  return {
@@ -639,6 +905,18 @@ def render_event_line(event: dict[str, Any]) -> list[str]:
639
905
  code = event.get("code")
640
906
  prefix = f"[{code}] " if code else ""
641
907
  return [f"- issue: {prefix}{symbol}"]
908
+ if etype == "issue.resolve":
909
+ issue_id = str(event.get("issue_id") or "").strip()
910
+ reason = str(event.get("reason") or event.get("note") or "").strip()
911
+ suffix = f" — {reason}" if reason else ""
912
+ return [f"- issue.resolve: {issue_id}{suffix}"] if issue_id else []
913
+ if etype == "correction":
914
+ target = str(event.get("target_event_id") or "").strip()
915
+ field = str(event.get("target_field") or "").strip()
916
+ reason = str(event.get("reason") or event.get("note") or "").strip()
917
+ suffix = f" — {reason}" if reason else ""
918
+ label = f"{target}.{field}".strip(".")
919
+ return [f"- correction: {label}{suffix}"] if label else []
642
920
  if etype == "artifact":
643
921
  path = str(event.get("path") or "").strip()
644
922
  note = str(event.get("note") or "").strip()
@@ -666,7 +944,14 @@ def render_execution_log(events: list[dict[str, Any]]) -> str:
666
944
  lines.append("")
667
945
  return "\n".join(lines)
668
946
 
669
- for phase, phase_events in group_events_by_phase(events):
947
+ projected_domain_events = iter(apply_event_corrections(events))
948
+ projected_events = [
949
+ copy.deepcopy(event)
950
+ if event.get("type") == "correction"
951
+ else next(projected_domain_events)
952
+ for event in events
953
+ ]
954
+ for phase, phase_events in group_events_by_phase(projected_events):
670
955
  attempts = split_phase_attempts(phase_events)
671
956
  for attempt_record in attempts:
672
957
  attempt_events = attempt_record["events"]
@@ -710,8 +995,9 @@ def write_execution_log(change_dir: Path, content: str) -> Path:
710
995
  def build_summary(change_dir: Path, events: list[dict[str, Any]]) -> dict[str, Any]:
711
996
  phases: dict[str, Any] = {}
712
997
  issues: list[dict[str, Any]] = []
998
+ projected_events = apply_event_corrections(events)
713
999
 
714
- for phase, phase_events in group_events_by_phase(events):
1000
+ for phase, phase_events in group_events_by_phase(projected_events):
715
1001
  attempt_records: list[dict[str, Any]] = []
716
1002
  for record in split_phase_attempts(phase_events):
717
1003
  attempt_events = record["events"]
@@ -745,12 +1031,13 @@ def build_summary(change_dir: Path, events: list[dict[str, Any]]) -> dict[str, A
745
1031
  "attempts": attempt_records,
746
1032
  }
747
1033
 
748
- for event in events:
1034
+ for event in projected_events:
749
1035
  if event.get("type") != "issue":
750
1036
  continue
751
1037
  issues.append(
752
1038
  {
753
1039
  "id": event.get("id"),
1040
+ "issue_id": _issue_identity(event),
754
1041
  "timestamp": event.get("timestamp"),
755
1042
  "phase": event.get("phase"),
756
1043
  "code": event.get("code"),
@@ -765,9 +1052,42 @@ def build_summary(change_dir: Path, events: list[dict[str, Any]]) -> dict[str, A
765
1052
  "event_count": len(events),
766
1053
  "phases": phases,
767
1054
  "issues": issues,
1055
+ "current_issues": [
1056
+ {
1057
+ "id": event.get("id"),
1058
+ "issue_id": _issue_identity(event),
1059
+ "timestamp": event.get("timestamp"),
1060
+ "phase": event.get("phase"),
1061
+ "code": event.get("code"),
1062
+ "severity": event.get("severity"),
1063
+ "message": event.get("message"),
1064
+ }
1065
+ for event in current_issues(events)
1066
+ ],
768
1067
  }
769
1068
 
770
1069
 
1070
+ def phase_end_already_recorded(
1071
+ events: list[dict[str, Any]], candidate: dict[str, Any]
1072
+ ) -> bool:
1073
+ """Return whether the candidate attempt already has a terminal event."""
1074
+ phase_events = [
1075
+ event for event in events if event.get("phase") == candidate.get("phase")
1076
+ ]
1077
+ candidate_attempt = candidate.get("attempt")
1078
+ if isinstance(candidate_attempt, int):
1079
+ return any(
1080
+ event.get("type") == "phase.end"
1081
+ and event.get("attempt") == candidate_attempt
1082
+ for event in phase_events
1083
+ )
1084
+ attempts = split_phase_attempts(phase_events)
1085
+ if not attempts:
1086
+ return False
1087
+ latest_events = attempts[-1].get("events") or []
1088
+ return any(event.get("type") == "phase.end" for event in latest_events)
1089
+
1090
+
771
1091
  def cmd_append(args: argparse.Namespace) -> int:
772
1092
  as_json = bool(args.json)
773
1093
  if args.type not in EVENT_TYPES:
@@ -775,6 +1095,13 @@ def cmd_append(args: argparse.Namespace) -> int:
775
1095
  f"unsupported type: {args.type}; expected one of {sorted(EVENT_TYPES)}",
776
1096
  as_json=as_json,
777
1097
  )
1098
+ legacy_lenient = bool(getattr(args, "legacy_lenient", False))
1099
+ validation = validate_append_event(args)
1100
+ if validation:
1101
+ error_code, message = validation
1102
+ legacy_compatible = legacy_lenient and args.type in {"issue", "verification"}
1103
+ if not legacy_compatible:
1104
+ return emit_error(message, as_json=as_json, error_code=error_code)
778
1105
  change_dir = resolve_change_dir(args.change_dir)
779
1106
  archived = archived_change_dir(change_dir)
780
1107
  if archived is not None:
@@ -790,24 +1117,55 @@ def cmd_append(args: argparse.Namespace) -> int:
790
1117
  # new_event_id 用完整 uuid,无需扫描去重。锁覆盖 atomic_append_line 的
791
1118
  # open/write/flush/fsync 全过程。
792
1119
  event = build_event(args, [])
793
- # Semantic hints (non-fatal): guide agents toward required fields.
794
- if args.type == "issue" and not args.severity:
1120
+ # Explicit compatibility mode preserves old append behavior and marks the
1121
+ # resulting event so strict consumers can distinguish it from valid v3.
1122
+ if validation:
1123
+ event["schemaValidation"] = "legacy"
1124
+ if legacy_lenient and args.type == "issue" and not args.severity:
795
1125
  event["severity"] = "info"
796
1126
  print(
797
1127
  "warning: issue without --severity, defaulted to info",
798
1128
  file=sys.stderr,
799
1129
  )
800
- if args.type == "verification" and (not args.name or not args.status):
1130
+ if legacy_lenient and args.type == "verification" and (not args.name or not args.status):
801
1131
  print(
802
1132
  "warning: verification missing --name or --status",
803
1133
  file=sys.stderr,
804
1134
  )
805
1135
  line = json.dumps(event, ensure_ascii=False, separators=(",", ":"))
1136
+ phase_closed = False
1137
+ projection_error: str | None = None
806
1138
  try:
807
1139
  with event_file_lock(lock_path):
808
- atomic_append_line(path, line)
809
- except (OSError, TimeoutError) as exc:
1140
+ existing_events = (
1141
+ load_events(path)
1142
+ if args.type in {"phase.end", "correction"}
1143
+ else []
1144
+ )
1145
+ if args.type == "phase.end":
1146
+ phase_closed = phase_end_already_recorded(existing_events, event)
1147
+ elif args.type == "correction":
1148
+ try:
1149
+ apply_event_corrections([*existing_events, event])
1150
+ except ValueError as exc:
1151
+ projection_error = str(exc)
1152
+ if not phase_closed and projection_error is None:
1153
+ atomic_append_line(path, line)
1154
+ except (OSError, TimeoutError, ValueError) as exc:
810
1155
  return emit_error(f"append failed: {exc}", as_json=as_json)
1156
+ if phase_closed:
1157
+ return emit_error(
1158
+ "PHASE_ALREADY_CLOSED: refusing a second phase.end for the same attempt",
1159
+ as_json=as_json,
1160
+ error_code="PHASE_ALREADY_CLOSED",
1161
+ )
1162
+ if projection_error is not None:
1163
+ error_code = projection_error.split(":", 1)[0]
1164
+ return emit_error(
1165
+ projection_error,
1166
+ as_json=as_json,
1167
+ error_code=error_code,
1168
+ )
811
1169
 
812
1170
  # §6.1: phase.end append -> 追加成功后执行一次 render(从完整 events 重建 log)。
813
1171
  # 普通 command/issue 等 append 不渲染(O(1));显式 `render` 子命令随时重建。
@@ -911,6 +1269,12 @@ def build_parser() -> argparse.ArgumentParser:
911
1269
  p_append.add_argument("--message", default=None)
912
1270
  p_append.add_argument("--decision", default=None)
913
1271
  p_append.add_argument("--reason", default=None)
1272
+ p_append.add_argument("--issue-id", default=None)
1273
+ p_append.add_argument("--scope", default=None)
1274
+ p_append.add_argument("--target-event-id", default=None)
1275
+ p_append.add_argument("--target-field", default=None)
1276
+ p_append.add_argument("--old-value-hash", default=None)
1277
+ p_append.add_argument("--new-value-json", default=None)
914
1278
  p_append.add_argument("--run-id", default=None)
915
1279
  p_append.add_argument("--attempt", type=int, default=None)
916
1280
  p_append.add_argument("--executor-tool", default=None)
@@ -918,6 +1282,18 @@ def build_parser() -> argparse.ArgumentParser:
918
1282
  p_append.add_argument("--executor-model", default=None)
919
1283
  p_append.add_argument("--handoff-from-tool", default=None)
920
1284
  p_append.add_argument("--handoff-reason", default=None)
1285
+ p_append.add_argument("--trace-id", default=None)
1286
+ p_append.add_argument("--span-id", default=None)
1287
+ p_append.add_argument("--parent-span-id", default=None)
1288
+ p_append.add_argument("--runner-ms", type=int, default=None)
1289
+ p_append.add_argument("--orchestration-active-ms", type=int, default=None)
1290
+ p_append.add_argument("--wall-clock-ms", type=int, default=None)
1291
+ p_append.add_argument("--user-wait-ms", type=int, default=None)
1292
+ p_append.add_argument(
1293
+ "--legacy-lenient",
1294
+ action="store_true",
1295
+ help="accept legacy incomplete/type-mismatched events and mark them explicitly",
1296
+ )
921
1297
  p_append.set_defaults(func=cmd_append)
922
1298
 
923
1299
  p_render = sub.add_parser(