@hunter-harness/workflow-harness 0.2.9 → 0.2.10

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 (282) hide show
  1. package/harness/bundles/general/claude-code/.harness-build.json +1 -1
  2. package/harness/bundles/general/claude-code/harness-archive/SKILL.md +1 -1
  3. package/harness/bundles/general/claude-code/harness-archive/reference.md +12 -2
  4. package/harness/bundles/general/claude-code/harness-archive/templates/summary-data-template.json +6 -2
  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 +3 -1
  7. package/harness/bundles/general/claude-code/harness-knowledge-ingest/reference.md +7 -0
  8. package/harness/bundles/general/claude-code/harness-knowledge-ingest/scripts/harness_knowledge.py +117 -0
  9. package/harness/bundles/general/claude-code/harness-knowledge-query/SKILL.md +1 -1
  10. package/harness/bundles/general/claude-code/harness-plan/SKILL.md +1 -1
  11. package/harness/bundles/general/claude-code/harness-review/SKILL.md +3 -3
  12. package/harness/bundles/general/claude-code/harness-run/SKILL.md +1 -1
  13. package/harness/bundles/general/claude-code/harness-run/reference.md +2 -0
  14. package/harness/bundles/general/claude-code/harness-submit/SKILL.md +15 -10
  15. package/harness/bundles/general/claude-code/harness-submit/checklist.md +40 -76
  16. package/harness/bundles/general/claude-code/harness-sync/SKILL.md +17 -2
  17. package/harness/bundles/general/claude-code/harness-sync/reference.md +2 -2
  18. package/harness/bundles/general/claude-code/harness-test/SKILL.md +1 -1
  19. package/harness/bundles/general/claude-code/harness-test/reference.md +2 -0
  20. package/harness/bundles/general/claude-code/protocols/archive-report-protocol.md +24 -4
  21. package/harness/bundles/general/claude-code/protocols/ledger-protocol.md +36 -0
  22. package/harness/bundles/general/claude-code/protocols/state-layout-protocol.md +15 -0
  23. package/harness/bundles/general/claude-code/scripts/harness_archive.py +803 -274
  24. package/harness/bundles/general/claude-code/scripts/harness_change.py +18 -0
  25. package/harness/bundles/general/claude-code/scripts/harness_deploy.py +1 -44
  26. package/harness/bundles/general/claude-code/scripts/harness_events.py +104 -6
  27. package/harness/bundles/general/claude-code/scripts/harness_gate.py +25 -4
  28. package/harness/bundles/general/claude-code/scripts/harness_integration.py +878 -0
  29. package/harness/bundles/general/claude-code/scripts/harness_ledger.py +440 -10
  30. package/harness/bundles/general/claude-code/scripts/harness_paths.py +315 -0
  31. package/harness/bundles/general/claude-code/scripts/harness_profile.py +45 -0
  32. package/harness/bundles/general/claude-code/scripts/harness_review.py +326 -0
  33. package/harness/bundles/general/claude-code/scripts/harness_sync.py +407 -0
  34. package/harness/bundles/general/claude-code/scripts/harness_test_guard.py +244 -49
  35. package/harness/bundles/general/codebuddy/.harness-build.json +1 -1
  36. package/harness/bundles/general/codebuddy/harness-archive/SKILL.md +1 -1
  37. package/harness/bundles/general/codebuddy/harness-archive/reference.md +12 -2
  38. package/harness/bundles/general/codebuddy/harness-archive/templates/summary-data-template.json +6 -2
  39. package/harness/bundles/general/codebuddy/harness-codebase-map/SKILL.md +1 -1
  40. package/harness/bundles/general/codebuddy/harness-knowledge-ingest/SKILL.md +3 -1
  41. package/harness/bundles/general/codebuddy/harness-knowledge-ingest/reference.md +7 -0
  42. package/harness/bundles/general/codebuddy/harness-knowledge-ingest/scripts/harness_knowledge.py +117 -0
  43. package/harness/bundles/general/codebuddy/harness-knowledge-query/SKILL.md +1 -1
  44. package/harness/bundles/general/codebuddy/harness-plan/SKILL.md +1 -1
  45. package/harness/bundles/general/codebuddy/harness-review/SKILL.md +3 -3
  46. package/harness/bundles/general/codebuddy/harness-run/SKILL.md +1 -1
  47. package/harness/bundles/general/codebuddy/harness-run/reference.md +2 -0
  48. package/harness/bundles/general/codebuddy/harness-submit/SKILL.md +15 -10
  49. package/harness/bundles/general/codebuddy/harness-submit/checklist.md +40 -76
  50. package/harness/bundles/general/codebuddy/harness-sync/SKILL.md +17 -2
  51. package/harness/bundles/general/codebuddy/harness-sync/reference.md +2 -2
  52. package/harness/bundles/general/codebuddy/harness-test/SKILL.md +1 -1
  53. package/harness/bundles/general/codebuddy/harness-test/reference.md +2 -0
  54. package/harness/bundles/general/codebuddy/protocols/archive-report-protocol.md +24 -4
  55. package/harness/bundles/general/codebuddy/protocols/ledger-protocol.md +36 -0
  56. package/harness/bundles/general/codebuddy/protocols/state-layout-protocol.md +15 -0
  57. package/harness/bundles/general/codebuddy/scripts/harness_archive.py +803 -274
  58. package/harness/bundles/general/codebuddy/scripts/harness_change.py +18 -0
  59. package/harness/bundles/general/codebuddy/scripts/harness_deploy.py +1 -44
  60. package/harness/bundles/general/codebuddy/scripts/harness_events.py +104 -6
  61. package/harness/bundles/general/codebuddy/scripts/harness_gate.py +25 -4
  62. package/harness/bundles/general/codebuddy/scripts/harness_integration.py +878 -0
  63. package/harness/bundles/general/codebuddy/scripts/harness_ledger.py +440 -10
  64. package/harness/bundles/general/codebuddy/scripts/harness_paths.py +315 -0
  65. package/harness/bundles/general/codebuddy/scripts/harness_profile.py +45 -0
  66. package/harness/bundles/general/codebuddy/scripts/harness_review.py +326 -0
  67. package/harness/bundles/general/codebuddy/scripts/harness_sync.py +407 -0
  68. package/harness/bundles/general/codebuddy/scripts/harness_test_guard.py +244 -49
  69. package/harness/bundles/general/codex/.harness-build.json +1 -1
  70. package/harness/bundles/general/codex/harness-archive/SKILL.md +1 -1
  71. package/harness/bundles/general/codex/harness-archive/reference.md +12 -2
  72. package/harness/bundles/general/codex/harness-archive/templates/summary-data-template.json +6 -2
  73. package/harness/bundles/general/codex/harness-codebase-map/SKILL.md +1 -1
  74. package/harness/bundles/general/codex/harness-knowledge-ingest/SKILL.md +3 -1
  75. package/harness/bundles/general/codex/harness-knowledge-ingest/reference.md +7 -0
  76. package/harness/bundles/general/codex/harness-knowledge-ingest/scripts/harness_knowledge.py +117 -0
  77. package/harness/bundles/general/codex/harness-knowledge-query/SKILL.md +1 -1
  78. package/harness/bundles/general/codex/harness-plan/SKILL.md +1 -1
  79. package/harness/bundles/general/codex/harness-review/SKILL.md +1 -1
  80. package/harness/bundles/general/codex/harness-run/SKILL.md +1 -1
  81. package/harness/bundles/general/codex/harness-run/reference.md +2 -0
  82. package/harness/bundles/general/codex/harness-submit/SKILL.md +15 -10
  83. package/harness/bundles/general/codex/harness-submit/checklist.md +40 -76
  84. package/harness/bundles/general/codex/harness-sync/SKILL.md +17 -2
  85. package/harness/bundles/general/codex/harness-sync/reference.md +2 -2
  86. package/harness/bundles/general/codex/harness-test/SKILL.md +1 -1
  87. package/harness/bundles/general/codex/harness-test/reference.md +2 -0
  88. package/harness/bundles/general/codex/protocols/archive-report-protocol.md +24 -4
  89. package/harness/bundles/general/codex/protocols/ledger-protocol.md +36 -0
  90. package/harness/bundles/general/codex/protocols/state-layout-protocol.md +15 -0
  91. package/harness/bundles/general/codex/scripts/harness_archive.py +803 -274
  92. package/harness/bundles/general/codex/scripts/harness_change.py +18 -0
  93. package/harness/bundles/general/codex/scripts/harness_deploy.py +1 -44
  94. package/harness/bundles/general/codex/scripts/harness_events.py +104 -6
  95. package/harness/bundles/general/codex/scripts/harness_gate.py +25 -4
  96. package/harness/bundles/general/codex/scripts/harness_integration.py +878 -0
  97. package/harness/bundles/general/codex/scripts/harness_ledger.py +440 -10
  98. package/harness/bundles/general/codex/scripts/harness_paths.py +315 -0
  99. package/harness/bundles/general/codex/scripts/harness_profile.py +45 -0
  100. package/harness/bundles/general/codex/scripts/harness_review.py +326 -0
  101. package/harness/bundles/general/codex/scripts/harness_sync.py +407 -0
  102. package/harness/bundles/general/codex/scripts/harness_test_guard.py +244 -49
  103. package/harness/bundles/general/cursor/.harness-build.json +1 -1
  104. package/harness/bundles/general/cursor/harness-archive/SKILL.md +1 -1
  105. package/harness/bundles/general/cursor/harness-archive/reference.md +12 -2
  106. package/harness/bundles/general/cursor/harness-archive/templates/summary-data-template.json +6 -2
  107. package/harness/bundles/general/cursor/harness-codebase-map/SKILL.md +1 -1
  108. package/harness/bundles/general/cursor/harness-knowledge-ingest/SKILL.md +3 -1
  109. package/harness/bundles/general/cursor/harness-knowledge-ingest/reference.md +7 -0
  110. package/harness/bundles/general/cursor/harness-knowledge-ingest/scripts/harness_knowledge.py +117 -0
  111. package/harness/bundles/general/cursor/harness-knowledge-query/SKILL.md +1 -1
  112. package/harness/bundles/general/cursor/harness-plan/SKILL.md +1 -1
  113. package/harness/bundles/general/cursor/harness-review/SKILL.md +1 -1
  114. package/harness/bundles/general/cursor/harness-run/SKILL.md +1 -1
  115. package/harness/bundles/general/cursor/harness-run/reference.md +2 -0
  116. package/harness/bundles/general/cursor/harness-submit/SKILL.md +15 -10
  117. package/harness/bundles/general/cursor/harness-submit/checklist.md +40 -76
  118. package/harness/bundles/general/cursor/harness-sync/SKILL.md +17 -2
  119. package/harness/bundles/general/cursor/harness-sync/reference.md +2 -2
  120. package/harness/bundles/general/cursor/harness-test/SKILL.md +1 -1
  121. package/harness/bundles/general/cursor/harness-test/reference.md +2 -0
  122. package/harness/bundles/general/cursor/protocols/archive-report-protocol.md +24 -4
  123. package/harness/bundles/general/cursor/protocols/ledger-protocol.md +36 -0
  124. package/harness/bundles/general/cursor/protocols/state-layout-protocol.md +15 -0
  125. package/harness/bundles/general/cursor/scripts/harness_archive.py +803 -274
  126. package/harness/bundles/general/cursor/scripts/harness_change.py +18 -0
  127. package/harness/bundles/general/cursor/scripts/harness_deploy.py +1 -44
  128. package/harness/bundles/general/cursor/scripts/harness_events.py +104 -6
  129. package/harness/bundles/general/cursor/scripts/harness_gate.py +25 -4
  130. package/harness/bundles/general/cursor/scripts/harness_integration.py +878 -0
  131. package/harness/bundles/general/cursor/scripts/harness_ledger.py +440 -10
  132. package/harness/bundles/general/cursor/scripts/harness_paths.py +315 -0
  133. package/harness/bundles/general/cursor/scripts/harness_profile.py +45 -0
  134. package/harness/bundles/general/cursor/scripts/harness_review.py +326 -0
  135. package/harness/bundles/general/cursor/scripts/harness_sync.py +407 -0
  136. package/harness/bundles/general/cursor/scripts/harness_test_guard.py +244 -49
  137. package/harness/bundles/java/claude-code/.harness-build.json +1 -1
  138. package/harness/bundles/java/claude-code/harness-apidoc/SKILL.md +1 -1
  139. package/harness/bundles/java/claude-code/harness-archive/SKILL.md +1 -1
  140. package/harness/bundles/java/claude-code/harness-archive/reference.md +12 -2
  141. package/harness/bundles/java/claude-code/harness-archive/templates/summary-data-template.json +6 -2
  142. package/harness/bundles/java/claude-code/harness-codebase-map/SKILL.md +1 -1
  143. package/harness/bundles/java/claude-code/harness-knowledge-ingest/SKILL.md +3 -1
  144. package/harness/bundles/java/claude-code/harness-knowledge-ingest/reference.md +7 -0
  145. package/harness/bundles/java/claude-code/harness-knowledge-ingest/scripts/harness_knowledge.py +117 -0
  146. package/harness/bundles/java/claude-code/harness-knowledge-query/SKILL.md +1 -1
  147. package/harness/bundles/java/claude-code/harness-package/SKILL.md +1 -1
  148. package/harness/bundles/java/claude-code/harness-plan/SKILL.md +1 -1
  149. package/harness/bundles/java/claude-code/harness-review/SKILL.md +3 -3
  150. package/harness/bundles/java/claude-code/harness-run/SKILL.md +1 -1
  151. package/harness/bundles/java/claude-code/harness-submit/SKILL.md +15 -10
  152. package/harness/bundles/java/claude-code/harness-submit/checklist.md +40 -76
  153. package/harness/bundles/java/claude-code/harness-sync/SKILL.md +17 -2
  154. package/harness/bundles/java/claude-code/harness-sync/reference.md +2 -2
  155. package/harness/bundles/java/claude-code/harness-test/SKILL.md +1 -1
  156. package/harness/bundles/java/claude-code/protocols/archive-report-protocol.md +24 -4
  157. package/harness/bundles/java/claude-code/protocols/ledger-protocol.md +36 -0
  158. package/harness/bundles/java/claude-code/protocols/state-layout-protocol.md +15 -0
  159. package/harness/bundles/java/claude-code/scripts/harness_archive.py +803 -274
  160. package/harness/bundles/java/claude-code/scripts/harness_change.py +18 -0
  161. package/harness/bundles/java/claude-code/scripts/harness_deploy.py +1 -44
  162. package/harness/bundles/java/claude-code/scripts/harness_events.py +104 -6
  163. package/harness/bundles/java/claude-code/scripts/harness_gate.py +25 -4
  164. package/harness/bundles/java/claude-code/scripts/harness_integration.py +878 -0
  165. package/harness/bundles/java/claude-code/scripts/harness_ledger.py +440 -10
  166. package/harness/bundles/java/claude-code/scripts/harness_paths.py +315 -0
  167. package/harness/bundles/java/claude-code/scripts/harness_profile.py +45 -0
  168. package/harness/bundles/java/claude-code/scripts/harness_review.py +326 -0
  169. package/harness/bundles/java/claude-code/scripts/harness_sync.py +407 -0
  170. package/harness/bundles/java/claude-code/scripts/harness_test_guard.py +244 -49
  171. package/harness/bundles/java/codebuddy/.harness-build.json +1 -1
  172. package/harness/bundles/java/codebuddy/harness-apidoc/SKILL.md +1 -1
  173. package/harness/bundles/java/codebuddy/harness-archive/SKILL.md +1 -1
  174. package/harness/bundles/java/codebuddy/harness-archive/reference.md +12 -2
  175. package/harness/bundles/java/codebuddy/harness-archive/templates/summary-data-template.json +6 -2
  176. package/harness/bundles/java/codebuddy/harness-codebase-map/SKILL.md +1 -1
  177. package/harness/bundles/java/codebuddy/harness-knowledge-ingest/SKILL.md +3 -1
  178. package/harness/bundles/java/codebuddy/harness-knowledge-ingest/reference.md +7 -0
  179. package/harness/bundles/java/codebuddy/harness-knowledge-ingest/scripts/harness_knowledge.py +117 -0
  180. package/harness/bundles/java/codebuddy/harness-knowledge-query/SKILL.md +1 -1
  181. package/harness/bundles/java/codebuddy/harness-package/SKILL.md +1 -1
  182. package/harness/bundles/java/codebuddy/harness-plan/SKILL.md +1 -1
  183. package/harness/bundles/java/codebuddy/harness-review/SKILL.md +3 -3
  184. package/harness/bundles/java/codebuddy/harness-run/SKILL.md +1 -1
  185. package/harness/bundles/java/codebuddy/harness-submit/SKILL.md +15 -10
  186. package/harness/bundles/java/codebuddy/harness-submit/checklist.md +40 -76
  187. package/harness/bundles/java/codebuddy/harness-sync/SKILL.md +17 -2
  188. package/harness/bundles/java/codebuddy/harness-sync/reference.md +2 -2
  189. package/harness/bundles/java/codebuddy/harness-test/SKILL.md +1 -1
  190. package/harness/bundles/java/codebuddy/protocols/archive-report-protocol.md +24 -4
  191. package/harness/bundles/java/codebuddy/protocols/ledger-protocol.md +36 -0
  192. package/harness/bundles/java/codebuddy/protocols/state-layout-protocol.md +15 -0
  193. package/harness/bundles/java/codebuddy/scripts/harness_archive.py +803 -274
  194. package/harness/bundles/java/codebuddy/scripts/harness_change.py +18 -0
  195. package/harness/bundles/java/codebuddy/scripts/harness_deploy.py +1 -44
  196. package/harness/bundles/java/codebuddy/scripts/harness_events.py +104 -6
  197. package/harness/bundles/java/codebuddy/scripts/harness_gate.py +25 -4
  198. package/harness/bundles/java/codebuddy/scripts/harness_integration.py +878 -0
  199. package/harness/bundles/java/codebuddy/scripts/harness_ledger.py +440 -10
  200. package/harness/bundles/java/codebuddy/scripts/harness_paths.py +315 -0
  201. package/harness/bundles/java/codebuddy/scripts/harness_profile.py +45 -0
  202. package/harness/bundles/java/codebuddy/scripts/harness_review.py +326 -0
  203. package/harness/bundles/java/codebuddy/scripts/harness_sync.py +407 -0
  204. package/harness/bundles/java/codebuddy/scripts/harness_test_guard.py +244 -49
  205. package/harness/bundles/java/codex/.harness-build.json +1 -1
  206. package/harness/bundles/java/codex/harness-apidoc/SKILL.md +1 -1
  207. package/harness/bundles/java/codex/harness-archive/SKILL.md +1 -1
  208. package/harness/bundles/java/codex/harness-archive/reference.md +12 -2
  209. package/harness/bundles/java/codex/harness-archive/templates/summary-data-template.json +6 -2
  210. package/harness/bundles/java/codex/harness-codebase-map/SKILL.md +1 -1
  211. package/harness/bundles/java/codex/harness-knowledge-ingest/SKILL.md +3 -1
  212. package/harness/bundles/java/codex/harness-knowledge-ingest/reference.md +7 -0
  213. package/harness/bundles/java/codex/harness-knowledge-ingest/scripts/harness_knowledge.py +117 -0
  214. package/harness/bundles/java/codex/harness-knowledge-query/SKILL.md +1 -1
  215. package/harness/bundles/java/codex/harness-package/SKILL.md +1 -1
  216. package/harness/bundles/java/codex/harness-plan/SKILL.md +1 -1
  217. package/harness/bundles/java/codex/harness-review/SKILL.md +1 -1
  218. package/harness/bundles/java/codex/harness-run/SKILL.md +1 -1
  219. package/harness/bundles/java/codex/harness-submit/SKILL.md +15 -10
  220. package/harness/bundles/java/codex/harness-submit/checklist.md +40 -76
  221. package/harness/bundles/java/codex/harness-sync/SKILL.md +17 -2
  222. package/harness/bundles/java/codex/harness-sync/reference.md +2 -2
  223. package/harness/bundles/java/codex/harness-test/SKILL.md +1 -1
  224. package/harness/bundles/java/codex/protocols/archive-report-protocol.md +24 -4
  225. package/harness/bundles/java/codex/protocols/ledger-protocol.md +36 -0
  226. package/harness/bundles/java/codex/protocols/state-layout-protocol.md +15 -0
  227. package/harness/bundles/java/codex/scripts/harness_archive.py +803 -274
  228. package/harness/bundles/java/codex/scripts/harness_change.py +18 -0
  229. package/harness/bundles/java/codex/scripts/harness_deploy.py +1 -44
  230. package/harness/bundles/java/codex/scripts/harness_events.py +104 -6
  231. package/harness/bundles/java/codex/scripts/harness_gate.py +25 -4
  232. package/harness/bundles/java/codex/scripts/harness_integration.py +878 -0
  233. package/harness/bundles/java/codex/scripts/harness_ledger.py +440 -10
  234. package/harness/bundles/java/codex/scripts/harness_paths.py +315 -0
  235. package/harness/bundles/java/codex/scripts/harness_profile.py +45 -0
  236. package/harness/bundles/java/codex/scripts/harness_review.py +326 -0
  237. package/harness/bundles/java/codex/scripts/harness_sync.py +407 -0
  238. package/harness/bundles/java/codex/scripts/harness_test_guard.py +244 -49
  239. package/harness/bundles/java/cursor/.harness-build.json +1 -1
  240. package/harness/bundles/java/cursor/harness-apidoc/SKILL.md +1 -1
  241. package/harness/bundles/java/cursor/harness-archive/SKILL.md +1 -1
  242. package/harness/bundles/java/cursor/harness-archive/reference.md +12 -2
  243. package/harness/bundles/java/cursor/harness-archive/templates/summary-data-template.json +6 -2
  244. package/harness/bundles/java/cursor/harness-codebase-map/SKILL.md +1 -1
  245. package/harness/bundles/java/cursor/harness-knowledge-ingest/SKILL.md +3 -1
  246. package/harness/bundles/java/cursor/harness-knowledge-ingest/reference.md +7 -0
  247. package/harness/bundles/java/cursor/harness-knowledge-ingest/scripts/harness_knowledge.py +117 -0
  248. package/harness/bundles/java/cursor/harness-knowledge-query/SKILL.md +1 -1
  249. package/harness/bundles/java/cursor/harness-package/SKILL.md +1 -1
  250. package/harness/bundles/java/cursor/harness-plan/SKILL.md +1 -1
  251. package/harness/bundles/java/cursor/harness-review/SKILL.md +1 -1
  252. package/harness/bundles/java/cursor/harness-run/SKILL.md +1 -1
  253. package/harness/bundles/java/cursor/harness-submit/SKILL.md +15 -10
  254. package/harness/bundles/java/cursor/harness-submit/checklist.md +40 -76
  255. package/harness/bundles/java/cursor/harness-sync/SKILL.md +17 -2
  256. package/harness/bundles/java/cursor/harness-sync/reference.md +2 -2
  257. package/harness/bundles/java/cursor/harness-test/SKILL.md +1 -1
  258. package/harness/bundles/java/cursor/protocols/archive-report-protocol.md +24 -4
  259. package/harness/bundles/java/cursor/protocols/ledger-protocol.md +36 -0
  260. package/harness/bundles/java/cursor/protocols/state-layout-protocol.md +15 -0
  261. package/harness/bundles/java/cursor/scripts/harness_archive.py +803 -274
  262. package/harness/bundles/java/cursor/scripts/harness_change.py +18 -0
  263. package/harness/bundles/java/cursor/scripts/harness_deploy.py +1 -44
  264. package/harness/bundles/java/cursor/scripts/harness_events.py +104 -6
  265. package/harness/bundles/java/cursor/scripts/harness_gate.py +25 -4
  266. package/harness/bundles/java/cursor/scripts/harness_integration.py +878 -0
  267. package/harness/bundles/java/cursor/scripts/harness_ledger.py +440 -10
  268. package/harness/bundles/java/cursor/scripts/harness_paths.py +315 -0
  269. package/harness/bundles/java/cursor/scripts/harness_profile.py +45 -0
  270. package/harness/bundles/java/cursor/scripts/harness_review.py +326 -0
  271. package/harness/bundles/java/cursor/scripts/harness_sync.py +407 -0
  272. package/harness/bundles/java/cursor/scripts/harness_test_guard.py +244 -49
  273. package/harness/manifests/general/claude-code.json +47 -31
  274. package/harness/manifests/general/codebuddy.json +47 -31
  275. package/harness/manifests/general/codex.json +47 -31
  276. package/harness/manifests/general/cursor.json +47 -31
  277. package/harness/manifests/java/claude-code.json +47 -31
  278. package/harness/manifests/java/codebuddy.json +47 -31
  279. package/harness/manifests/java/codex.json +47 -31
  280. package/harness/manifests/java/cursor.json +47 -31
  281. package/hunter-workflow-family.json +2 -2
  282. package/package.json +1 -1
@@ -0,0 +1,878 @@
1
+ #!/usr/bin/env python3
2
+ """Harness integration transaction.
3
+
4
+ Reentrant, journaled submit pipeline that never stashes, moves or overwrites
5
+ the primary worktree. All target-branch mutation happens inside a temporary
6
+ integration worktree; the journal is the single contract for step order,
7
+ identities and recovery.
8
+
9
+ Steps: preflight → prepare → merge → verify → push → cleanup.
10
+ Journal: ``.harness/state/integration/<transaction-id>/journal.json``.
11
+
12
+ Python 3.10+, stdlib only.
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import argparse
18
+ import datetime as dt
19
+ import hashlib
20
+ import json
21
+ import os
22
+ import re
23
+ import shlex
24
+ import shutil
25
+ import subprocess
26
+ import sys
27
+ import time
28
+ import uuid
29
+ from pathlib import Path
30
+ from typing import Any, Callable, Sequence
31
+
32
+ SCRIPTS_DIR = Path(__file__).resolve().parent
33
+ if str(SCRIPTS_DIR) not in sys.path:
34
+ sys.path.insert(0, str(SCRIPTS_DIR))
35
+
36
+ import harness_change # noqa: E402
37
+ import harness_ledger as hl # noqa: E402
38
+ import harness_paths # noqa: E402
39
+
40
+ if hasattr(sys.stdout, "reconfigure"):
41
+ sys.stdout.reconfigure(encoding="utf-8")
42
+ if hasattr(sys.stderr, "reconfigure"):
43
+ sys.stderr.reconfigure(encoding="utf-8")
44
+
45
+ STEP_ORDER = ["preflight", "prepare", "merge", "verify", "push", "cleanup"]
46
+ _FOREIGN_PATTERNS = (
47
+ re.compile(r"^\.harness/changes/([^/]+)(/|$)"),
48
+ re.compile(r"^\.harness/state/changes/([^/]+)(/|$)"),
49
+ )
50
+
51
+
52
+ def now_iso() -> str:
53
+ return dt.datetime.now().astimezone().isoformat(timespec="milliseconds")
54
+
55
+
56
+ class IntegrationError(Exception):
57
+ code = "INTEGRATION_ERROR"
58
+
59
+
60
+ class IntegrationLockHeldError(IntegrationError):
61
+ code = "INTEGRATION_LOCK_HELD"
62
+
63
+
64
+ class MergeFailedError(IntegrationError):
65
+ code = "MERGE_FAILED"
66
+
67
+
68
+ class ForeignChangePathsError(IntegrationError):
69
+ code = "FOREIGN_CHANGE_PATHS"
70
+
71
+
72
+ class VerificationFailedError(IntegrationError):
73
+ code = "VERIFICATION_FAILED"
74
+
75
+
76
+ class TargetMovedError(IntegrationError):
77
+ code = "TARGET_MOVED"
78
+
79
+
80
+ class CleanupRefusedError(IntegrationError):
81
+ code = "CLEANUP_REFUSED"
82
+
83
+
84
+ class JournalConflictError(IntegrationError):
85
+ code = "JOURNAL_CONFLICT"
86
+
87
+
88
+ JOURNAL_LOCK_STALE_SECONDS = 300
89
+
90
+
91
+ class GitRunner:
92
+ """Recordable git invocation wrapper (command spy for audits)."""
93
+
94
+ def __init__(self) -> None:
95
+ self.history: list[tuple[str, tuple[str, ...]]] = []
96
+
97
+ def run(self, cwd: Path, *args: str, check: bool = True) -> subprocess.CompletedProcess:
98
+ self.history.append((str(cwd), tuple(args)))
99
+ proc = subprocess.run(
100
+ ["git", *args],
101
+ cwd=str(cwd),
102
+ capture_output=True,
103
+ text=True,
104
+ encoding="utf-8",
105
+ errors="replace",
106
+ check=False,
107
+ )
108
+ if check and proc.returncode != 0:
109
+ raise IntegrationError(
110
+ f"git {' '.join(args)} failed ({proc.returncode}): {proc.stderr.strip()}"
111
+ )
112
+ return proc
113
+
114
+ def text(self, cwd: Path, *args: str) -> str | None:
115
+ proc = self.run(cwd, *args, check=False)
116
+ if proc.returncode != 0:
117
+ return None
118
+ return proc.stdout.strip()
119
+
120
+
121
+ def _sanitize(value: str) -> str:
122
+ return re.sub(r"[^A-Za-z0-9._-]+", "-", value).strip("-") or "txn"
123
+
124
+
125
+ def journal_dir(project_root: Path, transaction_id: str) -> Path:
126
+ return (
127
+ Path(project_root)
128
+ / ".harness"
129
+ / "state"
130
+ / "integration"
131
+ / transaction_id
132
+ )
133
+
134
+
135
+ def journal_path(project_root: Path, transaction_id: str) -> Path:
136
+ return journal_dir(project_root, transaction_id) / "journal.json"
137
+
138
+
139
+ def load_journal(project_root: Path, transaction_id: str) -> dict[str, Any]:
140
+ path = journal_path(project_root, transaction_id)
141
+ if not path.is_file():
142
+ raise FileNotFoundError(f"journal not found: {path}")
143
+ return json.loads(path.read_text(encoding="utf-8-sig"))
144
+
145
+
146
+ def _pid_alive(pid: int) -> bool:
147
+ if pid <= 0:
148
+ return False
149
+ try:
150
+ os.kill(pid, 0)
151
+ except ProcessLookupError:
152
+ return False
153
+ except PermissionError:
154
+ return True
155
+ except OSError:
156
+ return False
157
+ return True
158
+
159
+
160
+ def _acquire_journal_lock(lock_path: Path, journal_path: Path) -> int:
161
+ for _attempt in range(2):
162
+ try:
163
+ descriptor = os.open(
164
+ str(lock_path), os.O_CREAT | os.O_EXCL | os.O_WRONLY, 0o600
165
+ )
166
+ payload = json.dumps(
167
+ {"pid": os.getpid(), "createdAtEpoch": time.time()},
168
+ separators=(",", ":"),
169
+ ).encode("utf-8")
170
+ try:
171
+ os.write(descriptor, payload)
172
+ os.fsync(descriptor)
173
+ except BaseException:
174
+ os.close(descriptor)
175
+ lock_path.unlink(missing_ok=True)
176
+ raise
177
+ return descriptor
178
+ except FileExistsError as exc:
179
+ stale = False
180
+ try:
181
+ owner = json.loads(lock_path.read_text(encoding="utf-8-sig"))
182
+ owner_pid = int(owner.get("pid") or 0)
183
+ created = float(owner.get("createdAtEpoch") or 0)
184
+ stale = not _pid_alive(owner_pid) and (
185
+ created <= 0
186
+ or time.time() - created > JOURNAL_LOCK_STALE_SECONDS
187
+ )
188
+ except (OSError, ValueError, TypeError, json.JSONDecodeError):
189
+ try:
190
+ stale = (
191
+ time.time() - lock_path.stat().st_mtime
192
+ > JOURNAL_LOCK_STALE_SECONDS
193
+ )
194
+ except OSError:
195
+ stale = True
196
+ if stale:
197
+ try:
198
+ lock_path.unlink()
199
+ except FileNotFoundError:
200
+ pass
201
+ continue
202
+ raise JournalConflictError(
203
+ f"journal writer already active: {journal_path}"
204
+ ) from exc
205
+ raise JournalConflictError(f"could not reclaim stale journal lock: {journal_path}")
206
+
207
+
208
+ def _write_journal(project_root: Path, journal: dict[str, Any]) -> None:
209
+ path = journal_path(project_root, journal["transactionId"])
210
+ path.parent.mkdir(parents=True, exist_ok=True)
211
+ lock_path = path.with_name(path.name + ".lock")
212
+ lock_fd = _acquire_journal_lock(lock_path, path)
213
+ os.close(lock_fd)
214
+ expected_revision = int(journal.get("revision") or 0)
215
+ try:
216
+ if path.is_file():
217
+ current = json.loads(path.read_text(encoding="utf-8-sig"))
218
+ current_revision = int(current.get("revision") or 0)
219
+ if current_revision != expected_revision:
220
+ raise JournalConflictError(
221
+ f"stale journal revision: expected {expected_revision}, "
222
+ f"current {current_revision}"
223
+ )
224
+ elif expected_revision != 0:
225
+ raise JournalConflictError(
226
+ f"journal disappeared at revision {expected_revision}: {path}"
227
+ )
228
+ journal["revision"] = expected_revision + 1
229
+ text = json.dumps(journal, ensure_ascii=False, indent=2) + "\n"
230
+ tmp = path.with_name(f".{path.name}.{os.getpid()}.{uuid.uuid4().hex}.tmp")
231
+ try:
232
+ tmp.write_text(text, encoding="utf-8", newline="\n")
233
+ os.replace(tmp, path)
234
+ except BaseException:
235
+ tmp.unlink(missing_ok=True)
236
+ raise
237
+ finally:
238
+ lock_path.unlink(missing_ok=True)
239
+
240
+
241
+ class IntegrationTransaction:
242
+ def __init__(
243
+ self,
244
+ *,
245
+ project_root: Path,
246
+ change_id: str,
247
+ run_id: str,
248
+ target_branch: str,
249
+ feature_branch: str,
250
+ temp_root: Path,
251
+ runner: GitRunner | None = None,
252
+ ) -> None:
253
+ self.project_root = harness_paths.resolve_main_project_root(Path(project_root))
254
+ self.change_id = change_id
255
+ self.run_id = run_id
256
+ self.target_branch = target_branch
257
+ self.feature_branch = feature_branch
258
+ self.temp_root = Path(temp_root).resolve()
259
+ self.runner = runner or GitRunner()
260
+ self.transaction_id = f"{_sanitize(change_id)}-{_sanitize(run_id)}"
261
+ self.temp_branch = f"harness/integration/{self.transaction_id}"
262
+
263
+ # ------------------------------------------------------------- journal
264
+
265
+ def _new_journal(self, base: str, feature_head: str) -> dict[str, Any]:
266
+ return {
267
+ "schemaVersion": 1,
268
+ "transactionId": self.transaction_id,
269
+ "changeName": self.change_id,
270
+ "runId": self.run_id,
271
+ "repositoryId": harness_paths.repository_identity(self.project_root),
272
+ "primaryRoot": str(self.project_root),
273
+ "targetBranch": self.target_branch,
274
+ "featureBranch": self.feature_branch,
275
+ "base": base,
276
+ "featureHead": feature_head,
277
+ "evidenceIdentity": self._evidence_identity(),
278
+ "mergeCommit": None,
279
+ "pushedHead": None,
280
+ "integrationRoot": str(self.temp_root / self.transaction_id),
281
+ "allowedCleanupRoot": str(self.temp_root / self.transaction_id),
282
+ "protectionRefs": {
283
+ "base": f"refs/harness/integration/{self.transaction_id}/base",
284
+ "head": f"refs/harness/integration/{self.transaction_id}/head",
285
+ },
286
+ "steps": [
287
+ {"name": name, "status": "PENDING", "attempt": 0}
288
+ for name in STEP_ORDER
289
+ ],
290
+ "createdAt": now_iso(),
291
+ }
292
+
293
+ def _evidence_identity(self) -> dict[str, Any]:
294
+ change_dir = (
295
+ self.project_root / ".harness" / "changes" / self.change_id
296
+ ).resolve()
297
+ state_candidates = [
298
+ self.project_root / ".harness" / "state" / "changes" / self.change_id,
299
+ change_dir,
300
+ ]
301
+ events_file = next(
302
+ (root / "events.ndjson" for root in state_candidates if (root / "events.ndjson").is_file()),
303
+ None,
304
+ )
305
+ event_count = 0
306
+ last_event_id = None
307
+ event_hash = None
308
+ if events_file is not None:
309
+ raw = events_file.read_bytes()
310
+ event_hash = "sha256:" + hashlib.sha256(raw).hexdigest()
311
+ for line in raw.decode("utf-8", "replace").splitlines():
312
+ if not line.strip():
313
+ continue
314
+ event_count += 1
315
+ try:
316
+ item = json.loads(line)
317
+ except json.JSONDecodeError:
318
+ continue
319
+ last_event_id = item.get("id") or item.get("eventId") or last_event_id
320
+
321
+ artifact_hash = None
322
+ ledger_identity: dict[str, Any] = {}
323
+ for root in state_candidates:
324
+ for rel in (
325
+ Path("evidence/artifact-manifest.json"),
326
+ Path("evidence/archive-manifest-before.json"),
327
+ Path("evidence/test-tracking.json"),
328
+ ):
329
+ candidate = root / rel
330
+ if candidate.is_file():
331
+ artifact_hash = "sha256:" + hashlib.sha256(candidate.read_bytes()).hexdigest()
332
+ break
333
+ if artifact_hash:
334
+ break
335
+ for root in state_candidates:
336
+ ledger_path = root / "evidence" / "verification-ledger.json"
337
+ if not ledger_path.is_file():
338
+ continue
339
+ try:
340
+ ledger = json.loads(ledger_path.read_text(encoding="utf-8-sig"))
341
+ except (OSError, json.JSONDecodeError):
342
+ break
343
+ for key in (
344
+ "schemaVersion",
345
+ "repositoryId",
346
+ "changeName",
347
+ "baseCommit",
348
+ "currentHead",
349
+ "diffHash",
350
+ "ownershipHash",
351
+ ):
352
+ ledger_identity[key] = ledger.get(key)
353
+ break
354
+ return {
355
+ "eventHighWater": {
356
+ "count": event_count,
357
+ "lastEventId": last_event_id,
358
+ "sha256": event_hash,
359
+ },
360
+ "artifactManifestHash": artifact_hash,
361
+ "ledgerIdentity": ledger_identity,
362
+ }
363
+
364
+ def _load(self) -> dict[str, Any]:
365
+ return load_journal(self.project_root, self.transaction_id)
366
+
367
+ def _save(self, journal: dict[str, Any]) -> dict[str, Any]:
368
+ _write_journal(self.project_root, journal)
369
+ return journal
370
+
371
+ def _step(self, journal: dict[str, Any], name: str) -> dict[str, Any]:
372
+ return next(s for s in journal["steps"] if s["name"] == name)
373
+
374
+ def _run_step(
375
+ self,
376
+ name: str,
377
+ action: Callable[[dict[str, Any]], None],
378
+ ) -> dict[str, Any]:
379
+ journal = self._load()
380
+ step = self._step(journal, name)
381
+ if step["status"] == "DONE":
382
+ step["reentry"] = "REUSED"
383
+ return self._save(journal)
384
+ step["status"] = "RUNNING"
385
+ step["attempt"] = int(step.get("attempt", 0)) + 1
386
+ step["startedAt"] = now_iso()
387
+ step.pop("error", None)
388
+ self._save(journal)
389
+ try:
390
+ action(journal)
391
+ except IntegrationError as exc:
392
+ journal = self._load()
393
+ step = self._step(journal, name)
394
+ step["status"] = "FAILED"
395
+ step["error"] = f"{exc.code}: {exc}"
396
+ step["finishedAt"] = now_iso()
397
+ self._save(journal)
398
+ raise
399
+ except Exception as exc: # unexpected: still journal the failure
400
+ journal = self._load()
401
+ step = self._step(journal, name)
402
+ step["status"] = "FAILED"
403
+ step["error"] = f"UNEXPECTED: {exc}"
404
+ step["finishedAt"] = now_iso()
405
+ self._save(journal)
406
+ raise
407
+ journal = self._load()
408
+ step = self._step(journal, name)
409
+ step["status"] = "DONE"
410
+ step["finishedAt"] = now_iso()
411
+ return self._save(journal)
412
+
413
+ # -------------------------------------------------------------- steps
414
+
415
+ def preflight(self) -> dict[str, Any]:
416
+ def action(journal: dict[str, Any]) -> None:
417
+ base = self.runner.text(
418
+ self.project_root, "rev-parse", self.target_branch
419
+ )
420
+ feature_head = self.runner.text(
421
+ self.project_root, "rev-parse", self.feature_branch
422
+ )
423
+ if not base or not feature_head:
424
+ raise IntegrationError("target or feature branch not found")
425
+ fresh = self._new_journal(base, feature_head)
426
+ fresh["revision"] = int(journal.get("revision") or 0)
427
+ journal.clear()
428
+ journal.update(fresh)
429
+ # Re-mark preflight RUNNING on the fresh journal.
430
+ step = self._step(journal, "preflight")
431
+ step["status"] = "RUNNING"
432
+ step["attempt"] = 1
433
+ step["startedAt"] = now_iso()
434
+ refs = journal["protectionRefs"]
435
+ self.runner.run(
436
+ self.project_root, "update-ref", refs["base"], base
437
+ )
438
+ self.runner.run(
439
+ self.project_root, "update-ref", refs["head"], feature_head
440
+ )
441
+ self._save(journal)
442
+
443
+ try:
444
+ existing = self._load()
445
+ step = self._step(existing, "preflight")
446
+ if step["status"] == "DONE":
447
+ step["reentry"] = "REUSED"
448
+ return self._save(existing)
449
+ except FileNotFoundError:
450
+ pass
451
+
452
+ # Acquire the target lock BEFORE writing any state: a rejected
453
+ # contender must leave the worktree and state files untouched.
454
+ lock = harness_change.integration_lock_acquire(
455
+ self.project_root, run_id=self.run_id
456
+ )
457
+ if not lock.get("ok"):
458
+ raise IntegrationLockHeldError(
459
+ f"integration lock held: {lock.get('holder')}"
460
+ )
461
+
462
+ # Bootstrap: write an initial journal so _run_step has something to load.
463
+ initial = {
464
+ "schemaVersion": 1,
465
+ "transactionId": self.transaction_id,
466
+ "changeName": self.change_id,
467
+ "runId": self.run_id,
468
+ "steps": [
469
+ {"name": name, "status": "PENDING", "attempt": 0}
470
+ for name in STEP_ORDER
471
+ ],
472
+ }
473
+ self._save(initial)
474
+ return self._run_step("preflight", action)
475
+
476
+ def prepare(self) -> dict[str, Any]:
477
+ def action(journal: dict[str, Any]) -> None:
478
+ # Best-effort fetch; absence of a remote is tolerated.
479
+ self.runner.run(
480
+ self.project_root,
481
+ "fetch",
482
+ "origin",
483
+ self.target_branch,
484
+ check=False,
485
+ )
486
+ base = journal["base"]
487
+ self.runner.run(
488
+ self.project_root, "branch", "-f", self.temp_branch, base
489
+ )
490
+ intg_root = Path(journal["integrationRoot"])
491
+ intg_root.parent.mkdir(parents=True, exist_ok=True)
492
+ self.runner.run(
493
+ self.project_root,
494
+ "worktree",
495
+ "add",
496
+ str(intg_root),
497
+ self.temp_branch,
498
+ )
499
+
500
+ return self._run_step("prepare", action)
501
+
502
+ def _foreign_paths(self, journal: dict[str, Any]) -> list[str]:
503
+ diff = self.runner.text(
504
+ self.project_root,
505
+ "diff",
506
+ "--name-only",
507
+ journal["base"],
508
+ journal["featureHead"],
509
+ )
510
+ contract: dict[str, Any] = {}
511
+ contract_path = (
512
+ self.project_root
513
+ / ".harness"
514
+ / "changes"
515
+ / self.change_id
516
+ / "meta"
517
+ / "change-context.json"
518
+ )
519
+ if contract_path.is_file():
520
+ try:
521
+ contract = json.loads(contract_path.read_text(encoding="utf-8-sig"))
522
+ except (OSError, json.JSONDecodeError):
523
+ contract = {}
524
+ if not contract:
525
+ shown = self.runner.text(
526
+ self.project_root,
527
+ "show",
528
+ f"{journal['featureHead']}:.harness/changes/{self.change_id}/meta/change-context.json",
529
+ )
530
+ if shown:
531
+ try:
532
+ contract = json.loads(shown)
533
+ except json.JSONDecodeError:
534
+ contract = {}
535
+ ownership = contract.get("ownership") or {}
536
+ scoped = bool(
537
+ ownership.get("productPaths") or ownership.get("staticEvidencePaths")
538
+ )
539
+ foreign: list[str] = []
540
+ for line in (diff or "").splitlines():
541
+ line = line.strip()
542
+ if not line:
543
+ continue
544
+ if scoped:
545
+ verdict = hl._classify_ownership_path(line, self.change_id, ownership)
546
+ if verdict == "foreign":
547
+ foreign.append(line)
548
+ continue
549
+ for pattern in _FOREIGN_PATTERNS:
550
+ match = pattern.match(line)
551
+ if match and match.group(1) != self.change_id:
552
+ foreign.append(line)
553
+ break
554
+ return foreign
555
+
556
+ def merge(self) -> dict[str, Any]:
557
+ def action(journal: dict[str, Any]) -> None:
558
+ current_target = self.runner.text(
559
+ self.project_root, "rev-parse", self.target_branch
560
+ )
561
+ if current_target != journal["base"]:
562
+ raise TargetMovedError(
563
+ f"local {self.target_branch} moved: expected "
564
+ f"{journal['base']}, found {current_target}"
565
+ )
566
+ foreign = self._foreign_paths(journal)
567
+ if foreign:
568
+ raise ForeignChangePathsError(
569
+ "merge diff touches other changes: " + ", ".join(sorted(foreign))
570
+ )
571
+ intg_root = Path(journal["integrationRoot"])
572
+ proc = self.runner.run(
573
+ intg_root,
574
+ "merge",
575
+ "--no-ff",
576
+ "-m",
577
+ f"harness: integrate {self.change_id} into {self.target_branch}",
578
+ self.feature_branch,
579
+ check=False,
580
+ )
581
+ if proc.returncode != 0:
582
+ raise MergeFailedError(proc.stderr.strip() or "merge conflict")
583
+ journal["mergeCommit"] = self.runner.text(intg_root, "rev-parse", "HEAD")
584
+ self._save(journal)
585
+
586
+ return self._run_step("merge", action)
587
+
588
+ def verify(self, commands: Sequence[Sequence[str]] | None = None) -> dict[str, Any]:
589
+ commands = list(commands or [])
590
+
591
+ def action(journal: dict[str, Any]) -> None:
592
+ intg_root = Path(journal["integrationRoot"]).resolve()
593
+ results: list[dict[str, Any]] = []
594
+ for command in commands:
595
+ started = dt.datetime.now().astimezone()
596
+ proc = subprocess.run(
597
+ list(command),
598
+ cwd=str(intg_root),
599
+ capture_output=True,
600
+ text=True,
601
+ encoding="utf-8",
602
+ errors="replace",
603
+ check=False,
604
+ )
605
+ finished = dt.datetime.now().astimezone()
606
+ results.append(
607
+ {
608
+ "command": list(command),
609
+ "exitCode": proc.returncode,
610
+ "startedAt": started.isoformat(timespec="milliseconds"),
611
+ "finishedAt": finished.isoformat(timespec="milliseconds"),
612
+ "durationMs": int(
613
+ (finished - started).total_seconds() * 1000
614
+ ),
615
+ }
616
+ )
617
+ if proc.returncode != 0:
618
+ journal["verifyResults"] = results
619
+ self._save(journal)
620
+ raise VerificationFailedError(
621
+ f"command failed ({proc.returncode}): {' '.join(command)}"
622
+ )
623
+ journal["verifyResults"] = results
624
+ journal["verifyCwd"] = str(intg_root)
625
+ basis = json.dumps(
626
+ {
627
+ "mergeCommit": journal.get("mergeCommit"),
628
+ "results": results,
629
+ "evidenceIdentity": journal.get("evidenceIdentity"),
630
+ },
631
+ ensure_ascii=False,
632
+ sort_keys=True,
633
+ )
634
+ journal["verificationIdentity"] = {
635
+ "sha256": "sha256:" + hashlib.sha256(basis.encode("utf-8")).hexdigest(),
636
+ "ledgerIdentity": (journal.get("evidenceIdentity") or {}).get("ledgerIdentity") or {},
637
+ }
638
+ self._save(journal)
639
+
640
+ return self._run_step("verify", action)
641
+
642
+ def push(self) -> dict[str, Any]:
643
+ def action(journal: dict[str, Any]) -> None:
644
+ remote_head = self.runner.text(
645
+ self.project_root, "ls-remote", "origin", self.target_branch
646
+ )
647
+ remote_head = (remote_head or "").split()[0] if remote_head else ""
648
+ if not remote_head:
649
+ raise IntegrationError(
650
+ f"remote branch not found: origin/{self.target_branch}"
651
+ )
652
+ merge_commit = journal.get("mergeCommit")
653
+ if not merge_commit:
654
+ raise IntegrationError("merge step not completed")
655
+ if remote_head == merge_commit:
656
+ # The previous push may have reached the remote before the local
657
+ # process could persist pushedHead/step=DONE. Reconcile that
658
+ # successful outcome instead of misclassifying it as drift.
659
+ journal["pushedHead"] = merge_commit
660
+ self._save(journal)
661
+ return
662
+ if remote_head != journal["base"]:
663
+ raise TargetMovedError(
664
+ f"remote {self.target_branch} moved: expected "
665
+ f"{journal['base']}, found {remote_head}"
666
+ )
667
+ self.runner.run(
668
+ self.project_root,
669
+ "push",
670
+ "origin",
671
+ f"{merge_commit}:refs/heads/{self.target_branch}",
672
+ )
673
+ journal["pushedHead"] = merge_commit
674
+ self._save(journal)
675
+
676
+ return self._run_step("push", action)
677
+
678
+ # ------------------------------------------------------------ cleanup
679
+
680
+ def cleanup_target(self, target: Path) -> None:
681
+ """Boundary-checked removal of this transaction's exact worktree."""
682
+ resolved = harness_paths.assert_path_within(target, self.temp_root)
683
+ try:
684
+ journal = self._load()
685
+ except FileNotFoundError as exc:
686
+ raise CleanupRefusedError(
687
+ f"no journal for transaction {self.transaction_id}; "
688
+ f"refusing cleanup of {resolved}"
689
+ ) from exc
690
+ allowed = journal.get("allowedCleanupRoot")
691
+ if not allowed or resolved != Path(allowed).resolve():
692
+ raise CleanupRefusedError(
693
+ f"refusing to clean non-transaction path: {resolved}"
694
+ )
695
+ self.runner.run(
696
+ self.project_root, "worktree", "remove", "--force", str(resolved),
697
+ check=False,
698
+ )
699
+ if resolved.exists():
700
+ shutil.rmtree(resolved)
701
+ listed = self.runner.text(self.project_root, "worktree", "list", "--porcelain")
702
+ if listed and str(resolved) in listed:
703
+ raise IntegrationError(f"worktree still registered: {resolved}")
704
+
705
+ def cleanup(self) -> dict[str, Any]:
706
+ try:
707
+ existing = self._load()
708
+ step = self._step(existing, "cleanup")
709
+ if step["status"] == "DONE":
710
+ step["reentry"] = "REUSED"
711
+ return self._save(existing)
712
+ except FileNotFoundError:
713
+ raise IntegrationError("cannot cleanup before preflight")
714
+
715
+ def action(journal: dict[str, Any]) -> None:
716
+ intg_root = Path(journal["integrationRoot"])
717
+ if intg_root.exists():
718
+ self.cleanup_target(intg_root)
719
+ self.runner.run(
720
+ self.project_root, "branch", "-D", self.temp_branch, check=False
721
+ )
722
+ if self._step(journal, "push")["status"] == "DONE":
723
+ refs = journal["protectionRefs"]
724
+ for ref in (refs["base"], refs["head"]):
725
+ self.runner.run(
726
+ self.project_root, "update-ref", "-d", ref, check=False
727
+ )
728
+
729
+ journal = self._run_step("cleanup", action)
730
+ harness_change.integration_lock_release(self.project_root, run_id=self.run_id)
731
+ return journal
732
+
733
+ # ------------------------------------------------------------ recover
734
+
735
+ def recover(
736
+ self, verify_commands: Sequence[Sequence[str]] | None = None
737
+ ) -> dict[str, Any]:
738
+ journal = self._load()
739
+ actions: dict[str, Callable[[], dict[str, Any]]] = {
740
+ "preflight": self.preflight,
741
+ "prepare": self.prepare,
742
+ "merge": self.merge,
743
+ "verify": lambda: self.verify(commands=verify_commands),
744
+ "push": self.push,
745
+ "cleanup": self.cleanup,
746
+ }
747
+ for name in STEP_ORDER:
748
+ step = self._step(journal, name)
749
+ if step["status"] == "DONE":
750
+ step["reentry"] = "REUSED"
751
+ self._save(journal)
752
+ continue
753
+ journal = actions[name]()
754
+ return self._load()
755
+
756
+ def status(self) -> dict[str, Any]:
757
+ journal = self._load()
758
+ return {
759
+ "ok": True,
760
+ "transactionId": self.transaction_id,
761
+ "steps": journal["steps"],
762
+ "mergeCommit": journal.get("mergeCommit"),
763
+ "pushedHead": journal.get("pushedHead"),
764
+ }
765
+
766
+
767
+ # ------------------------------------------------------------------- CLI
768
+
769
+
770
+ def _txn_from_args(args: argparse.Namespace) -> IntegrationTransaction:
771
+ return IntegrationTransaction(
772
+ project_root=Path.cwd(),
773
+ change_id=args.change,
774
+ run_id=args.run_id,
775
+ target_branch=args.target_branch,
776
+ feature_branch=args.feature_branch,
777
+ temp_root=Path(args.temp_root),
778
+ )
779
+
780
+
781
+ def _emit(payload: Any) -> int:
782
+ sys.stdout.write(json.dumps(payload, ensure_ascii=False, indent=2) + "\n")
783
+ return 0
784
+
785
+
786
+ def cmd_preflight(args: argparse.Namespace) -> int:
787
+ return _emit(_txn_from_args(args).preflight())
788
+
789
+
790
+ def cmd_prepare(args: argparse.Namespace) -> int:
791
+ return _emit(_txn_from_args(args).prepare())
792
+
793
+
794
+ def cmd_merge(args: argparse.Namespace) -> int:
795
+ return _emit(_txn_from_args(args).merge())
796
+
797
+
798
+ def _parse_verify_commands(values: Sequence[str] | None) -> list[list[str]]:
799
+ """Split ``--command`` strings honoring quotes (shlex, POSIX mode).
800
+
801
+ Plain ``str.split(" ")`` breaks quoted arguments (e.g. ``-k 'a b'``).
802
+ POSIX shlex semantics: single/double quotes group tokens, backslash
803
+ escapes the next character (quote Windows paths).
804
+ """
805
+ return [shlex.split(value) for value in (values or [])]
806
+
807
+
808
+ def cmd_verify(args: argparse.Namespace) -> int:
809
+ return _emit(_txn_from_args(args).verify(commands=_parse_verify_commands(args.command)))
810
+
811
+
812
+ def cmd_push(args: argparse.Namespace) -> int:
813
+ return _emit(_txn_from_args(args).push())
814
+
815
+
816
+ def cmd_cleanup(args: argparse.Namespace) -> int:
817
+ return _emit(_txn_from_args(args).cleanup())
818
+
819
+
820
+ def cmd_recover(args: argparse.Namespace) -> int:
821
+ return _emit(_txn_from_args(args).recover())
822
+
823
+
824
+ def cmd_status(args: argparse.Namespace) -> int:
825
+ return _emit(_txn_from_args(args).status())
826
+
827
+
828
+ def build_parser() -> argparse.ArgumentParser:
829
+ parser = argparse.ArgumentParser(prog="harness_integration.py")
830
+ sub = parser.add_subparsers(dest="command_name", required=True)
831
+
832
+ def add(name: str, func: Callable[[argparse.Namespace], int]) -> None:
833
+ p = sub.add_parser(name)
834
+ p.add_argument("--change", required=True)
835
+ p.add_argument("--run-id", required=True)
836
+ p.add_argument("--target-branch", default="main")
837
+ p.add_argument("--feature-branch", required=True)
838
+ p.add_argument("--temp-root", required=True)
839
+ p.set_defaults(func=func)
840
+
841
+ add("preflight", cmd_preflight)
842
+ add("prepare", cmd_prepare)
843
+ add("merge", cmd_merge)
844
+ add("push", cmd_push)
845
+ add("cleanup", cmd_cleanup)
846
+ add("recover", cmd_recover)
847
+ add("status", cmd_status)
848
+
849
+ p_verify = sub.add_parser("verify")
850
+ p_verify.add_argument("--change", required=True)
851
+ p_verify.add_argument("--run-id", required=True)
852
+ p_verify.add_argument("--target-branch", default="main")
853
+ p_verify.add_argument("--feature-branch", required=True)
854
+ p_verify.add_argument("--temp-root", required=True)
855
+ p_verify.add_argument("--command", action="append")
856
+ p_verify.set_defaults(func=cmd_verify)
857
+
858
+ return parser
859
+
860
+
861
+ def main(argv: list[str] | None = None) -> int:
862
+ parser = build_parser()
863
+ args = parser.parse_args(argv)
864
+ try:
865
+ return int(args.func(args))
866
+ except IntegrationError as exc:
867
+ sys.stderr.write(
868
+ json.dumps(
869
+ {"ok": False, "code": exc.code, "message": str(exc)},
870
+ ensure_ascii=False,
871
+ )
872
+ + "\n"
873
+ )
874
+ return 1
875
+
876
+
877
+ if __name__ == "__main__":
878
+ raise SystemExit(main())