@hunter-harness/workflow-harness 0.2.54 → 0.2.56

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (262) hide show
  1. package/harness/bundles/general/claude-code/.harness-build.json +1 -1
  2. package/harness/bundles/general/claude-code/README.md +3 -3
  3. package/harness/bundles/general/claude-code/contracts/workflow-policy.json +1 -1
  4. package/harness/bundles/general/claude-code/harness-archive/SKILL.md +21 -33
  5. package/harness/bundles/general/claude-code/harness-archive/checklist.md +6 -10
  6. package/harness/bundles/general/claude-code/harness-codebase-map/SKILL.md +1 -1
  7. package/harness/bundles/general/claude-code/harness-knowledge-ingest/SKILL.md +1 -1
  8. package/harness/bundles/general/claude-code/harness-knowledge-query/SKILL.md +1 -1
  9. package/harness/bundles/general/claude-code/harness-plan/SKILL.md +7 -3
  10. package/harness/bundles/general/claude-code/harness-plan/checklist.md +2 -1
  11. package/harness/bundles/general/claude-code/harness-review/SKILL.md +8 -4
  12. package/harness/bundles/general/claude-code/harness-run/SKILL.md +9 -7
  13. package/harness/bundles/general/claude-code/harness-run/checklist.md +1 -1
  14. package/harness/bundles/general/claude-code/harness-run/reference.md +3 -3
  15. package/harness/bundles/general/claude-code/harness-submit/SKILL.md +6 -6
  16. package/harness/bundles/general/claude-code/harness-submit/checklist.md +1 -1
  17. package/harness/bundles/general/claude-code/harness-sync/SKILL.md +1 -1
  18. package/harness/bundles/general/claude-code/harness-test/SKILL.md +8 -6
  19. package/harness/bundles/general/claude-code/harness-test/checklist.md +1 -1
  20. package/harness/bundles/general/claude-code/harness-test/reference.md +2 -2
  21. package/harness/bundles/general/claude-code/protocols/ledger-protocol.md +3 -3
  22. package/harness/bundles/general/claude-code/scripts/harness_archive.py +322 -74
  23. package/harness/bundles/general/claude-code/scripts/harness_context.py +236 -63
  24. package/harness/bundles/general/claude-code/scripts/harness_events.py +86 -5
  25. package/harness/bundles/general/claude-code/scripts/harness_events_sync.py +201 -17
  26. package/harness/bundles/general/claude-code/scripts/harness_fixback.py +189 -1
  27. package/harness/bundles/general/claude-code/scripts/harness_gate.py +353 -3
  28. package/harness/bundles/general/claude-code/scripts/harness_ledger.py +195 -11
  29. package/harness/bundles/general/claude-code/scripts/harness_plan_finalize.py +82 -0
  30. package/harness/bundles/general/claude-code/scripts/harness_test_guard.py +37 -44
  31. package/harness/bundles/general/codebuddy/.harness-build.json +1 -1
  32. package/harness/bundles/general/codebuddy/README.md +3 -3
  33. package/harness/bundles/general/codebuddy/contracts/workflow-policy.json +1 -1
  34. package/harness/bundles/general/codebuddy/harness-archive/SKILL.md +21 -33
  35. package/harness/bundles/general/codebuddy/harness-archive/checklist.md +6 -10
  36. package/harness/bundles/general/codebuddy/harness-codebase-map/SKILL.md +1 -1
  37. package/harness/bundles/general/codebuddy/harness-knowledge-ingest/SKILL.md +1 -1
  38. package/harness/bundles/general/codebuddy/harness-knowledge-query/SKILL.md +1 -1
  39. package/harness/bundles/general/codebuddy/harness-plan/SKILL.md +7 -3
  40. package/harness/bundles/general/codebuddy/harness-plan/checklist.md +2 -1
  41. package/harness/bundles/general/codebuddy/harness-review/SKILL.md +8 -4
  42. package/harness/bundles/general/codebuddy/harness-run/SKILL.md +9 -7
  43. package/harness/bundles/general/codebuddy/harness-run/checklist.md +1 -1
  44. package/harness/bundles/general/codebuddy/harness-run/reference.md +3 -3
  45. package/harness/bundles/general/codebuddy/harness-submit/SKILL.md +6 -6
  46. package/harness/bundles/general/codebuddy/harness-submit/checklist.md +1 -1
  47. package/harness/bundles/general/codebuddy/harness-sync/SKILL.md +1 -1
  48. package/harness/bundles/general/codebuddy/harness-test/SKILL.md +8 -6
  49. package/harness/bundles/general/codebuddy/harness-test/checklist.md +1 -1
  50. package/harness/bundles/general/codebuddy/harness-test/reference.md +2 -2
  51. package/harness/bundles/general/codebuddy/protocols/ledger-protocol.md +3 -3
  52. package/harness/bundles/general/codebuddy/scripts/harness_archive.py +322 -74
  53. package/harness/bundles/general/codebuddy/scripts/harness_context.py +236 -63
  54. package/harness/bundles/general/codebuddy/scripts/harness_events.py +86 -5
  55. package/harness/bundles/general/codebuddy/scripts/harness_events_sync.py +201 -17
  56. package/harness/bundles/general/codebuddy/scripts/harness_fixback.py +189 -1
  57. package/harness/bundles/general/codebuddy/scripts/harness_gate.py +353 -3
  58. package/harness/bundles/general/codebuddy/scripts/harness_ledger.py +195 -11
  59. package/harness/bundles/general/codebuddy/scripts/harness_plan_finalize.py +82 -0
  60. package/harness/bundles/general/codebuddy/scripts/harness_test_guard.py +37 -44
  61. package/harness/bundles/general/codex/.harness-build.json +1 -1
  62. package/harness/bundles/general/codex/README.md +3 -3
  63. package/harness/bundles/general/codex/contracts/workflow-policy.json +1 -1
  64. package/harness/bundles/general/codex/harness-archive/SKILL.md +21 -33
  65. package/harness/bundles/general/codex/harness-archive/checklist.md +6 -10
  66. package/harness/bundles/general/codex/harness-codebase-map/SKILL.md +1 -1
  67. package/harness/bundles/general/codex/harness-knowledge-ingest/SKILL.md +1 -1
  68. package/harness/bundles/general/codex/harness-knowledge-query/SKILL.md +1 -1
  69. package/harness/bundles/general/codex/harness-plan/SKILL.md +7 -3
  70. package/harness/bundles/general/codex/harness-plan/checklist.md +2 -1
  71. package/harness/bundles/general/codex/harness-review/SKILL.md +4 -4
  72. package/harness/bundles/general/codex/harness-run/SKILL.md +9 -7
  73. package/harness/bundles/general/codex/harness-run/checklist.md +1 -1
  74. package/harness/bundles/general/codex/harness-run/reference.md +3 -3
  75. package/harness/bundles/general/codex/harness-submit/SKILL.md +6 -6
  76. package/harness/bundles/general/codex/harness-submit/checklist.md +1 -1
  77. package/harness/bundles/general/codex/harness-sync/SKILL.md +1 -1
  78. package/harness/bundles/general/codex/harness-test/SKILL.md +8 -6
  79. package/harness/bundles/general/codex/harness-test/checklist.md +1 -1
  80. package/harness/bundles/general/codex/harness-test/reference.md +2 -2
  81. package/harness/bundles/general/codex/protocols/ledger-protocol.md +3 -3
  82. package/harness/bundles/general/codex/scripts/harness_archive.py +322 -74
  83. package/harness/bundles/general/codex/scripts/harness_context.py +236 -63
  84. package/harness/bundles/general/codex/scripts/harness_events.py +86 -5
  85. package/harness/bundles/general/codex/scripts/harness_events_sync.py +201 -17
  86. package/harness/bundles/general/codex/scripts/harness_fixback.py +189 -1
  87. package/harness/bundles/general/codex/scripts/harness_gate.py +353 -3
  88. package/harness/bundles/general/codex/scripts/harness_ledger.py +195 -11
  89. package/harness/bundles/general/codex/scripts/harness_plan_finalize.py +82 -0
  90. package/harness/bundles/general/codex/scripts/harness_test_guard.py +37 -44
  91. package/harness/bundles/general/cursor/.harness-build.json +1 -1
  92. package/harness/bundles/general/cursor/README.md +3 -3
  93. package/harness/bundles/general/cursor/contracts/workflow-policy.json +1 -1
  94. package/harness/bundles/general/cursor/harness-archive/SKILL.md +21 -33
  95. package/harness/bundles/general/cursor/harness-archive/checklist.md +6 -10
  96. package/harness/bundles/general/cursor/harness-codebase-map/SKILL.md +1 -1
  97. package/harness/bundles/general/cursor/harness-knowledge-ingest/SKILL.md +1 -1
  98. package/harness/bundles/general/cursor/harness-knowledge-query/SKILL.md +1 -1
  99. package/harness/bundles/general/cursor/harness-plan/SKILL.md +7 -3
  100. package/harness/bundles/general/cursor/harness-plan/checklist.md +2 -1
  101. package/harness/bundles/general/cursor/harness-review/SKILL.md +4 -4
  102. package/harness/bundles/general/cursor/harness-run/SKILL.md +9 -7
  103. package/harness/bundles/general/cursor/harness-run/checklist.md +1 -1
  104. package/harness/bundles/general/cursor/harness-run/reference.md +3 -3
  105. package/harness/bundles/general/cursor/harness-submit/SKILL.md +6 -6
  106. package/harness/bundles/general/cursor/harness-submit/checklist.md +1 -1
  107. package/harness/bundles/general/cursor/harness-sync/SKILL.md +1 -1
  108. package/harness/bundles/general/cursor/harness-test/SKILL.md +8 -6
  109. package/harness/bundles/general/cursor/harness-test/checklist.md +1 -1
  110. package/harness/bundles/general/cursor/harness-test/reference.md +2 -2
  111. package/harness/bundles/general/cursor/protocols/ledger-protocol.md +3 -3
  112. package/harness/bundles/general/cursor/scripts/harness_archive.py +322 -74
  113. package/harness/bundles/general/cursor/scripts/harness_context.py +236 -63
  114. package/harness/bundles/general/cursor/scripts/harness_events.py +86 -5
  115. package/harness/bundles/general/cursor/scripts/harness_events_sync.py +201 -17
  116. package/harness/bundles/general/cursor/scripts/harness_fixback.py +189 -1
  117. package/harness/bundles/general/cursor/scripts/harness_gate.py +353 -3
  118. package/harness/bundles/general/cursor/scripts/harness_ledger.py +195 -11
  119. package/harness/bundles/general/cursor/scripts/harness_plan_finalize.py +82 -0
  120. package/harness/bundles/general/cursor/scripts/harness_test_guard.py +37 -44
  121. package/harness/bundles/java/claude-code/.harness-build.json +1 -1
  122. package/harness/bundles/java/claude-code/README.md +3 -3
  123. package/harness/bundles/java/claude-code/contracts/workflow-policy.json +1 -1
  124. package/harness/bundles/java/claude-code/harness-apidoc/SKILL.md +1 -1
  125. package/harness/bundles/java/claude-code/harness-archive/SKILL.md +21 -33
  126. package/harness/bundles/java/claude-code/harness-archive/checklist.md +6 -10
  127. package/harness/bundles/java/claude-code/harness-codebase-map/SKILL.md +1 -1
  128. package/harness/bundles/java/claude-code/harness-knowledge-ingest/SKILL.md +1 -1
  129. package/harness/bundles/java/claude-code/harness-knowledge-query/SKILL.md +1 -1
  130. package/harness/bundles/java/claude-code/harness-package/SKILL.md +1 -1
  131. package/harness/bundles/java/claude-code/harness-package/checklist.md +1 -1
  132. package/harness/bundles/java/claude-code/harness-plan/SKILL.md +7 -3
  133. package/harness/bundles/java/claude-code/harness-plan/checklist.md +2 -1
  134. package/harness/bundles/java/claude-code/harness-review/SKILL.md +8 -4
  135. package/harness/bundles/java/claude-code/harness-run/SKILL.md +9 -7
  136. package/harness/bundles/java/claude-code/harness-run/checklist.md +1 -1
  137. package/harness/bundles/java/claude-code/harness-run/reference.md +2 -2
  138. package/harness/bundles/java/claude-code/harness-submit/SKILL.md +6 -6
  139. package/harness/bundles/java/claude-code/harness-submit/checklist.md +1 -1
  140. package/harness/bundles/java/claude-code/harness-sync/SKILL.md +1 -1
  141. package/harness/bundles/java/claude-code/harness-test/SKILL.md +8 -6
  142. package/harness/bundles/java/claude-code/harness-test/checklist.md +1 -1
  143. package/harness/bundles/java/claude-code/harness-test/reference.md +1 -1
  144. package/harness/bundles/java/claude-code/protocols/ledger-protocol.md +3 -3
  145. package/harness/bundles/java/claude-code/scripts/harness_archive.py +322 -74
  146. package/harness/bundles/java/claude-code/scripts/harness_context.py +236 -63
  147. package/harness/bundles/java/claude-code/scripts/harness_events.py +86 -5
  148. package/harness/bundles/java/claude-code/scripts/harness_events_sync.py +201 -17
  149. package/harness/bundles/java/claude-code/scripts/harness_fixback.py +189 -1
  150. package/harness/bundles/java/claude-code/scripts/harness_gate.py +353 -3
  151. package/harness/bundles/java/claude-code/scripts/harness_ledger.py +195 -11
  152. package/harness/bundles/java/claude-code/scripts/harness_plan_finalize.py +82 -0
  153. package/harness/bundles/java/claude-code/scripts/harness_test_guard.py +37 -44
  154. package/harness/bundles/java/codebuddy/.harness-build.json +1 -1
  155. package/harness/bundles/java/codebuddy/README.md +3 -3
  156. package/harness/bundles/java/codebuddy/contracts/workflow-policy.json +1 -1
  157. package/harness/bundles/java/codebuddy/harness-apidoc/SKILL.md +1 -1
  158. package/harness/bundles/java/codebuddy/harness-archive/SKILL.md +21 -33
  159. package/harness/bundles/java/codebuddy/harness-archive/checklist.md +6 -10
  160. package/harness/bundles/java/codebuddy/harness-codebase-map/SKILL.md +1 -1
  161. package/harness/bundles/java/codebuddy/harness-knowledge-ingest/SKILL.md +1 -1
  162. package/harness/bundles/java/codebuddy/harness-knowledge-query/SKILL.md +1 -1
  163. package/harness/bundles/java/codebuddy/harness-package/SKILL.md +1 -1
  164. package/harness/bundles/java/codebuddy/harness-package/checklist.md +1 -1
  165. package/harness/bundles/java/codebuddy/harness-plan/SKILL.md +7 -3
  166. package/harness/bundles/java/codebuddy/harness-plan/checklist.md +2 -1
  167. package/harness/bundles/java/codebuddy/harness-review/SKILL.md +8 -4
  168. package/harness/bundles/java/codebuddy/harness-run/SKILL.md +9 -7
  169. package/harness/bundles/java/codebuddy/harness-run/checklist.md +1 -1
  170. package/harness/bundles/java/codebuddy/harness-run/reference.md +2 -2
  171. package/harness/bundles/java/codebuddy/harness-submit/SKILL.md +6 -6
  172. package/harness/bundles/java/codebuddy/harness-submit/checklist.md +1 -1
  173. package/harness/bundles/java/codebuddy/harness-sync/SKILL.md +1 -1
  174. package/harness/bundles/java/codebuddy/harness-test/SKILL.md +8 -6
  175. package/harness/bundles/java/codebuddy/harness-test/checklist.md +1 -1
  176. package/harness/bundles/java/codebuddy/harness-test/reference.md +1 -1
  177. package/harness/bundles/java/codebuddy/protocols/ledger-protocol.md +3 -3
  178. package/harness/bundles/java/codebuddy/scripts/harness_archive.py +322 -74
  179. package/harness/bundles/java/codebuddy/scripts/harness_context.py +236 -63
  180. package/harness/bundles/java/codebuddy/scripts/harness_events.py +86 -5
  181. package/harness/bundles/java/codebuddy/scripts/harness_events_sync.py +201 -17
  182. package/harness/bundles/java/codebuddy/scripts/harness_fixback.py +189 -1
  183. package/harness/bundles/java/codebuddy/scripts/harness_gate.py +353 -3
  184. package/harness/bundles/java/codebuddy/scripts/harness_ledger.py +195 -11
  185. package/harness/bundles/java/codebuddy/scripts/harness_plan_finalize.py +82 -0
  186. package/harness/bundles/java/codebuddy/scripts/harness_test_guard.py +37 -44
  187. package/harness/bundles/java/codex/.harness-build.json +1 -1
  188. package/harness/bundles/java/codex/README.md +3 -3
  189. package/harness/bundles/java/codex/contracts/workflow-policy.json +1 -1
  190. package/harness/bundles/java/codex/harness-apidoc/SKILL.md +1 -1
  191. package/harness/bundles/java/codex/harness-archive/SKILL.md +21 -33
  192. package/harness/bundles/java/codex/harness-archive/checklist.md +6 -10
  193. package/harness/bundles/java/codex/harness-codebase-map/SKILL.md +1 -1
  194. package/harness/bundles/java/codex/harness-knowledge-ingest/SKILL.md +1 -1
  195. package/harness/bundles/java/codex/harness-knowledge-query/SKILL.md +1 -1
  196. package/harness/bundles/java/codex/harness-package/SKILL.md +1 -1
  197. package/harness/bundles/java/codex/harness-package/checklist.md +1 -1
  198. package/harness/bundles/java/codex/harness-plan/SKILL.md +7 -3
  199. package/harness/bundles/java/codex/harness-plan/checklist.md +2 -1
  200. package/harness/bundles/java/codex/harness-review/SKILL.md +4 -4
  201. package/harness/bundles/java/codex/harness-run/SKILL.md +9 -7
  202. package/harness/bundles/java/codex/harness-run/checklist.md +1 -1
  203. package/harness/bundles/java/codex/harness-run/reference.md +2 -2
  204. package/harness/bundles/java/codex/harness-submit/SKILL.md +6 -6
  205. package/harness/bundles/java/codex/harness-submit/checklist.md +1 -1
  206. package/harness/bundles/java/codex/harness-sync/SKILL.md +1 -1
  207. package/harness/bundles/java/codex/harness-test/SKILL.md +8 -6
  208. package/harness/bundles/java/codex/harness-test/checklist.md +1 -1
  209. package/harness/bundles/java/codex/harness-test/reference.md +1 -1
  210. package/harness/bundles/java/codex/protocols/ledger-protocol.md +3 -3
  211. package/harness/bundles/java/codex/scripts/harness_archive.py +322 -74
  212. package/harness/bundles/java/codex/scripts/harness_context.py +236 -63
  213. package/harness/bundles/java/codex/scripts/harness_events.py +86 -5
  214. package/harness/bundles/java/codex/scripts/harness_events_sync.py +201 -17
  215. package/harness/bundles/java/codex/scripts/harness_fixback.py +189 -1
  216. package/harness/bundles/java/codex/scripts/harness_gate.py +353 -3
  217. package/harness/bundles/java/codex/scripts/harness_ledger.py +195 -11
  218. package/harness/bundles/java/codex/scripts/harness_plan_finalize.py +82 -0
  219. package/harness/bundles/java/codex/scripts/harness_test_guard.py +37 -44
  220. package/harness/bundles/java/cursor/.harness-build.json +1 -1
  221. package/harness/bundles/java/cursor/README.md +3 -3
  222. package/harness/bundles/java/cursor/contracts/workflow-policy.json +1 -1
  223. package/harness/bundles/java/cursor/harness-apidoc/SKILL.md +1 -1
  224. package/harness/bundles/java/cursor/harness-archive/SKILL.md +21 -33
  225. package/harness/bundles/java/cursor/harness-archive/checklist.md +6 -10
  226. package/harness/bundles/java/cursor/harness-codebase-map/SKILL.md +1 -1
  227. package/harness/bundles/java/cursor/harness-knowledge-ingest/SKILL.md +1 -1
  228. package/harness/bundles/java/cursor/harness-knowledge-query/SKILL.md +1 -1
  229. package/harness/bundles/java/cursor/harness-package/SKILL.md +1 -1
  230. package/harness/bundles/java/cursor/harness-package/checklist.md +1 -1
  231. package/harness/bundles/java/cursor/harness-plan/SKILL.md +7 -3
  232. package/harness/bundles/java/cursor/harness-plan/checklist.md +2 -1
  233. package/harness/bundles/java/cursor/harness-review/SKILL.md +4 -4
  234. package/harness/bundles/java/cursor/harness-run/SKILL.md +9 -7
  235. package/harness/bundles/java/cursor/harness-run/checklist.md +1 -1
  236. package/harness/bundles/java/cursor/harness-run/reference.md +2 -2
  237. package/harness/bundles/java/cursor/harness-submit/SKILL.md +6 -6
  238. package/harness/bundles/java/cursor/harness-submit/checklist.md +1 -1
  239. package/harness/bundles/java/cursor/harness-sync/SKILL.md +1 -1
  240. package/harness/bundles/java/cursor/harness-test/SKILL.md +8 -6
  241. package/harness/bundles/java/cursor/harness-test/checklist.md +1 -1
  242. package/harness/bundles/java/cursor/harness-test/reference.md +1 -1
  243. package/harness/bundles/java/cursor/protocols/ledger-protocol.md +3 -3
  244. package/harness/bundles/java/cursor/scripts/harness_archive.py +322 -74
  245. package/harness/bundles/java/cursor/scripts/harness_context.py +236 -63
  246. package/harness/bundles/java/cursor/scripts/harness_events.py +86 -5
  247. package/harness/bundles/java/cursor/scripts/harness_events_sync.py +201 -17
  248. package/harness/bundles/java/cursor/scripts/harness_fixback.py +189 -1
  249. package/harness/bundles/java/cursor/scripts/harness_gate.py +353 -3
  250. package/harness/bundles/java/cursor/scripts/harness_ledger.py +195 -11
  251. package/harness/bundles/java/cursor/scripts/harness_plan_finalize.py +82 -0
  252. package/harness/bundles/java/cursor/scripts/harness_test_guard.py +37 -44
  253. package/harness/manifests/general/claude-code.json +32 -32
  254. package/harness/manifests/general/codebuddy.json +32 -32
  255. package/harness/manifests/general/codex.json +32 -32
  256. package/harness/manifests/general/cursor.json +32 -32
  257. package/harness/manifests/java/claude-code.json +35 -35
  258. package/harness/manifests/java/codebuddy.json +35 -35
  259. package/harness/manifests/java/codex.json +35 -35
  260. package/harness/manifests/java/cursor.json +35 -35
  261. package/hunter-workflow-family.json +5 -4
  262. package/package.json +1 -1
@@ -23,6 +23,18 @@ PHASE_GRAPH = {
23
23
  "submit": (),
24
24
  }
25
25
 
26
+ WORKFLOW_PHASES = (
27
+ "plan",
28
+ "run",
29
+ "test",
30
+ "review",
31
+ "package",
32
+ "apidoc",
33
+ "submit",
34
+ "merge",
35
+ "archive",
36
+ )
37
+
26
38
  DISPLAY_TITLE_MAX_LENGTH = 80
27
39
 
28
40
 
@@ -206,6 +218,107 @@ def _contract(project: Path, change: str) -> tuple[Path, dict[str, Any], Path]:
206
218
  return contract_root, contract, state_root
207
219
 
208
220
 
221
+ def _phase_plan(contract_root: Path) -> tuple[list[str] | None, str]:
222
+ policy_path = contract_root / "meta" / "gate-policy.json"
223
+ if not policy_path.is_file():
224
+ return None, "legacy"
225
+ try:
226
+ policy = _read_json(policy_path)
227
+ except (OSError, ValueError, json.JSONDecodeError):
228
+ return None, "legacy"
229
+ for field, source in (
230
+ ("plannedPhases", "change"),
231
+ ("defaultPhases", "policy-default"),
232
+ ):
233
+ raw = policy.get(field)
234
+ if not isinstance(raw, list):
235
+ continue
236
+ phases = [str(item).strip() for item in raw if str(item).strip()]
237
+ if (
238
+ phases
239
+ and len(phases) == len(set(phases))
240
+ and all(item in WORKFLOW_PHASES for item in phases)
241
+ ):
242
+ return phases, source
243
+ return None, "legacy"
244
+
245
+
246
+ def _allowed_next_phases(contract_root: Path, from_phase: str) -> list[str]:
247
+ planned, _source = _phase_plan(contract_root)
248
+ if planned is None:
249
+ return list(PHASE_GRAPH.get(from_phase, ()))
250
+ if from_phase not in planned:
251
+ return []
252
+ index = planned.index(from_phase)
253
+ allowed = planned[index + 1 : index + 2]
254
+ if from_phase in {"test", "review"} and "run" in planned:
255
+ allowed = [*allowed, "run"]
256
+ return list(dict.fromkeys(allowed))
257
+
258
+
259
+ def configure_phase_plan(
260
+ project: Path,
261
+ change: str,
262
+ *,
263
+ phases: list[str],
264
+ operator: str,
265
+ reason: str,
266
+ ) -> dict[str, Any]:
267
+ """Persist the single phase plan consumed by Context, Archive and Platform."""
268
+ project = Path(project).resolve()
269
+ try:
270
+ contract_root, _contract_data, _state_root = _contract(project, change)
271
+ except (OSError, ValueError, json.JSONDecodeError) as exc:
272
+ return {"ok": False, "code": "CHANGE_NOT_FOUND", "error": str(exc)}
273
+ normalized = [str(item).strip() for item in phases if str(item).strip()]
274
+ if (
275
+ not normalized
276
+ or len(normalized) != len(set(normalized))
277
+ or any(item not in WORKFLOW_PHASES for item in normalized)
278
+ or normalized[0] != "plan"
279
+ or normalized[-1] != "archive"
280
+ ):
281
+ return {
282
+ "ok": False,
283
+ "code": "PHASE_PLAN_INVALID",
284
+ "message": "phase plan must be unique, start with plan, and end with archive",
285
+ "allowedPhases": list(WORKFLOW_PHASES),
286
+ }
287
+ if not operator.strip() or not reason.strip():
288
+ return {
289
+ "ok": False,
290
+ "code": "PHASE_PLAN_JUSTIFICATION_REQUIRED",
291
+ }
292
+ policy_path = contract_root / "meta" / "gate-policy.json"
293
+ policy = _read_json(policy_path) if policy_path.is_file() else {"schemaVersion": 1}
294
+ skipped = [
295
+ {
296
+ "phase": phase,
297
+ "reason": reason.strip(),
298
+ "operator": operator.strip(),
299
+ "decidedAt": _now().isoformat(),
300
+ }
301
+ for phase in WORKFLOW_PHASES
302
+ if phase not in normalized and phase != "merge"
303
+ ]
304
+ policy["plannedPhases"] = normalized
305
+ policy["skippedPhases"] = skipped
306
+ policy["phasePlan"] = {
307
+ "source": "change-override",
308
+ "operator": operator.strip(),
309
+ "reason": reason.strip(),
310
+ "updatedAt": _now().isoformat(),
311
+ }
312
+ _write_json_atomic(policy_path, policy)
313
+ return {
314
+ "ok": True,
315
+ "code": "PHASE_PLAN_CONFIGURED",
316
+ "plannedPhases": normalized,
317
+ "skippedPhases": skipped,
318
+ "path": str(policy_path),
319
+ }
320
+
321
+
209
322
  def _active_changes(project: Path) -> list[str]:
210
323
  root = project.resolve() / ".harness" / "changes"
211
324
  if not root.is_dir():
@@ -319,6 +432,14 @@ def prepare_context(
319
432
  and lifecycle.get("status") not in {None, "active"}
320
433
  ):
321
434
  return {"ok": False, "code": "CHANGE_NOT_ACTIVE", "changeName": change}
435
+ planned_phases, phase_plan_source = _phase_plan(contract_root)
436
+ if planned_phases is not None and phase not in planned_phases:
437
+ return {
438
+ "ok": False,
439
+ "code": "PHASE_NOT_PLANNED",
440
+ "phase": phase,
441
+ "plannedPhases": planned_phases,
442
+ }
322
443
  paths = _paths(state_root)
323
444
  paths["runtime"].mkdir(parents=True, exist_ok=True)
324
445
  recovery: dict[str, Any] | None = None
@@ -395,9 +516,13 @@ def prepare_context(
395
516
  },
396
517
  )
397
518
  next_phases = (
398
- list(PHASE_GRAPH.get(str(latest.get("toPhase")), ()))
399
- if latest
400
- else ["plan", "run"]
519
+ _allowed_next_phases(contract_root, phase)
520
+ if planned_phases is not None
521
+ else (
522
+ list(PHASE_GRAPH.get(str(latest.get("toPhase")), ()))
523
+ if latest
524
+ else ["plan", "run"]
525
+ )
401
526
  )
402
527
  return {
403
528
  "ok": True,
@@ -408,6 +533,8 @@ def prepare_context(
408
533
  "contractRoot": str(contract_root),
409
534
  "stateRoot": str(state_root),
410
535
  "nextPhases": next_phases,
536
+ "plannedPhases": planned_phases,
537
+ "phasePlanSource": phase_plan_source,
411
538
  "legacyBootstrap": not transitions,
412
539
  "latestTransition": latest,
413
540
  "lease": lease,
@@ -423,42 +550,19 @@ def _invalidate_for_fixback(
423
550
  from_phase: str,
424
551
  to_phase: str,
425
552
  ) -> dict[str, Any]:
426
- ledger_path = state_root / "evidence" / "verification-ledger.json"
427
- invalidated: list[str] = []
428
- if ledger_path.is_file():
429
- try:
430
- ledger = _read_json(ledger_path)
431
- except (OSError, ValueError, json.JSONDecodeError):
432
- ledger = {}
433
- targets = ledger.get("verificationTargets")
434
- if isinstance(targets, dict):
435
- for target_id, target in targets.items():
436
- if not isinstance(target, dict):
437
- continue
438
- target["reusable"] = False
439
- target["invalidation"] = {
440
- "code": "FIXBACK_INVALIDATED",
441
- "transitionHash": transition_hash,
442
- "fromPhase": from_phase,
443
- "toPhase": to_phase,
444
- "invalidatedAt": _now().isoformat(),
445
- }
446
- invalidated.append(str(target_id))
447
- validations = ledger.get("validations")
448
- if isinstance(validations, dict):
449
- for entry in validations.values():
450
- if isinstance(entry, dict):
451
- entry["reusable"] = False
452
- entry["invalidation"] = {
453
- "code": "FIXBACK_INVALIDATED",
454
- "transitionHash": transition_hash,
455
- }
456
- _write_json_atomic(ledger_path, ledger)
553
+ # A review→run handoff happens before product files are changed. Invalidating
554
+ # every target here made unrelated API/build evidence unusable. The Fixback
555
+ # resolver now invalidates only entries whose inputsFiles intersect the
556
+ # issue's actual changedFiles.
457
557
  record = {
458
558
  "schemaVersion": 1,
459
- "code": "FIXBACK_INVALIDATED",
559
+ "code": "FIXBACK_INVALIDATION_DEFERRED",
460
560
  "transitionHash": transition_hash,
461
- "targetIds": invalidated,
561
+ "fromPhase": from_phase,
562
+ "toPhase": to_phase,
563
+ "targetIds": [],
564
+ "deferred": True,
565
+ "reason": "changed-files-not-known",
462
566
  "createdAt": _now().isoformat(),
463
567
  }
464
568
  _append_ndjson(
@@ -474,30 +578,103 @@ def close_transition(
474
578
  *,
475
579
  from_phase: str,
476
580
  to_phase: str,
477
- executor: str,
581
+ executor: str | None,
478
582
  artifacts: list[str] | None = None,
479
583
  status: str = "OK",
480
584
  ) -> dict[str, Any]:
481
585
  project = Path(project).resolve()
482
- if to_phase not in PHASE_GRAPH.get(from_phase, ()):
586
+ try:
587
+ contract_root, _contract_data, state_root = _contract(project, change)
588
+ except (OSError, ValueError, json.JSONDecodeError) as exc:
589
+ return {"ok": False, "code": "CHANGE_NOT_FOUND", "error": str(exc)}
590
+ allowed_next = _allowed_next_phases(contract_root, from_phase)
591
+ if to_phase not in allowed_next:
592
+ planned_phases, source = _phase_plan(contract_root)
483
593
  return {
484
594
  "ok": False,
485
595
  "code": "TRANSITION_ILLEGAL",
486
596
  "fromPhase": from_phase,
487
597
  "toPhase": to_phase,
598
+ "allowedNextPhases": allowed_next,
599
+ "plannedPhases": planned_phases,
600
+ "phasePlanSource": source,
488
601
  }
489
- try:
490
- contract_root, _contract_data, state_root = _contract(project, change)
491
- except (OSError, ValueError, json.JSONDecodeError) as exc:
492
- return {"ok": False, "code": "CHANGE_NOT_FOUND", "error": str(exc)}
493
602
  paths = _paths(state_root)
603
+
604
+ artifact_entries: list[dict[str, Any]] = []
605
+ for raw in artifacts or []:
606
+ requested = Path(raw)
607
+ candidates = (
608
+ [requested]
609
+ if requested.is_absolute()
610
+ else [project / requested, contract_root / requested]
611
+ )
612
+ valid: list[Path] = []
613
+ for candidate in candidates:
614
+ try:
615
+ resolved_candidate = candidate.resolve()
616
+ except OSError:
617
+ continue
618
+ if resolved_candidate in valid:
619
+ continue
620
+ if resolved_candidate.is_relative_to(project) and resolved_candidate.is_file():
621
+ valid.append(resolved_candidate)
622
+ if len(valid) > 1:
623
+ return {
624
+ "ok": False,
625
+ "code": "TRANSITION_ARTIFACT_AMBIGUOUS",
626
+ "path": raw,
627
+ "candidates": [item.relative_to(project).as_posix() for item in valid],
628
+ "message": "relative artifact exists under both project and change roots",
629
+ }
630
+ if not valid:
631
+ return {
632
+ "ok": False,
633
+ "code": "TRANSITION_ARTIFACT_INVALID",
634
+ "path": raw,
635
+ "acceptedBases": [str(project), str(contract_root)],
636
+ "examples": [
637
+ str(contract_root / "meta" / "plan-finalization.json"),
638
+ f".harness/changes/{change}/meta/plan-finalization.json",
639
+ "meta/plan-finalization.json",
640
+ ],
641
+ }
642
+ resolved_artifact = valid[0]
643
+ artifact_entries.append(
644
+ {
645
+ "path": resolved_artifact.relative_to(project).as_posix(),
646
+ "sha256": _sha256(resolved_artifact),
647
+ }
648
+ )
649
+
650
+ transitions = _read_ndjson(paths["transitions"])
494
651
  if not paths["lease"].is_file():
652
+ latest = transitions[-1] if transitions else None
653
+ if (
654
+ isinstance(latest, dict)
655
+ and latest.get("fromPhase") == from_phase
656
+ and latest.get("toPhase") == to_phase
657
+ and latest.get("status") == status
658
+ and latest.get("artifacts") == artifact_entries
659
+ and (executor is None or latest.get("executor") == executor)
660
+ ):
661
+ return {
662
+ "ok": True,
663
+ "code": "TRANSITION_ALREADY_CLOSED",
664
+ "idempotent": True,
665
+ "receipt": latest,
666
+ "path": str(paths["transitions"]),
667
+ "invalidation": None,
668
+ }
495
669
  return {"ok": False, "code": "CONTEXT_LEASE_REQUIRED"}
496
670
  try:
497
671
  lease = _read_json(paths["lease"])
498
672
  except (OSError, ValueError, json.JSONDecodeError):
499
673
  return {"ok": False, "code": "CONTEXT_LEASE_INVALID"}
500
- if lease.get("owner") != executor or lease.get("phase") != from_phase:
674
+ effective_executor = executor or str(lease.get("owner") or "")
675
+ if not effective_executor:
676
+ return {"ok": False, "code": "CONTEXT_EXECUTOR_REQUIRED"}
677
+ if lease.get("owner") != effective_executor or lease.get("phase") != from_phase:
501
678
  return {
502
679
  "ok": False,
503
680
  "code": "CONTEXT_LEASE_MISMATCH",
@@ -508,25 +685,6 @@ def close_transition(
508
685
  if expiry is None or _now() >= expiry:
509
686
  return {"ok": False, "code": "CONTEXT_LEASE_EXPIRED"}
510
687
 
511
- artifact_entries: list[dict[str, Any]] = []
512
- for raw in artifacts or []:
513
- path = Path(raw)
514
- if not path.is_absolute():
515
- path = contract_root / path
516
- resolved = path.resolve()
517
- if not resolved.is_relative_to(project) or not resolved.is_file():
518
- return {
519
- "ok": False,
520
- "code": "TRANSITION_ARTIFACT_INVALID",
521
- "path": str(path),
522
- }
523
- artifact_entries.append(
524
- {
525
- "path": resolved.relative_to(project).as_posix(),
526
- "sha256": _sha256(resolved),
527
- }
528
- )
529
- transitions = _read_ndjson(paths["transitions"])
530
688
  previous_hash = transitions[-1].get("receiptHash") if transitions else None
531
689
  attempt = (
532
690
  1
@@ -543,7 +701,7 @@ def close_transition(
543
701
  "fromPhase": from_phase,
544
702
  "toPhase": to_phase,
545
703
  "status": status,
546
- "executor": executor,
704
+ "executor": effective_executor,
547
705
  "productCommit": _head(project),
548
706
  "artifacts": artifact_entries,
549
707
  "attempt": attempt,
@@ -724,6 +882,13 @@ def build_parser() -> argparse.ArgumentParser:
724
882
  begin.add_argument("--change", required=True)
725
883
  begin.add_argument("--phase", required=True)
726
884
  begin.add_argument("--executor", required=True)
885
+ configure = sub.add_parser("configure-plan")
886
+ configure.add_argument("--json", action="store_true")
887
+ configure.add_argument("--project", required=True, type=Path)
888
+ configure.add_argument("--change", required=True)
889
+ configure.add_argument("--phases", required=True)
890
+ configure.add_argument("--operator", required=True)
891
+ configure.add_argument("--reason", required=True)
727
892
  view = sub.add_parser("view")
728
893
  view.add_argument("--json", action="store_true")
729
894
  view.add_argument("--project", required=True, type=Path)
@@ -759,6 +924,14 @@ def main(argv: list[str] | None = None) -> int:
759
924
  phase=args.phase,
760
925
  executor=args.executor,
761
926
  )
927
+ elif args.command == "configure-plan":
928
+ result = configure_phase_plan(
929
+ args.project,
930
+ args.change,
931
+ phases=[item.strip() for item in args.phases.split(",") if item.strip()],
932
+ operator=args.operator,
933
+ reason=args.reason,
934
+ )
762
935
  else:
763
936
  result = context_view(args.project, args.change)
764
937
  print(json.dumps(result, ensure_ascii=False, indent=2))
@@ -41,7 +41,7 @@ def _project_root_for_change(change_dir: Path) -> Path | None:
41
41
  return None
42
42
 
43
43
 
44
- def _nudge_remote_sync(change_dir: Path) -> None:
44
+ def nudge_remote_sync(change_dir: Path) -> None:
45
45
  """Best-effort wake-up after the event append has been flushed to disk."""
46
46
  project_root = _project_root_for_change(change_dir)
47
47
  if project_root is None:
@@ -55,6 +55,11 @@ def _nudge_remote_sync(change_dir: Path) -> None:
55
55
  return
56
56
 
57
57
 
58
+ def _nudge_remote_sync(change_dir: Path) -> None:
59
+ """Compatibility wrapper for older callers and focused tests."""
60
+ nudge_remote_sync(change_dir)
61
+
62
+
58
63
  if hasattr(sys.stdout, "reconfigure"):
59
64
  sys.stdout.reconfigure(encoding="utf-8")
60
65
  if hasattr(sys.stderr, "reconfigure"):
@@ -66,6 +71,8 @@ EVENT_TYPES = frozenset(
66
71
  "phase.start",
67
72
  "phase.end",
68
73
  "phase.auto_sealed",
74
+ "gate.blocked",
75
+ "gate.recovered",
69
76
  "command",
70
77
  "verification",
71
78
  "artifact",
@@ -121,6 +128,12 @@ OPTIONAL_FIELDS = (
121
128
  "renamed_from",
122
129
  "renamed_to",
123
130
  "change_uuid",
131
+ "execution_mode",
132
+ "decision_reason_code",
133
+ "fallback_reason_code",
134
+ "trigger",
135
+ "from_phase",
136
+ "result_status",
124
137
  )
125
138
 
126
139
  _PROVENANCE_FIELDS = frozenset(
@@ -139,6 +152,12 @@ _PROVENANCE_FIELDS = frozenset(
139
152
  "orchestration_active_ms",
140
153
  "wall_clock_ms",
141
154
  "user_wait_ms",
155
+ "execution_mode",
156
+ "decision_reason_code",
157
+ "fallback_reason_code",
158
+ "trigger",
159
+ "from_phase",
160
+ "result_status",
142
161
  }
143
162
  )
144
163
  _EVENT_ALLOWED_FIELDS = {
@@ -148,6 +167,10 @@ _EVENT_ALLOWED_FIELDS = {
148
167
  )
149
168
  | _PROVENANCE_FIELDS,
150
169
  "phase.auto_sealed": frozenset({"status", "reason", "note"}) | _PROVENANCE_FIELDS,
170
+ "gate.blocked": frozenset({"status", "code", "reason", "note"})
171
+ | _PROVENANCE_FIELDS,
172
+ "gate.recovered": frozenset({"status", "code", "reason", "note"})
173
+ | _PROVENANCE_FIELDS,
151
174
  "command": frozenset({"command", "exit_code", "duration_ms", "note"})
152
175
  | _PROVENANCE_FIELDS,
153
176
  "verification": frozenset(
@@ -1698,6 +1721,22 @@ def append_with_auto_seal(
1698
1721
  else []
1699
1722
  )
1700
1723
  )
1724
+ if event_type in {"phase.start", "phase.end", "phase.auto_sealed"} and phase:
1725
+ phase_attempts = split_phase_attempts(
1726
+ [item for item in loaded if item.get("phase") == phase]
1727
+ )
1728
+ latest_attempt = max(
1729
+ (
1730
+ int(item.get("attempt"))
1731
+ for item in phase_attempts
1732
+ if isinstance(item.get("attempt"), int)
1733
+ ),
1734
+ default=0,
1735
+ )
1736
+ if not isinstance(event.get("attempt"), int) or int(event["attempt"]) <= 0:
1737
+ event["attempt"] = (
1738
+ latest_attempt + 1 if event_type == "phase.start" else latest_attempt or 1
1739
+ )
1701
1740
  if event_type == "phase.start" and phase:
1702
1741
  auto_sealed = seal_open_phase_attempts(loaded, phase=phase)
1703
1742
  for seal_event in auto_sealed:
@@ -1765,18 +1804,45 @@ def cmd_append(args: argparse.Namespace) -> int:
1765
1804
  "warning: verification missing --name or --status",
1766
1805
  file=sys.stderr,
1767
1806
  )
1768
- line = json.dumps(event, ensure_ascii=False, separators=(",", ":"))
1769
1807
  phase_closed = False
1770
1808
  projection_error: str | None = None
1809
+ attempt_error: str | None = None
1771
1810
  auto_sealed_events: list[dict[str, Any]] = []
1772
1811
  try:
1773
1812
  with event_file_lock(lock_path):
1774
1813
  existing_events = (
1775
1814
  load_events(path)
1776
1815
  if args.type in {"phase.end", "correction", "phase.start"}
1816
+ or event.get("run_id") is not None
1817
+ or event.get("attempt") is not None
1777
1818
  else []
1778
1819
  )
1779
- if args.type == "phase.end":
1820
+ run_id = event.get("run_id")
1821
+ if run_id is not None:
1822
+ run_attempts = {
1823
+ int(item["attempt"])
1824
+ for item in existing_events
1825
+ if item.get("phase") == event.get("phase")
1826
+ and item.get("run_id") == run_id
1827
+ and isinstance(item.get("attempt"), int)
1828
+ and int(item["attempt"]) > 0
1829
+ }
1830
+ if run_attempts:
1831
+ expected_attempt = max(run_attempts)
1832
+ explicit_attempt = event.get("attempt")
1833
+ if (
1834
+ isinstance(explicit_attempt, int)
1835
+ and explicit_attempt != expected_attempt
1836
+ ):
1837
+ attempt_error = (
1838
+ "EVENT_ATTEMPT_CONFLICT: run_id is already bound to "
1839
+ f"attempt {expected_attempt}, received {explicit_attempt}"
1840
+ )
1841
+ else:
1842
+ event["attempt"] = expected_attempt
1843
+ if attempt_error is not None:
1844
+ pass
1845
+ elif args.type == "phase.end":
1780
1846
  phase_closed = phase_end_already_recorded(existing_events, event)
1781
1847
  elif args.type == "correction":
1782
1848
  try:
@@ -1797,13 +1863,16 @@ def cmd_append(args: argparse.Namespace) -> int:
1797
1863
  phase=args.phase,
1798
1864
  seal_reason=inferred_reason,
1799
1865
  )
1800
- if not phase_closed and projection_error is None:
1866
+ if not phase_closed and projection_error is None and attempt_error is None:
1801
1867
  for seal_event in auto_sealed_events:
1802
1868
  atomic_append_line(
1803
1869
  path,
1804
1870
  json.dumps(seal_event, ensure_ascii=False, separators=(",", ":")),
1805
1871
  )
1806
- atomic_append_line(path, line)
1872
+ atomic_append_line(
1873
+ path,
1874
+ json.dumps(event, ensure_ascii=False, separators=(",", ":")),
1875
+ )
1807
1876
  except (OSError, TimeoutError, ValueError) as exc:
1808
1877
  return emit_error(f"append failed: {exc}", as_json=as_json)
1809
1878
  if phase_closed:
@@ -1819,6 +1888,12 @@ def cmd_append(args: argparse.Namespace) -> int:
1819
1888
  as_json=as_json,
1820
1889
  error_code=error_code,
1821
1890
  )
1891
+ if attempt_error is not None:
1892
+ return emit_error(
1893
+ attempt_error,
1894
+ as_json=as_json,
1895
+ error_code="EVENT_ATTEMPT_CONFLICT",
1896
+ )
1822
1897
 
1823
1898
  _nudge_remote_sync(change_dir)
1824
1899
 
@@ -1967,6 +2042,12 @@ def build_parser() -> argparse.ArgumentParser:
1967
2042
  p_append.add_argument("--orchestration-active-ms", type=int, default=None)
1968
2043
  p_append.add_argument("--wall-clock-ms", type=int, default=None)
1969
2044
  p_append.add_argument("--user-wait-ms", type=int, default=None)
2045
+ p_append.add_argument("--execution-mode", default=None)
2046
+ p_append.add_argument("--decision-reason-code", default=None)
2047
+ p_append.add_argument("--fallback-reason-code", default=None)
2048
+ p_append.add_argument("--trigger", default=None)
2049
+ p_append.add_argument("--from-phase", default=None)
2050
+ p_append.add_argument("--result-status", default=None)
1970
2051
  p_append.add_argument(
1971
2052
  "--legacy-lenient",
1972
2053
  action="store_true",