@hunter-harness/workflow-harness 0.2.11 → 0.2.13

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 (250) hide show
  1. package/harness/bundles/general/claude-code/.harness-build.json +1 -1
  2. package/harness/bundles/general/claude-code/contracts/ci-metrics.schema.json +36 -0
  3. package/harness/bundles/general/claude-code/contracts/workflow-policy.json +30 -0
  4. package/harness/bundles/general/claude-code/harness-archive/SKILL.md +1 -1
  5. package/harness/bundles/general/claude-code/harness-codebase-map/SKILL.md +1 -1
  6. package/harness/bundles/general/claude-code/harness-knowledge-ingest/SKILL.md +1 -1
  7. package/harness/bundles/general/claude-code/harness-knowledge-query/SKILL.md +1 -1
  8. package/harness/bundles/general/claude-code/harness-plan/SKILL.md +4 -4
  9. package/harness/bundles/general/claude-code/harness-plan/checklist.md +6 -0
  10. package/harness/bundles/general/claude-code/harness-plan/reference.md +7 -2
  11. package/harness/bundles/general/claude-code/harness-review/SKILL.md +1 -1
  12. package/harness/bundles/general/claude-code/harness-run/SKILL.md +1 -1
  13. package/harness/bundles/general/claude-code/harness-run/checklist.md +3 -2
  14. package/harness/bundles/general/claude-code/harness-run/reference.md +6 -4
  15. package/harness/bundles/general/claude-code/harness-submit/SKILL.md +1 -1
  16. package/harness/bundles/general/claude-code/harness-sync/SKILL.md +2 -2
  17. package/harness/bundles/general/claude-code/harness-sync/reference.md +5 -4
  18. package/harness/bundles/general/claude-code/harness-test/SKILL.md +1 -1
  19. package/harness/bundles/general/claude-code/scripts/harness_archive.py +282 -120
  20. package/harness/bundles/general/claude-code/scripts/harness_events.py +400 -24
  21. package/harness/bundles/general/claude-code/scripts/harness_gate.py +585 -21
  22. package/harness/bundles/general/claude-code/scripts/harness_integration.py +403 -18
  23. package/harness/bundles/general/claude-code/scripts/harness_ledger.py +225 -29
  24. package/harness/bundles/general/claude-code/scripts/harness_phase.py +926 -0
  25. package/harness/bundles/general/claude-code/scripts/harness_plan_finalize.py +361 -0
  26. package/harness/bundles/general/claude-code/scripts/harness_profile.py +50 -0
  27. package/harness/bundles/general/claude-code/scripts/harness_runtime.py +248 -0
  28. package/harness/bundles/general/claude-code/scripts/harness_state.py +15 -2
  29. package/harness/bundles/general/claude-code/scripts/harness_test_guard.py +188 -0
  30. package/harness/bundles/general/claude-code/scripts/harness_workflow_policy.py +19 -0
  31. package/harness/bundles/general/codebuddy/.harness-build.json +1 -1
  32. package/harness/bundles/general/codebuddy/contracts/ci-metrics.schema.json +36 -0
  33. package/harness/bundles/general/codebuddy/contracts/workflow-policy.json +30 -0
  34. package/harness/bundles/general/codebuddy/harness-archive/SKILL.md +1 -1
  35. package/harness/bundles/general/codebuddy/harness-codebase-map/SKILL.md +1 -1
  36. package/harness/bundles/general/codebuddy/harness-knowledge-ingest/SKILL.md +1 -1
  37. package/harness/bundles/general/codebuddy/harness-knowledge-query/SKILL.md +1 -1
  38. package/harness/bundles/general/codebuddy/harness-plan/SKILL.md +4 -4
  39. package/harness/bundles/general/codebuddy/harness-plan/checklist.md +6 -0
  40. package/harness/bundles/general/codebuddy/harness-plan/reference.md +7 -2
  41. package/harness/bundles/general/codebuddy/harness-review/SKILL.md +1 -1
  42. package/harness/bundles/general/codebuddy/harness-run/SKILL.md +1 -1
  43. package/harness/bundles/general/codebuddy/harness-run/checklist.md +3 -2
  44. package/harness/bundles/general/codebuddy/harness-run/reference.md +6 -4
  45. package/harness/bundles/general/codebuddy/harness-submit/SKILL.md +1 -1
  46. package/harness/bundles/general/codebuddy/harness-sync/SKILL.md +2 -2
  47. package/harness/bundles/general/codebuddy/harness-sync/reference.md +5 -4
  48. package/harness/bundles/general/codebuddy/harness-test/SKILL.md +1 -1
  49. package/harness/bundles/general/codebuddy/scripts/harness_archive.py +282 -120
  50. package/harness/bundles/general/codebuddy/scripts/harness_events.py +400 -24
  51. package/harness/bundles/general/codebuddy/scripts/harness_gate.py +585 -21
  52. package/harness/bundles/general/codebuddy/scripts/harness_integration.py +403 -18
  53. package/harness/bundles/general/codebuddy/scripts/harness_ledger.py +225 -29
  54. package/harness/bundles/general/codebuddy/scripts/harness_phase.py +926 -0
  55. package/harness/bundles/general/codebuddy/scripts/harness_plan_finalize.py +361 -0
  56. package/harness/bundles/general/codebuddy/scripts/harness_profile.py +50 -0
  57. package/harness/bundles/general/codebuddy/scripts/harness_runtime.py +248 -0
  58. package/harness/bundles/general/codebuddy/scripts/harness_state.py +15 -2
  59. package/harness/bundles/general/codebuddy/scripts/harness_test_guard.py +188 -0
  60. package/harness/bundles/general/codebuddy/scripts/harness_workflow_policy.py +19 -0
  61. package/harness/bundles/general/codex/.harness-build.json +1 -1
  62. package/harness/bundles/general/codex/contracts/ci-metrics.schema.json +36 -0
  63. package/harness/bundles/general/codex/contracts/workflow-policy.json +30 -0
  64. package/harness/bundles/general/codex/harness-archive/SKILL.md +1 -1
  65. package/harness/bundles/general/codex/harness-codebase-map/SKILL.md +1 -1
  66. package/harness/bundles/general/codex/harness-knowledge-ingest/SKILL.md +1 -1
  67. package/harness/bundles/general/codex/harness-knowledge-query/SKILL.md +1 -1
  68. package/harness/bundles/general/codex/harness-plan/SKILL.md +4 -4
  69. package/harness/bundles/general/codex/harness-plan/checklist.md +6 -0
  70. package/harness/bundles/general/codex/harness-plan/reference.md +7 -2
  71. package/harness/bundles/general/codex/harness-review/SKILL.md +1 -1
  72. package/harness/bundles/general/codex/harness-run/SKILL.md +1 -1
  73. package/harness/bundles/general/codex/harness-run/checklist.md +3 -2
  74. package/harness/bundles/general/codex/harness-run/reference.md +6 -4
  75. package/harness/bundles/general/codex/harness-submit/SKILL.md +1 -1
  76. package/harness/bundles/general/codex/harness-sync/SKILL.md +2 -2
  77. package/harness/bundles/general/codex/harness-sync/reference.md +5 -4
  78. package/harness/bundles/general/codex/harness-test/SKILL.md +1 -1
  79. package/harness/bundles/general/codex/scripts/harness_archive.py +282 -120
  80. package/harness/bundles/general/codex/scripts/harness_events.py +400 -24
  81. package/harness/bundles/general/codex/scripts/harness_gate.py +585 -21
  82. package/harness/bundles/general/codex/scripts/harness_integration.py +403 -18
  83. package/harness/bundles/general/codex/scripts/harness_ledger.py +225 -29
  84. package/harness/bundles/general/codex/scripts/harness_phase.py +926 -0
  85. package/harness/bundles/general/codex/scripts/harness_plan_finalize.py +361 -0
  86. package/harness/bundles/general/codex/scripts/harness_profile.py +50 -0
  87. package/harness/bundles/general/codex/scripts/harness_runtime.py +248 -0
  88. package/harness/bundles/general/codex/scripts/harness_state.py +15 -2
  89. package/harness/bundles/general/codex/scripts/harness_test_guard.py +188 -0
  90. package/harness/bundles/general/codex/scripts/harness_workflow_policy.py +19 -0
  91. package/harness/bundles/general/cursor/.harness-build.json +1 -1
  92. package/harness/bundles/general/cursor/contracts/ci-metrics.schema.json +36 -0
  93. package/harness/bundles/general/cursor/contracts/workflow-policy.json +30 -0
  94. package/harness/bundles/general/cursor/harness-archive/SKILL.md +1 -1
  95. package/harness/bundles/general/cursor/harness-codebase-map/SKILL.md +1 -1
  96. package/harness/bundles/general/cursor/harness-knowledge-ingest/SKILL.md +1 -1
  97. package/harness/bundles/general/cursor/harness-knowledge-query/SKILL.md +1 -1
  98. package/harness/bundles/general/cursor/harness-plan/SKILL.md +4 -4
  99. package/harness/bundles/general/cursor/harness-plan/checklist.md +6 -0
  100. package/harness/bundles/general/cursor/harness-plan/reference.md +7 -2
  101. package/harness/bundles/general/cursor/harness-review/SKILL.md +1 -1
  102. package/harness/bundles/general/cursor/harness-run/SKILL.md +1 -1
  103. package/harness/bundles/general/cursor/harness-run/checklist.md +3 -2
  104. package/harness/bundles/general/cursor/harness-run/reference.md +6 -4
  105. package/harness/bundles/general/cursor/harness-submit/SKILL.md +1 -1
  106. package/harness/bundles/general/cursor/harness-sync/SKILL.md +2 -2
  107. package/harness/bundles/general/cursor/harness-sync/reference.md +5 -4
  108. package/harness/bundles/general/cursor/harness-test/SKILL.md +1 -1
  109. package/harness/bundles/general/cursor/scripts/harness_archive.py +282 -120
  110. package/harness/bundles/general/cursor/scripts/harness_events.py +400 -24
  111. package/harness/bundles/general/cursor/scripts/harness_gate.py +585 -21
  112. package/harness/bundles/general/cursor/scripts/harness_integration.py +403 -18
  113. package/harness/bundles/general/cursor/scripts/harness_ledger.py +225 -29
  114. package/harness/bundles/general/cursor/scripts/harness_phase.py +926 -0
  115. package/harness/bundles/general/cursor/scripts/harness_plan_finalize.py +361 -0
  116. package/harness/bundles/general/cursor/scripts/harness_profile.py +50 -0
  117. package/harness/bundles/general/cursor/scripts/harness_runtime.py +248 -0
  118. package/harness/bundles/general/cursor/scripts/harness_state.py +15 -2
  119. package/harness/bundles/general/cursor/scripts/harness_test_guard.py +188 -0
  120. package/harness/bundles/general/cursor/scripts/harness_workflow_policy.py +19 -0
  121. package/harness/bundles/java/claude-code/.harness-build.json +1 -1
  122. package/harness/bundles/java/claude-code/contracts/ci-metrics.schema.json +36 -0
  123. package/harness/bundles/java/claude-code/contracts/workflow-policy.json +30 -0
  124. package/harness/bundles/java/claude-code/harness-apidoc/SKILL.md +1 -1
  125. package/harness/bundles/java/claude-code/harness-archive/SKILL.md +1 -1
  126. package/harness/bundles/java/claude-code/harness-codebase-map/SKILL.md +1 -1
  127. package/harness/bundles/java/claude-code/harness-knowledge-ingest/SKILL.md +1 -1
  128. package/harness/bundles/java/claude-code/harness-knowledge-query/SKILL.md +1 -1
  129. package/harness/bundles/java/claude-code/harness-package/SKILL.md +1 -1
  130. package/harness/bundles/java/claude-code/harness-plan/SKILL.md +4 -4
  131. package/harness/bundles/java/claude-code/harness-plan/checklist.md +6 -0
  132. package/harness/bundles/java/claude-code/harness-plan/reference.md +7 -2
  133. package/harness/bundles/java/claude-code/harness-review/SKILL.md +1 -1
  134. package/harness/bundles/java/claude-code/harness-run/SKILL.md +1 -1
  135. package/harness/bundles/java/claude-code/harness-submit/SKILL.md +1 -1
  136. package/harness/bundles/java/claude-code/harness-sync/SKILL.md +2 -2
  137. package/harness/bundles/java/claude-code/harness-sync/reference.md +5 -4
  138. package/harness/bundles/java/claude-code/harness-test/SKILL.md +1 -1
  139. package/harness/bundles/java/claude-code/scripts/harness_archive.py +282 -120
  140. package/harness/bundles/java/claude-code/scripts/harness_events.py +400 -24
  141. package/harness/bundles/java/claude-code/scripts/harness_gate.py +585 -21
  142. package/harness/bundles/java/claude-code/scripts/harness_integration.py +403 -18
  143. package/harness/bundles/java/claude-code/scripts/harness_ledger.py +225 -29
  144. package/harness/bundles/java/claude-code/scripts/harness_phase.py +926 -0
  145. package/harness/bundles/java/claude-code/scripts/harness_plan_finalize.py +361 -0
  146. package/harness/bundles/java/claude-code/scripts/harness_profile.py +50 -0
  147. package/harness/bundles/java/claude-code/scripts/harness_runtime.py +248 -0
  148. package/harness/bundles/java/claude-code/scripts/harness_state.py +15 -2
  149. package/harness/bundles/java/claude-code/scripts/harness_test_guard.py +188 -0
  150. package/harness/bundles/java/claude-code/scripts/harness_workflow_policy.py +19 -0
  151. package/harness/bundles/java/codebuddy/.harness-build.json +1 -1
  152. package/harness/bundles/java/codebuddy/contracts/ci-metrics.schema.json +36 -0
  153. package/harness/bundles/java/codebuddy/contracts/workflow-policy.json +30 -0
  154. package/harness/bundles/java/codebuddy/harness-apidoc/SKILL.md +1 -1
  155. package/harness/bundles/java/codebuddy/harness-archive/SKILL.md +1 -1
  156. package/harness/bundles/java/codebuddy/harness-codebase-map/SKILL.md +1 -1
  157. package/harness/bundles/java/codebuddy/harness-knowledge-ingest/SKILL.md +1 -1
  158. package/harness/bundles/java/codebuddy/harness-knowledge-query/SKILL.md +1 -1
  159. package/harness/bundles/java/codebuddy/harness-package/SKILL.md +1 -1
  160. package/harness/bundles/java/codebuddy/harness-plan/SKILL.md +4 -4
  161. package/harness/bundles/java/codebuddy/harness-plan/checklist.md +6 -0
  162. package/harness/bundles/java/codebuddy/harness-plan/reference.md +7 -2
  163. package/harness/bundles/java/codebuddy/harness-review/SKILL.md +1 -1
  164. package/harness/bundles/java/codebuddy/harness-run/SKILL.md +1 -1
  165. package/harness/bundles/java/codebuddy/harness-submit/SKILL.md +1 -1
  166. package/harness/bundles/java/codebuddy/harness-sync/SKILL.md +2 -2
  167. package/harness/bundles/java/codebuddy/harness-sync/reference.md +5 -4
  168. package/harness/bundles/java/codebuddy/harness-test/SKILL.md +1 -1
  169. package/harness/bundles/java/codebuddy/scripts/harness_archive.py +282 -120
  170. package/harness/bundles/java/codebuddy/scripts/harness_events.py +400 -24
  171. package/harness/bundles/java/codebuddy/scripts/harness_gate.py +585 -21
  172. package/harness/bundles/java/codebuddy/scripts/harness_integration.py +403 -18
  173. package/harness/bundles/java/codebuddy/scripts/harness_ledger.py +225 -29
  174. package/harness/bundles/java/codebuddy/scripts/harness_phase.py +926 -0
  175. package/harness/bundles/java/codebuddy/scripts/harness_plan_finalize.py +361 -0
  176. package/harness/bundles/java/codebuddy/scripts/harness_profile.py +50 -0
  177. package/harness/bundles/java/codebuddy/scripts/harness_runtime.py +248 -0
  178. package/harness/bundles/java/codebuddy/scripts/harness_state.py +15 -2
  179. package/harness/bundles/java/codebuddy/scripts/harness_test_guard.py +188 -0
  180. package/harness/bundles/java/codebuddy/scripts/harness_workflow_policy.py +19 -0
  181. package/harness/bundles/java/codex/.harness-build.json +1 -1
  182. package/harness/bundles/java/codex/contracts/ci-metrics.schema.json +36 -0
  183. package/harness/bundles/java/codex/contracts/workflow-policy.json +30 -0
  184. package/harness/bundles/java/codex/harness-apidoc/SKILL.md +1 -1
  185. package/harness/bundles/java/codex/harness-archive/SKILL.md +1 -1
  186. package/harness/bundles/java/codex/harness-codebase-map/SKILL.md +1 -1
  187. package/harness/bundles/java/codex/harness-knowledge-ingest/SKILL.md +1 -1
  188. package/harness/bundles/java/codex/harness-knowledge-query/SKILL.md +1 -1
  189. package/harness/bundles/java/codex/harness-package/SKILL.md +1 -1
  190. package/harness/bundles/java/codex/harness-plan/SKILL.md +4 -4
  191. package/harness/bundles/java/codex/harness-plan/checklist.md +6 -0
  192. package/harness/bundles/java/codex/harness-plan/reference.md +7 -2
  193. package/harness/bundles/java/codex/harness-review/SKILL.md +1 -1
  194. package/harness/bundles/java/codex/harness-run/SKILL.md +1 -1
  195. package/harness/bundles/java/codex/harness-submit/SKILL.md +1 -1
  196. package/harness/bundles/java/codex/harness-sync/SKILL.md +2 -2
  197. package/harness/bundles/java/codex/harness-sync/reference.md +5 -4
  198. package/harness/bundles/java/codex/harness-test/SKILL.md +1 -1
  199. package/harness/bundles/java/codex/scripts/harness_archive.py +282 -120
  200. package/harness/bundles/java/codex/scripts/harness_events.py +400 -24
  201. package/harness/bundles/java/codex/scripts/harness_gate.py +585 -21
  202. package/harness/bundles/java/codex/scripts/harness_integration.py +403 -18
  203. package/harness/bundles/java/codex/scripts/harness_ledger.py +225 -29
  204. package/harness/bundles/java/codex/scripts/harness_phase.py +926 -0
  205. package/harness/bundles/java/codex/scripts/harness_plan_finalize.py +361 -0
  206. package/harness/bundles/java/codex/scripts/harness_profile.py +50 -0
  207. package/harness/bundles/java/codex/scripts/harness_runtime.py +248 -0
  208. package/harness/bundles/java/codex/scripts/harness_state.py +15 -2
  209. package/harness/bundles/java/codex/scripts/harness_test_guard.py +188 -0
  210. package/harness/bundles/java/codex/scripts/harness_workflow_policy.py +19 -0
  211. package/harness/bundles/java/cursor/.harness-build.json +1 -1
  212. package/harness/bundles/java/cursor/contracts/ci-metrics.schema.json +36 -0
  213. package/harness/bundles/java/cursor/contracts/workflow-policy.json +30 -0
  214. package/harness/bundles/java/cursor/harness-apidoc/SKILL.md +1 -1
  215. package/harness/bundles/java/cursor/harness-archive/SKILL.md +1 -1
  216. package/harness/bundles/java/cursor/harness-codebase-map/SKILL.md +1 -1
  217. package/harness/bundles/java/cursor/harness-knowledge-ingest/SKILL.md +1 -1
  218. package/harness/bundles/java/cursor/harness-knowledge-query/SKILL.md +1 -1
  219. package/harness/bundles/java/cursor/harness-package/SKILL.md +1 -1
  220. package/harness/bundles/java/cursor/harness-plan/SKILL.md +4 -4
  221. package/harness/bundles/java/cursor/harness-plan/checklist.md +6 -0
  222. package/harness/bundles/java/cursor/harness-plan/reference.md +7 -2
  223. package/harness/bundles/java/cursor/harness-review/SKILL.md +1 -1
  224. package/harness/bundles/java/cursor/harness-run/SKILL.md +1 -1
  225. package/harness/bundles/java/cursor/harness-submit/SKILL.md +1 -1
  226. package/harness/bundles/java/cursor/harness-sync/SKILL.md +2 -2
  227. package/harness/bundles/java/cursor/harness-sync/reference.md +5 -4
  228. package/harness/bundles/java/cursor/harness-test/SKILL.md +1 -1
  229. package/harness/bundles/java/cursor/scripts/harness_archive.py +282 -120
  230. package/harness/bundles/java/cursor/scripts/harness_events.py +400 -24
  231. package/harness/bundles/java/cursor/scripts/harness_gate.py +585 -21
  232. package/harness/bundles/java/cursor/scripts/harness_integration.py +403 -18
  233. package/harness/bundles/java/cursor/scripts/harness_ledger.py +225 -29
  234. package/harness/bundles/java/cursor/scripts/harness_phase.py +926 -0
  235. package/harness/bundles/java/cursor/scripts/harness_plan_finalize.py +361 -0
  236. package/harness/bundles/java/cursor/scripts/harness_profile.py +50 -0
  237. package/harness/bundles/java/cursor/scripts/harness_runtime.py +248 -0
  238. package/harness/bundles/java/cursor/scripts/harness_state.py +15 -2
  239. package/harness/bundles/java/cursor/scripts/harness_test_guard.py +188 -0
  240. package/harness/bundles/java/cursor/scripts/harness_workflow_policy.py +19 -0
  241. package/harness/manifests/general/claude-code.json +43 -27
  242. package/harness/manifests/general/codebuddy.json +43 -27
  243. package/harness/manifests/general/codex.json +43 -27
  244. package/harness/manifests/general/cursor.json +43 -27
  245. package/harness/manifests/java/claude-code.json +43 -27
  246. package/harness/manifests/java/codebuddy.json +43 -27
  247. package/harness/manifests/java/codex.json +43 -27
  248. package/harness/manifests/java/cursor.json +43 -27
  249. package/hunter-workflow-family.json +2 -2
  250. package/package.json +1 -1
@@ -43,6 +43,9 @@ if hasattr(sys.stderr, "reconfigure"):
43
43
  sys.stderr.reconfigure(encoding="utf-8")
44
44
 
45
45
  STEP_ORDER = ["preflight", "prepare", "merge", "verify", "push", "cleanup"]
46
+ HEAVY_WORKTREE_ROOTS = (
47
+ "node_modules", ".venv", "venv", "build", "dist", "target", ".cache", "__pycache__"
48
+ )
46
49
  _FOREIGN_PATTERNS = (
47
50
  re.compile(r"^\.harness/changes/([^/]+)(/|$)"),
48
51
  re.compile(r"^\.harness/state/changes/([^/]+)(/|$)"),
@@ -73,14 +76,33 @@ class VerificationFailedError(IntegrationError):
73
76
  code = "VERIFICATION_FAILED"
74
77
 
75
78
 
79
+ class VerifyPlanMissingError(IntegrationError):
80
+ code = "VERIFY_PLAN_MISSING"
81
+
82
+
76
83
  class TargetMovedError(IntegrationError):
77
84
  code = "TARGET_MOVED"
78
85
 
79
86
 
87
+ class LedgerSyncError(IntegrationError):
88
+ code = "LEDGER_SYNC_PENDING"
89
+
90
+
80
91
  class CleanupRefusedError(IntegrationError):
81
92
  code = "CLEANUP_REFUSED"
82
93
 
83
94
 
95
+ class CleanupResidualError(IntegrationError):
96
+ code = "CLEANUP_RESIDUAL"
97
+
98
+ def __init__(self, result: dict[str, Any]):
99
+ self.result = result
100
+ super().__init__(
101
+ "worktree cleanup retained paths: "
102
+ + ", ".join(result.get("residualPaths") or ["<registration>"])
103
+ )
104
+
105
+
84
106
  class JournalConflictError(IntegrationError):
85
107
  code = "JOURNAL_CONFLICT"
86
108
 
@@ -361,6 +383,120 @@ class IntegrationTransaction:
361
383
  "ledgerIdentity": ledger_identity,
362
384
  }
363
385
 
386
+ def _ledger_path(self) -> Path | None:
387
+ candidates = (
388
+ self.project_root / ".harness" / "state" / "changes" / self.change_id
389
+ / "evidence" / "verification-ledger.json",
390
+ self.project_root / ".harness" / "changes" / self.change_id
391
+ / "evidence" / "verification-ledger.json",
392
+ self.project_root / ".harness" / "state" / "changes" / self.change_id
393
+ / "verification-ledger.json",
394
+ )
395
+ return next((path for path in candidates if path.is_file()), None)
396
+
397
+ def _finalize_remote_push(
398
+ self,
399
+ journal: dict[str, Any],
400
+ *,
401
+ merge_commit: str,
402
+ remote_head: str,
403
+ ) -> None:
404
+ journal.update({
405
+ "pushedHead": merge_commit,
406
+ "mergeFinalHash": merge_commit,
407
+ "ciExpectedHead": merge_commit,
408
+ "remoteHead": remote_head,
409
+ "pushIntent": {
410
+ "status": "REMOTE_CONFIRMED",
411
+ "expectedRemoteHead": journal.get("remoteTargetHead") or journal.get("base"),
412
+ "mergeCommit": merge_commit,
413
+ "observedRemoteHead": remote_head,
414
+ "updatedAt": now_iso(),
415
+ },
416
+ "ledgerSync": {"status": "PENDING", "updatedAt": now_iso()},
417
+ })
418
+ self._save(journal)
419
+ ledger_path = self._ledger_path()
420
+ if ledger_path is None:
421
+ result = {"ok": False, "code": "LEDGER_MISSING"}
422
+ else:
423
+ result = hl.record_integration_hashes(
424
+ ledger_path,
425
+ repository_id=str(journal["repositoryId"]),
426
+ merge_final_hash=merge_commit,
427
+ ci_expected_head=merge_commit,
428
+ remote_head=remote_head,
429
+ )
430
+ journal = self._load()
431
+ journal["ledgerSync"] = {
432
+ "status": "DONE" if result.get("ok") else "FAILED",
433
+ "result": result,
434
+ "updatedAt": now_iso(),
435
+ }
436
+ self._save(journal)
437
+ if not result.get("ok"):
438
+ raise LedgerSyncError(
439
+ f"remote push is confirmed but ledger sync requires recovery: {result.get('code')}"
440
+ )
441
+
442
+ def _assert_cleanup_consistency(self, journal: dict[str, Any]) -> None:
443
+ push_step = self._step(journal, "push")
444
+ merge_commit = str(journal.get("mergeCommit") or "")
445
+ remote_line = self.runner.text(
446
+ self.project_root, "ls-remote", "origin", self.target_branch
447
+ )
448
+ observed_remote = (
449
+ remote_line.split()[0] if remote_line and remote_line.split() else ""
450
+ )
451
+ remote_contains_merge = bool(merge_commit and observed_remote == merge_commit)
452
+ if push_step.get("status") != "DONE" and not remote_contains_merge:
453
+ if self._step(journal, "prepare").get("status") == "DONE":
454
+ raise CleanupRefusedError(
455
+ "push is not complete and remote does not contain the merge commit"
456
+ )
457
+ return
458
+ final_values = [
459
+ journal.get("pushedHead"),
460
+ journal.get("mergeFinalHash"),
461
+ journal.get("ciExpectedHead"),
462
+ journal.get("remoteHead"),
463
+ observed_remote,
464
+ ]
465
+ if any(not isinstance(value, str) or not value for value in final_values) or len(
466
+ set(final_values)
467
+ ) != 1:
468
+ raise CleanupRefusedError(
469
+ "remote/journal final hashes are missing or inconsistent"
470
+ )
471
+ ledger_path = self._ledger_path()
472
+ if ledger_path is None:
473
+ raise CleanupRefusedError("verification ledger missing before cleanup")
474
+ try:
475
+ ledger = json.loads(ledger_path.read_text(encoding="utf-8-sig"))
476
+ except (OSError, json.JSONDecodeError) as exc:
477
+ raise CleanupRefusedError(f"verification ledger unreadable: {exc}") from exc
478
+ ledger_values = [
479
+ ledger.get("mergeFinalHash"),
480
+ ledger.get("ciExpectedHead"),
481
+ ledger.get("remoteHead"),
482
+ ]
483
+ if len(set(ledger_values + [observed_remote])) != 1:
484
+ raise CleanupRefusedError(
485
+ "remote/journal/ledger final hashes are inconsistent"
486
+ )
487
+ ancestor = self.runner.run(
488
+ self.project_root,
489
+ "merge-base",
490
+ "--is-ancestor",
491
+ str(journal.get("featureHead") or ""),
492
+ observed_remote,
493
+ check=False,
494
+ )
495
+ if ancestor.returncode != 0:
496
+ raise CleanupRefusedError(
497
+ "feature head is not an ancestor of the pushed target"
498
+ )
499
+
364
500
  def _load(self) -> dict[str, Any]:
365
501
  return load_journal(self.project_root, self.transaction_id)
366
502
 
@@ -414,15 +550,68 @@ class IntegrationTransaction:
414
550
 
415
551
  def preflight(self) -> dict[str, Any]:
416
552
  def action(journal: dict[str, Any]) -> None:
417
- base = self.runner.text(
553
+ self.runner.run(
554
+ self.project_root,
555
+ "fetch",
556
+ "origin",
557
+ self.target_branch,
558
+ check=False,
559
+ )
560
+ local_target = self.runner.text(
418
561
  self.project_root, "rev-parse", self.target_branch
419
562
  )
563
+ remote_line = self.runner.text(
564
+ self.project_root, "ls-remote", "origin", self.target_branch
565
+ )
566
+ remote_target = (
567
+ remote_line.split()[0] if remote_line and remote_line.split() else None
568
+ )
420
569
  feature_head = self.runner.text(
421
570
  self.project_root, "rev-parse", self.feature_branch
422
571
  )
423
- if not base or not feature_head:
572
+ if not local_target or not feature_head:
424
573
  raise IntegrationError("target or feature branch not found")
574
+ reconciliation_required = False
575
+ base = local_target
576
+ base_source = "local"
577
+ if remote_target:
578
+ if remote_target == local_target:
579
+ base = remote_target
580
+ base_source = "remote"
581
+ else:
582
+ local_behind = self.runner.run(
583
+ self.project_root,
584
+ "merge-base",
585
+ "--is-ancestor",
586
+ local_target,
587
+ remote_target,
588
+ check=False,
589
+ )
590
+ remote_behind = self.runner.run(
591
+ self.project_root,
592
+ "merge-base",
593
+ "--is-ancestor",
594
+ remote_target,
595
+ local_target,
596
+ check=False,
597
+ )
598
+ if local_behind.returncode == 0:
599
+ base = remote_target
600
+ base_source = "remote-ahead"
601
+ reconciliation_required = True
602
+ elif remote_behind.returncode == 0:
603
+ base = local_target
604
+ base_source = "local-ahead"
605
+ else:
606
+ raise TargetMovedError(
607
+ f"local and remote {self.target_branch} diverged: "
608
+ f"local={local_target}, remote={remote_target}"
609
+ )
425
610
  fresh = self._new_journal(base, feature_head)
611
+ fresh["baseSource"] = base_source
612
+ fresh["localTargetHead"] = local_target
613
+ fresh["remoteTargetHead"] = remote_target
614
+ fresh["primaryReconciliationRequired"] = reconciliation_required
426
615
  fresh["revision"] = int(journal.get("revision") or 0)
427
616
  journal.clear()
428
617
  journal.update(fresh)
@@ -500,11 +689,17 @@ class IntegrationTransaction:
500
689
  return self._run_step("prepare", action)
501
690
 
502
691
  def _foreign_paths(self, journal: dict[str, Any]) -> list[str]:
692
+ feature_base = self.runner.text(
693
+ self.project_root,
694
+ "merge-base",
695
+ journal["base"],
696
+ journal["featureHead"],
697
+ ) or journal["base"]
503
698
  diff = self.runner.text(
504
699
  self.project_root,
505
700
  "diff",
506
701
  "--name-only",
507
- journal["base"],
702
+ feature_base,
508
703
  journal["featureHead"],
509
704
  )
510
705
  contract: dict[str, Any] = {}
@@ -558,11 +753,27 @@ class IntegrationTransaction:
558
753
  current_target = self.runner.text(
559
754
  self.project_root, "rev-parse", self.target_branch
560
755
  )
561
- if current_target != journal["base"]:
756
+ expected_local = journal.get("localTargetHead") or journal["base"]
757
+ if current_target != expected_local:
562
758
  raise TargetMovedError(
563
759
  f"local {self.target_branch} moved: expected "
564
- f"{journal['base']}, found {current_target}"
760
+ f"{expected_local}, found {current_target}"
761
+ )
762
+ if journal.get("remoteTargetHead"):
763
+ remote_line = self.runner.text(
764
+ self.project_root, "ls-remote", "origin", self.target_branch
765
+ )
766
+ current_remote = (
767
+ remote_line.split()[0]
768
+ if remote_line and remote_line.split()
769
+ else None
565
770
  )
771
+ expected_remote = journal.get("remoteTargetHead")
772
+ if current_remote != expected_remote:
773
+ raise TargetMovedError(
774
+ f"remote {self.target_branch} moved: expected "
775
+ f"{expected_remote}, found {current_remote}"
776
+ )
566
777
  foreign = self._foreign_paths(journal)
567
778
  if foreign:
568
779
  raise ForeignChangePathsError(
@@ -589,6 +800,13 @@ class IntegrationTransaction:
589
800
  commands = list(commands or [])
590
801
 
591
802
  def action(journal: dict[str, Any]) -> None:
803
+ if not commands or any(
804
+ not isinstance(command, (list, tuple)) or not command or not command[0]
805
+ for command in commands
806
+ ):
807
+ raise VerifyPlanMissingError(
808
+ "integration verification requires at least one executable command"
809
+ )
592
810
  intg_root = Path(journal["integrationRoot"]).resolve()
593
811
  results: list[dict[str, Any]] = []
594
812
  for command in commands:
@@ -641,6 +859,24 @@ class IntegrationTransaction:
641
859
 
642
860
  def push(self) -> dict[str, Any]:
643
861
  def action(journal: dict[str, Any]) -> None:
862
+ verify_step = self._step(journal, "verify")
863
+ verify_results = journal.get("verifyResults")
864
+ if (
865
+ verify_step.get("status") != "DONE"
866
+ or not isinstance(verify_results, list)
867
+ or not verify_results
868
+ or any(
869
+ not isinstance(item, dict)
870
+ or item.get("exitCode") != 0
871
+ or not isinstance(item.get("command"), list)
872
+ or not item.get("command")
873
+ for item in verify_results
874
+ )
875
+ or not isinstance(journal.get("verificationIdentity"), dict)
876
+ ):
877
+ raise VerifyPlanMissingError(
878
+ "push requires a completed, non-empty verification plan"
879
+ )
644
880
  remote_head = self.runner.text(
645
881
  self.project_root, "ls-remote", "origin", self.target_branch
646
882
  )
@@ -656,28 +892,60 @@ class IntegrationTransaction:
656
892
  # The previous push may have reached the remote before the local
657
893
  # process could persist pushedHead/step=DONE. Reconcile that
658
894
  # successful outcome instead of misclassifying it as drift.
659
- journal["pushedHead"] = merge_commit
660
- self._save(journal)
895
+ self._finalize_remote_push(
896
+ journal, merge_commit=merge_commit, remote_head=remote_head
897
+ )
661
898
  return
662
- if remote_head != journal["base"]:
899
+ expected_remote = journal.get("remoteTargetHead") or journal["base"]
900
+ if remote_head != expected_remote:
663
901
  raise TargetMovedError(
664
902
  f"remote {self.target_branch} moved: expected "
665
- f"{journal['base']}, found {remote_head}"
903
+ f"{expected_remote}, found {remote_head}"
666
904
  )
905
+ journal["pushIntent"] = {
906
+ "status": "PENDING",
907
+ "expectedRemoteHead": expected_remote,
908
+ "mergeCommit": merge_commit,
909
+ "updatedAt": now_iso(),
910
+ }
911
+ self._save(journal)
667
912
  self.runner.run(
668
913
  self.project_root,
669
914
  "push",
670
915
  "origin",
671
916
  f"{merge_commit}:refs/heads/{self.target_branch}",
672
917
  )
673
- journal["pushedHead"] = merge_commit
674
- self._save(journal)
918
+ remote_after_line = self.runner.text(
919
+ self.project_root, "ls-remote", "origin", self.target_branch
920
+ )
921
+ remote_after = (
922
+ remote_after_line.split()[0]
923
+ if remote_after_line and remote_after_line.split()
924
+ else ""
925
+ )
926
+ if remote_after != merge_commit:
927
+ journal = self._load()
928
+ journal["pushIntent"] = {
929
+ "status": "REMOTE_UNCONFIRMED",
930
+ "expectedRemoteHead": expected_remote,
931
+ "mergeCommit": merge_commit,
932
+ "observedRemoteHead": remote_after,
933
+ "updatedAt": now_iso(),
934
+ }
935
+ self._save(journal)
936
+ raise IntegrationError(
937
+ "push returned but the remote target does not equal the merge commit"
938
+ )
939
+ journal = self._load()
940
+ self._finalize_remote_push(
941
+ journal, merge_commit=merge_commit, remote_head=remote_after
942
+ )
675
943
 
676
944
  return self._run_step("push", action)
677
945
 
678
946
  # ------------------------------------------------------------ cleanup
679
947
 
680
- def cleanup_target(self, target: Path) -> None:
948
+ def cleanup_target(self, target: Path) -> dict[str, Any]:
681
949
  """Boundary-checked removal of this transaction's exact worktree."""
682
950
  resolved = harness_paths.assert_path_within(target, self.temp_root)
683
951
  try:
@@ -692,15 +960,71 @@ class IntegrationTransaction:
692
960
  raise CleanupRefusedError(
693
961
  f"refusing to clean non-transaction path: {resolved}"
694
962
  )
963
+ listed_before = self.runner.text(
964
+ self.project_root, "worktree", "list", "--porcelain"
965
+ )
966
+ registered_before = {
967
+ str(Path(line.removeprefix("worktree ")).resolve())
968
+ for line in (listed_before or "").splitlines()
969
+ if line.startswith("worktree ")
970
+ }
971
+ if resolved.exists() and str(resolved) not in registered_before:
972
+ raise CleanupRefusedError(
973
+ f"refusing to clean unregistered worktree path: {resolved}"
974
+ )
975
+
976
+ heavy_removed: list[str] = []
977
+ heavy_failures: list[dict[str, str]] = []
978
+ if resolved.is_dir():
979
+ for name in HEAVY_WORKTREE_ROOTS:
980
+ heavy = resolved / name
981
+ if not heavy.exists():
982
+ continue
983
+ harness_paths.assert_path_within(heavy, resolved)
984
+ try:
985
+ if heavy.is_dir() and not heavy.is_symlink():
986
+ shutil.rmtree(heavy)
987
+ else:
988
+ heavy.unlink()
989
+ heavy_removed.append(name)
990
+ except OSError as exc:
991
+ heavy_failures.append({"path": name, "error": str(exc)})
695
992
  self.runner.run(
696
993
  self.project_root, "worktree", "remove", "--force", str(resolved),
697
994
  check=False,
698
995
  )
699
- if resolved.exists():
700
- shutil.rmtree(resolved)
701
996
  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}")
997
+ registered_after = {
998
+ str(Path(line.removeprefix("worktree ")).resolve())
999
+ for line in (listed or "").splitlines()
1000
+ if line.startswith("worktree ")
1001
+ }
1002
+ still_registered = str(resolved) in registered_after
1003
+ residual_paths: list[str] = []
1004
+ if resolved.exists():
1005
+ if resolved.is_file():
1006
+ residual_paths.append(resolved.name)
1007
+ else:
1008
+ for root, dirs, files in os.walk(resolved):
1009
+ rel_root = Path(root).relative_to(resolved)
1010
+ for name in sorted(dirs + files):
1011
+ rel = (rel_root / name).as_posix()
1012
+ if rel not in residual_paths:
1013
+ residual_paths.append(rel)
1014
+ if len(residual_paths) >= 200:
1015
+ break
1016
+ if len(residual_paths) >= 200:
1017
+ break
1018
+ ok = not heavy_failures and not still_registered and not residual_paths
1019
+ return {
1020
+ "ok": ok,
1021
+ "code": "CLEANUP_COMPLETE" if ok else "CLEANUP_RESIDUAL",
1022
+ "target": str(resolved),
1023
+ "heavyRootsRemoved": heavy_removed,
1024
+ "heavyRootFailures": heavy_failures,
1025
+ "worktreeRegistered": still_registered,
1026
+ "residualPaths": residual_paths,
1027
+ }
704
1028
 
705
1029
  def cleanup(self) -> dict[str, Any]:
706
1030
  try:
@@ -713,9 +1037,23 @@ class IntegrationTransaction:
713
1037
  raise IntegrationError("cannot cleanup before preflight")
714
1038
 
715
1039
  def action(journal: dict[str, Any]) -> None:
1040
+ self._assert_cleanup_consistency(journal)
716
1041
  intg_root = Path(journal["integrationRoot"])
1042
+ cleanup_result: dict[str, Any] = {
1043
+ "ok": True,
1044
+ "code": "ALREADY_ABSENT",
1045
+ "target": str(intg_root.resolve()),
1046
+ "heavyRootsRemoved": [],
1047
+ "heavyRootFailures": [],
1048
+ "worktreeRegistered": False,
1049
+ "residualPaths": [],
1050
+ }
717
1051
  if intg_root.exists():
718
- self.cleanup_target(intg_root)
1052
+ cleanup_result = self.cleanup_target(intg_root)
1053
+ journal["cleanupResult"] = cleanup_result
1054
+ self._save(journal)
1055
+ if not cleanup_result["ok"]:
1056
+ raise CleanupResidualError(cleanup_result)
719
1057
  self.runner.run(
720
1058
  self.project_root, "branch", "-D", self.temp_branch, check=False
721
1059
  )
@@ -725,6 +1063,49 @@ class IntegrationTransaction:
725
1063
  self.runner.run(
726
1064
  self.project_root, "update-ref", "-d", ref, check=False
727
1065
  )
1066
+ worktrees = self.runner.text(
1067
+ self.project_root, "worktree", "list", "--porcelain"
1068
+ ) or ""
1069
+ feature_ref = f"refs/heads/{self.feature_branch}"
1070
+ feature_branch_retained = self.runner.run(
1071
+ self.project_root,
1072
+ "show-ref",
1073
+ "--verify",
1074
+ feature_ref,
1075
+ check=False,
1076
+ ).returncode == 0
1077
+ local_target = self.runner.text(
1078
+ self.project_root, "rev-parse", self.target_branch
1079
+ )
1080
+ remote_line = self.runner.text(
1081
+ self.project_root, "ls-remote", "origin", self.target_branch
1082
+ )
1083
+ remote_target = (
1084
+ remote_line.split()[0] if remote_line and remote_line.split() else None
1085
+ )
1086
+ journal["cleanupSummary"] = {
1087
+ "transactionArtifactsRemoved": {
1088
+ "integrationWorktree": not intg_root.exists(),
1089
+ "temporaryBranch": self.runner.run(
1090
+ self.project_root,
1091
+ "show-ref",
1092
+ "--verify",
1093
+ f"refs/heads/{self.temp_branch}",
1094
+ check=False,
1095
+ ).returncode != 0,
1096
+ "protectionRefs": self._step(journal, "push")["status"] == "DONE",
1097
+ "heavyRoots": cleanup_result.get("heavyRootsRemoved") or [],
1098
+ },
1099
+ "sourceWorktreeRetained": f"branch {feature_ref}" in worktrees,
1100
+ "featureBranchRetained": feature_branch_retained,
1101
+ "primaryWorktreeUpdated": bool(
1102
+ local_target and remote_target and local_target == remote_target
1103
+ ),
1104
+ "primaryHead": local_target,
1105
+ "remoteHead": remote_target,
1106
+ "residualPaths": cleanup_result.get("residualPaths") or [],
1107
+ }
1108
+ self._save(journal)
728
1109
 
729
1110
  journal = self._run_step("cleanup", action)
730
1111
  harness_change.integration_lock_release(self.project_root, run_id=self.run_id)
@@ -864,9 +1245,13 @@ def main(argv: list[str] | None = None) -> int:
864
1245
  try:
865
1246
  return int(args.func(args))
866
1247
  except IntegrationError as exc:
1248
+ extra = getattr(exc, "result", None)
1249
+ payload = {"ok": False, "code": exc.code, "message": str(exc)}
1250
+ if isinstance(extra, dict):
1251
+ payload["cleanup"] = extra
867
1252
  sys.stderr.write(
868
1253
  json.dumps(
869
- {"ok": False, "code": exc.code, "message": str(exc)},
1254
+ payload,
870
1255
  ensure_ascii=False,
871
1256
  )
872
1257
  + "\n"