@hunter-harness/workflow-harness 0.4.2 → 0.4.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (242) hide show
  1. package/harness/bundles/general/claude-code/.harness-build.json +1 -1
  2. package/harness/bundles/general/claude-code/harness-archive/SKILL.md +4 -3
  3. package/harness/bundles/general/claude-code/harness-codebase-map/SKILL.md +1 -1
  4. package/harness/bundles/general/claude-code/harness-execute/SKILL.md +4 -2
  5. package/harness/bundles/general/claude-code/harness-knowledge-ingest/SKILL.md +1 -1
  6. package/harness/bundles/general/claude-code/harness-knowledge-query/SKILL.md +1 -1
  7. package/harness/bundles/general/claude-code/harness-plan/SKILL.md +2 -2
  8. package/harness/bundles/general/claude-code/harness-plan/reference.md +30 -3
  9. package/harness/bundles/general/claude-code/harness-pull/SKILL.md +1 -1
  10. package/harness/bundles/general/claude-code/harness-push/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-submit/SKILL.md +2 -2
  14. package/harness/bundles/general/claude-code/harness-sync/SKILL.md +2 -2
  15. package/harness/bundles/general/claude-code/harness-test/SKILL.md +1 -1
  16. package/harness/bundles/general/claude-code/scripts/harness_archive.py +28 -1
  17. package/harness/bundles/general/claude-code/scripts/harness_change.py +74 -0
  18. package/harness/bundles/general/claude-code/scripts/harness_context.py +271 -10
  19. package/harness/bundles/general/claude-code/scripts/harness_events.py +4 -1
  20. package/harness/bundles/general/claude-code/scripts/harness_gate.py +280 -76
  21. package/harness/bundles/general/claude-code/scripts/harness_review.py +120 -0
  22. package/harness/bundles/general/claude-code/scripts/harness_runtime.py +14 -0
  23. package/harness/bundles/general/codebuddy/.harness-build.json +1 -1
  24. package/harness/bundles/general/codebuddy/harness-archive/SKILL.md +4 -3
  25. package/harness/bundles/general/codebuddy/harness-codebase-map/SKILL.md +1 -1
  26. package/harness/bundles/general/codebuddy/harness-execute/SKILL.md +4 -2
  27. package/harness/bundles/general/codebuddy/harness-knowledge-ingest/SKILL.md +1 -1
  28. package/harness/bundles/general/codebuddy/harness-knowledge-query/SKILL.md +1 -1
  29. package/harness/bundles/general/codebuddy/harness-plan/SKILL.md +2 -2
  30. package/harness/bundles/general/codebuddy/harness-plan/reference.md +30 -3
  31. package/harness/bundles/general/codebuddy/harness-pull/SKILL.md +1 -1
  32. package/harness/bundles/general/codebuddy/harness-push/SKILL.md +1 -1
  33. package/harness/bundles/general/codebuddy/harness-review/SKILL.md +3 -3
  34. package/harness/bundles/general/codebuddy/harness-run/SKILL.md +1 -1
  35. package/harness/bundles/general/codebuddy/harness-submit/SKILL.md +2 -2
  36. package/harness/bundles/general/codebuddy/harness-sync/SKILL.md +2 -2
  37. package/harness/bundles/general/codebuddy/harness-test/SKILL.md +1 -1
  38. package/harness/bundles/general/codebuddy/scripts/harness_archive.py +28 -1
  39. package/harness/bundles/general/codebuddy/scripts/harness_change.py +74 -0
  40. package/harness/bundles/general/codebuddy/scripts/harness_context.py +271 -10
  41. package/harness/bundles/general/codebuddy/scripts/harness_events.py +4 -1
  42. package/harness/bundles/general/codebuddy/scripts/harness_gate.py +280 -76
  43. package/harness/bundles/general/codebuddy/scripts/harness_review.py +120 -0
  44. package/harness/bundles/general/codebuddy/scripts/harness_runtime.py +14 -0
  45. package/harness/bundles/general/codex/.harness-build.json +1 -1
  46. package/harness/bundles/general/codex/harness-archive/SKILL.md +4 -3
  47. package/harness/bundles/general/codex/harness-codebase-map/SKILL.md +1 -1
  48. package/harness/bundles/general/codex/harness-execute/SKILL.md +4 -2
  49. package/harness/bundles/general/codex/harness-knowledge-ingest/SKILL.md +1 -1
  50. package/harness/bundles/general/codex/harness-knowledge-query/SKILL.md +1 -1
  51. package/harness/bundles/general/codex/harness-plan/SKILL.md +2 -2
  52. package/harness/bundles/general/codex/harness-plan/reference.md +30 -3
  53. package/harness/bundles/general/codex/harness-pull/SKILL.md +1 -1
  54. package/harness/bundles/general/codex/harness-push/SKILL.md +1 -1
  55. package/harness/bundles/general/codex/harness-review/SKILL.md +1 -1
  56. package/harness/bundles/general/codex/harness-run/SKILL.md +1 -1
  57. package/harness/bundles/general/codex/harness-submit/SKILL.md +2 -2
  58. package/harness/bundles/general/codex/harness-sync/SKILL.md +2 -2
  59. package/harness/bundles/general/codex/harness-test/SKILL.md +1 -1
  60. package/harness/bundles/general/codex/scripts/harness_archive.py +28 -1
  61. package/harness/bundles/general/codex/scripts/harness_change.py +74 -0
  62. package/harness/bundles/general/codex/scripts/harness_context.py +271 -10
  63. package/harness/bundles/general/codex/scripts/harness_events.py +4 -1
  64. package/harness/bundles/general/codex/scripts/harness_gate.py +280 -76
  65. package/harness/bundles/general/codex/scripts/harness_review.py +120 -0
  66. package/harness/bundles/general/codex/scripts/harness_runtime.py +14 -0
  67. package/harness/bundles/general/cursor/.harness-build.json +1 -1
  68. package/harness/bundles/general/cursor/harness-archive/SKILL.md +4 -3
  69. package/harness/bundles/general/cursor/harness-codebase-map/SKILL.md +1 -1
  70. package/harness/bundles/general/cursor/harness-execute/SKILL.md +4 -2
  71. package/harness/bundles/general/cursor/harness-knowledge-ingest/SKILL.md +1 -1
  72. package/harness/bundles/general/cursor/harness-knowledge-query/SKILL.md +1 -1
  73. package/harness/bundles/general/cursor/harness-plan/SKILL.md +2 -2
  74. package/harness/bundles/general/cursor/harness-plan/reference.md +30 -3
  75. package/harness/bundles/general/cursor/harness-pull/SKILL.md +1 -1
  76. package/harness/bundles/general/cursor/harness-push/SKILL.md +1 -1
  77. package/harness/bundles/general/cursor/harness-review/SKILL.md +1 -1
  78. package/harness/bundles/general/cursor/harness-run/SKILL.md +1 -1
  79. package/harness/bundles/general/cursor/harness-submit/SKILL.md +2 -2
  80. package/harness/bundles/general/cursor/harness-sync/SKILL.md +2 -2
  81. package/harness/bundles/general/cursor/harness-test/SKILL.md +1 -1
  82. package/harness/bundles/general/cursor/scripts/harness_archive.py +28 -1
  83. package/harness/bundles/general/cursor/scripts/harness_change.py +74 -0
  84. package/harness/bundles/general/cursor/scripts/harness_context.py +271 -10
  85. package/harness/bundles/general/cursor/scripts/harness_events.py +4 -1
  86. package/harness/bundles/general/cursor/scripts/harness_gate.py +280 -76
  87. package/harness/bundles/general/cursor/scripts/harness_review.py +120 -0
  88. package/harness/bundles/general/cursor/scripts/harness_runtime.py +14 -0
  89. package/harness/bundles/general/pi/.harness-build.json +1 -1
  90. package/harness/bundles/general/pi/harness-archive/SKILL.md +4 -3
  91. package/harness/bundles/general/pi/harness-codebase-map/SKILL.md +1 -1
  92. package/harness/bundles/general/pi/harness-execute/SKILL.md +4 -2
  93. package/harness/bundles/general/pi/harness-knowledge-ingest/SKILL.md +1 -1
  94. package/harness/bundles/general/pi/harness-knowledge-query/SKILL.md +1 -1
  95. package/harness/bundles/general/pi/harness-plan/SKILL.md +2 -2
  96. package/harness/bundles/general/pi/harness-plan/reference.md +30 -3
  97. package/harness/bundles/general/pi/harness-pull/SKILL.md +1 -1
  98. package/harness/bundles/general/pi/harness-push/SKILL.md +1 -1
  99. package/harness/bundles/general/pi/harness-review/SKILL.md +1 -1
  100. package/harness/bundles/general/pi/harness-run/SKILL.md +1 -1
  101. package/harness/bundles/general/pi/harness-submit/SKILL.md +2 -2
  102. package/harness/bundles/general/pi/harness-sync/SKILL.md +2 -2
  103. package/harness/bundles/general/pi/harness-test/SKILL.md +1 -1
  104. package/harness/bundles/general/pi/scripts/harness_archive.py +28 -1
  105. package/harness/bundles/general/pi/scripts/harness_change.py +74 -0
  106. package/harness/bundles/general/pi/scripts/harness_context.py +271 -10
  107. package/harness/bundles/general/pi/scripts/harness_events.py +4 -1
  108. package/harness/bundles/general/pi/scripts/harness_gate.py +280 -76
  109. package/harness/bundles/general/pi/scripts/harness_review.py +120 -0
  110. package/harness/bundles/general/pi/scripts/harness_runtime.py +14 -0
  111. package/harness/bundles/java/claude-code/.harness-build.json +1 -1
  112. package/harness/bundles/java/claude-code/harness-apidoc/SKILL.md +1 -1
  113. package/harness/bundles/java/claude-code/harness-archive/SKILL.md +4 -3
  114. package/harness/bundles/java/claude-code/harness-codebase-map/SKILL.md +1 -1
  115. package/harness/bundles/java/claude-code/harness-execute/SKILL.md +4 -2
  116. package/harness/bundles/java/claude-code/harness-knowledge-ingest/SKILL.md +1 -1
  117. package/harness/bundles/java/claude-code/harness-knowledge-query/SKILL.md +1 -1
  118. package/harness/bundles/java/claude-code/harness-package/SKILL.md +1 -1
  119. package/harness/bundles/java/claude-code/harness-plan/SKILL.md +2 -2
  120. package/harness/bundles/java/claude-code/harness-plan/reference.md +30 -3
  121. package/harness/bundles/java/claude-code/harness-pull/SKILL.md +1 -1
  122. package/harness/bundles/java/claude-code/harness-push/SKILL.md +1 -1
  123. package/harness/bundles/java/claude-code/harness-review/SKILL.md +3 -3
  124. package/harness/bundles/java/claude-code/harness-run/SKILL.md +1 -1
  125. package/harness/bundles/java/claude-code/harness-submit/SKILL.md +2 -2
  126. package/harness/bundles/java/claude-code/harness-sync/SKILL.md +2 -2
  127. package/harness/bundles/java/claude-code/harness-test/SKILL.md +1 -1
  128. package/harness/bundles/java/claude-code/scripts/harness_archive.py +28 -1
  129. package/harness/bundles/java/claude-code/scripts/harness_change.py +74 -0
  130. package/harness/bundles/java/claude-code/scripts/harness_context.py +271 -10
  131. package/harness/bundles/java/claude-code/scripts/harness_events.py +4 -1
  132. package/harness/bundles/java/claude-code/scripts/harness_gate.py +280 -76
  133. package/harness/bundles/java/claude-code/scripts/harness_review.py +120 -0
  134. package/harness/bundles/java/claude-code/scripts/harness_runtime.py +14 -0
  135. package/harness/bundles/java/codebuddy/.harness-build.json +1 -1
  136. package/harness/bundles/java/codebuddy/harness-apidoc/SKILL.md +1 -1
  137. package/harness/bundles/java/codebuddy/harness-archive/SKILL.md +4 -3
  138. package/harness/bundles/java/codebuddy/harness-codebase-map/SKILL.md +1 -1
  139. package/harness/bundles/java/codebuddy/harness-execute/SKILL.md +4 -2
  140. package/harness/bundles/java/codebuddy/harness-knowledge-ingest/SKILL.md +1 -1
  141. package/harness/bundles/java/codebuddy/harness-knowledge-query/SKILL.md +1 -1
  142. package/harness/bundles/java/codebuddy/harness-package/SKILL.md +1 -1
  143. package/harness/bundles/java/codebuddy/harness-plan/SKILL.md +2 -2
  144. package/harness/bundles/java/codebuddy/harness-plan/reference.md +30 -3
  145. package/harness/bundles/java/codebuddy/harness-pull/SKILL.md +1 -1
  146. package/harness/bundles/java/codebuddy/harness-push/SKILL.md +1 -1
  147. package/harness/bundles/java/codebuddy/harness-review/SKILL.md +3 -3
  148. package/harness/bundles/java/codebuddy/harness-run/SKILL.md +1 -1
  149. package/harness/bundles/java/codebuddy/harness-submit/SKILL.md +2 -2
  150. package/harness/bundles/java/codebuddy/harness-sync/SKILL.md +2 -2
  151. package/harness/bundles/java/codebuddy/harness-test/SKILL.md +1 -1
  152. package/harness/bundles/java/codebuddy/scripts/harness_archive.py +28 -1
  153. package/harness/bundles/java/codebuddy/scripts/harness_change.py +74 -0
  154. package/harness/bundles/java/codebuddy/scripts/harness_context.py +271 -10
  155. package/harness/bundles/java/codebuddy/scripts/harness_events.py +4 -1
  156. package/harness/bundles/java/codebuddy/scripts/harness_gate.py +280 -76
  157. package/harness/bundles/java/codebuddy/scripts/harness_review.py +120 -0
  158. package/harness/bundles/java/codebuddy/scripts/harness_runtime.py +14 -0
  159. package/harness/bundles/java/codex/.harness-build.json +1 -1
  160. package/harness/bundles/java/codex/harness-apidoc/SKILL.md +1 -1
  161. package/harness/bundles/java/codex/harness-archive/SKILL.md +4 -3
  162. package/harness/bundles/java/codex/harness-codebase-map/SKILL.md +1 -1
  163. package/harness/bundles/java/codex/harness-execute/SKILL.md +4 -2
  164. package/harness/bundles/java/codex/harness-knowledge-ingest/SKILL.md +1 -1
  165. package/harness/bundles/java/codex/harness-knowledge-query/SKILL.md +1 -1
  166. package/harness/bundles/java/codex/harness-package/SKILL.md +1 -1
  167. package/harness/bundles/java/codex/harness-plan/SKILL.md +2 -2
  168. package/harness/bundles/java/codex/harness-plan/reference.md +30 -3
  169. package/harness/bundles/java/codex/harness-pull/SKILL.md +1 -1
  170. package/harness/bundles/java/codex/harness-push/SKILL.md +1 -1
  171. package/harness/bundles/java/codex/harness-review/SKILL.md +1 -1
  172. package/harness/bundles/java/codex/harness-run/SKILL.md +1 -1
  173. package/harness/bundles/java/codex/harness-submit/SKILL.md +2 -2
  174. package/harness/bundles/java/codex/harness-sync/SKILL.md +2 -2
  175. package/harness/bundles/java/codex/harness-test/SKILL.md +1 -1
  176. package/harness/bundles/java/codex/scripts/harness_archive.py +28 -1
  177. package/harness/bundles/java/codex/scripts/harness_change.py +74 -0
  178. package/harness/bundles/java/codex/scripts/harness_context.py +271 -10
  179. package/harness/bundles/java/codex/scripts/harness_events.py +4 -1
  180. package/harness/bundles/java/codex/scripts/harness_gate.py +280 -76
  181. package/harness/bundles/java/codex/scripts/harness_review.py +120 -0
  182. package/harness/bundles/java/codex/scripts/harness_runtime.py +14 -0
  183. package/harness/bundles/java/cursor/.harness-build.json +1 -1
  184. package/harness/bundles/java/cursor/harness-apidoc/SKILL.md +1 -1
  185. package/harness/bundles/java/cursor/harness-archive/SKILL.md +4 -3
  186. package/harness/bundles/java/cursor/harness-codebase-map/SKILL.md +1 -1
  187. package/harness/bundles/java/cursor/harness-execute/SKILL.md +4 -2
  188. package/harness/bundles/java/cursor/harness-knowledge-ingest/SKILL.md +1 -1
  189. package/harness/bundles/java/cursor/harness-knowledge-query/SKILL.md +1 -1
  190. package/harness/bundles/java/cursor/harness-package/SKILL.md +1 -1
  191. package/harness/bundles/java/cursor/harness-plan/SKILL.md +2 -2
  192. package/harness/bundles/java/cursor/harness-plan/reference.md +30 -3
  193. package/harness/bundles/java/cursor/harness-pull/SKILL.md +1 -1
  194. package/harness/bundles/java/cursor/harness-push/SKILL.md +1 -1
  195. package/harness/bundles/java/cursor/harness-review/SKILL.md +1 -1
  196. package/harness/bundles/java/cursor/harness-run/SKILL.md +1 -1
  197. package/harness/bundles/java/cursor/harness-submit/SKILL.md +2 -2
  198. package/harness/bundles/java/cursor/harness-sync/SKILL.md +2 -2
  199. package/harness/bundles/java/cursor/harness-test/SKILL.md +1 -1
  200. package/harness/bundles/java/cursor/scripts/harness_archive.py +28 -1
  201. package/harness/bundles/java/cursor/scripts/harness_change.py +74 -0
  202. package/harness/bundles/java/cursor/scripts/harness_context.py +271 -10
  203. package/harness/bundles/java/cursor/scripts/harness_events.py +4 -1
  204. package/harness/bundles/java/cursor/scripts/harness_gate.py +280 -76
  205. package/harness/bundles/java/cursor/scripts/harness_review.py +120 -0
  206. package/harness/bundles/java/cursor/scripts/harness_runtime.py +14 -0
  207. package/harness/bundles/java/pi/.harness-build.json +1 -1
  208. package/harness/bundles/java/pi/harness-apidoc/SKILL.md +1 -1
  209. package/harness/bundles/java/pi/harness-archive/SKILL.md +4 -3
  210. package/harness/bundles/java/pi/harness-codebase-map/SKILL.md +1 -1
  211. package/harness/bundles/java/pi/harness-execute/SKILL.md +4 -2
  212. package/harness/bundles/java/pi/harness-knowledge-ingest/SKILL.md +1 -1
  213. package/harness/bundles/java/pi/harness-knowledge-query/SKILL.md +1 -1
  214. package/harness/bundles/java/pi/harness-package/SKILL.md +1 -1
  215. package/harness/bundles/java/pi/harness-plan/SKILL.md +2 -2
  216. package/harness/bundles/java/pi/harness-plan/reference.md +30 -3
  217. package/harness/bundles/java/pi/harness-pull/SKILL.md +1 -1
  218. package/harness/bundles/java/pi/harness-push/SKILL.md +1 -1
  219. package/harness/bundles/java/pi/harness-review/SKILL.md +1 -1
  220. package/harness/bundles/java/pi/harness-run/SKILL.md +1 -1
  221. package/harness/bundles/java/pi/harness-submit/SKILL.md +2 -2
  222. package/harness/bundles/java/pi/harness-sync/SKILL.md +2 -2
  223. package/harness/bundles/java/pi/harness-test/SKILL.md +1 -1
  224. package/harness/bundles/java/pi/scripts/harness_archive.py +28 -1
  225. package/harness/bundles/java/pi/scripts/harness_change.py +74 -0
  226. package/harness/bundles/java/pi/scripts/harness_context.py +271 -10
  227. package/harness/bundles/java/pi/scripts/harness_events.py +4 -1
  228. package/harness/bundles/java/pi/scripts/harness_gate.py +280 -76
  229. package/harness/bundles/java/pi/scripts/harness_review.py +120 -0
  230. package/harness/bundles/java/pi/scripts/harness_runtime.py +14 -0
  231. package/harness/manifests/general/claude-code.json +22 -22
  232. package/harness/manifests/general/codebuddy.json +22 -22
  233. package/harness/manifests/general/codex.json +22 -22
  234. package/harness/manifests/general/cursor.json +22 -22
  235. package/harness/manifests/general/pi.json +22 -22
  236. package/harness/manifests/java/claude-code.json +24 -24
  237. package/harness/manifests/java/codebuddy.json +24 -24
  238. package/harness/manifests/java/codex.json +24 -24
  239. package/harness/manifests/java/cursor.json +24 -24
  240. package/harness/manifests/java/pi.json +24 -24
  241. package/hunter-workflow-family.json +2 -2
  242. package/package.json +15 -15
@@ -317,6 +317,24 @@ def _allowed_next_phases(contract_root: Path, from_phase: str) -> list[str]:
317
317
  return list(dict.fromkeys(allowed))
318
318
 
319
319
 
320
+ def allowed_next_phases(project: Path, change: str, phase: str) -> list[str]:
321
+ """公开版后继查询:包装 _contract + _allowed_next_phases。
322
+
323
+ gate close 的幂等续跑(phase.end 已写但 handoff 未完成的中间态)需要在
324
+ 没有 --to-phase 的情况下派生唯一计划后继;契约读取失败一律回退空表,
325
+ 由调用方退到"后继不明确,请显式 --to-phase"的报错路径。
326
+ """
327
+ project = Path(project).resolve()
328
+ try:
329
+ contract_root, _contract_data, _state_root = _contract(project, change)
330
+ except (OSError, ValueError, json.JSONDecodeError):
331
+ return []
332
+ canonical = hpaths.resolve_phase_name(phase)
333
+ if canonical is None:
334
+ return []
335
+ return _allowed_next_phases(contract_root, canonical)
336
+
337
+
320
338
  def configure_phase_plan(
321
339
  project: Path,
322
340
  change: str,
@@ -450,6 +468,16 @@ def _execution_root(project: Path, contract_root: Path, state_root: Path) -> Pat
450
468
  return project.resolve()
451
469
 
452
470
 
471
+ def _phase_has_ended(event_paths: list[Path], phase: str) -> bool:
472
+ """目标阶段是否已写入 phase.end(关门事实,与租约/交接状态无关)。"""
473
+ return any(
474
+ hpaths.resolve_phase_name(item.get("phase")) == phase
475
+ and item.get("type") == "phase.end"
476
+ for path in event_paths
477
+ for item in _read_ndjson(path)
478
+ )
479
+
480
+
453
481
  def _reselect_review_fixback(
454
482
  project: Path,
455
483
  change: str,
@@ -468,13 +496,75 @@ def _reselect_review_fixback(
468
496
  isinstance(latest, dict)
469
497
  and hpaths.resolve_phase_name(latest.get("fromPhase")) == "review"
470
498
  and hpaths.resolve_phase_name(latest.get("toPhase")) == "execute"
471
- and latest.get("trigger") == "review-fixback"
472
499
  ):
500
+ # review→execute 已选定即为目标分支:trigger 只区分来源(close 时显式
501
+ # --to-phase execute 的也是同一份意图),不该把修复挡在门外
473
502
  return {
474
503
  "ok": True,
475
504
  "code": "FIXBACK_BRANCH_ALREADY_SELECTED",
476
505
  "receipt": latest,
477
506
  }
507
+ event_paths = [state_root / "events.ndjson"]
508
+ if state_root != contract_root:
509
+ event_paths.append(contract_root / "events.ndjson")
510
+ review_ended = _phase_has_ended(event_paths, "review")
511
+ if (
512
+ isinstance(latest, dict)
513
+ and hpaths.resolve_phase_name(latest.get("fromPhase")) == "execute"
514
+ and hpaths.resolve_phase_name(latest.get("toPhase")) == "review"
515
+ and review_ended
516
+ ):
517
+ # review 已关门但从没写下后继分支(0.4.7 及之前 plain close 不带
518
+ # --to-phase 的静默断链产物):此时分支选择还开着,fixback 是唯一
519
+ # 合法的推进——补写 review→execute 收据,与重选 submit 分支同构。
520
+ if paths["lease"].is_file():
521
+ try:
522
+ lease = _read_json(paths["lease"])
523
+ except (OSError, ValueError, json.JSONDecodeError):
524
+ return {"ok": False, "code": "CONTEXT_LEASE_INVALID"}
525
+ if lease.get("phase") != "review":
526
+ return {
527
+ "ok": False,
528
+ "code": "FIXBACK_RESELECT_UNSAFE",
529
+ "message": "当前上下文租约不属于已结束的评审阶段。",
530
+ }
531
+ paths["lease"].unlink(missing_ok=True)
532
+ paths["current"].unlink(missing_ok=True)
533
+ receipt: dict[str, Any] = {
534
+ "schemaVersion": 1,
535
+ "changeName": change,
536
+ "fromPhase": "review",
537
+ "toPhase": "execute",
538
+ "status": "OK",
539
+ "executor": executor,
540
+ "productCommit": _head(project),
541
+ "artifacts": [],
542
+ "attempt": 1
543
+ + sum(
544
+ 1
545
+ for item in transitions
546
+ if hpaths.resolve_phase_name(item.get("fromPhase")) == "review"
547
+ and hpaths.resolve_phase_name(item.get("toPhase")) == "execute"
548
+ ),
549
+ "closedAt": _now().isoformat(),
550
+ "previousReceiptHash": latest.get("receiptHash"),
551
+ "trigger": "review-fixback",
552
+ "selectionReason": "用户选择处理评审中的代码修复项",
553
+ }
554
+ receipt["receiptHash"] = _payload_hash(receipt)
555
+ _append_ndjson(paths["transitions"], receipt)
556
+ invalidation = _invalidate_for_fixback(
557
+ state_root,
558
+ transition_hash=receipt["receiptHash"],
559
+ from_phase="review",
560
+ to_phase="execute",
561
+ )
562
+ return {
563
+ "ok": True,
564
+ "code": "FIXBACK_BRANCH_RESELECTED",
565
+ "receipt": receipt,
566
+ "invalidation": invalidation,
567
+ }
478
568
  if not (
479
569
  isinstance(latest, dict)
480
570
  and latest.get("fromPhase") == "review"
@@ -484,6 +574,23 @@ def _reselect_review_fixback(
484
574
  "ok": False,
485
575
  "code": "FIXBACK_RESELECT_UNAVAILABLE",
486
576
  "message": "当前没有可安全重选的评审后继分支。",
577
+ "latestTransition": (
578
+ {
579
+ "fromPhase": latest.get("fromPhase"),
580
+ "toPhase": latest.get("toPhase"),
581
+ "trigger": latest.get("trigger"),
582
+ }
583
+ if isinstance(latest, dict)
584
+ else None
585
+ ),
586
+ "reviewPhaseEnded": review_ended,
587
+ "recoveryAction": (
588
+ "修复分支要求评审已关门。若 review 尚未关门:先 "
589
+ "harness_gate.py close --phase review --status <OK|WARN> "
590
+ "(0.4.5+ 可省略 --to-phase,唯一后继自动交接;要进修复则显式 "
591
+ "--to-phase execute),再重跑 /harness-execute --fixback。"
592
+ "若 submit 已关门,修复窗口已过,请走新一轮评审流程。"
593
+ ),
487
594
  }
488
595
  begins = _read_ndjson(paths["begins"])
489
596
  submit_begun = any(
@@ -492,9 +599,6 @@ def _reselect_review_fixback(
492
599
  and item.get("receiptHash") == latest.get("receiptHash")
493
600
  for item in begins
494
601
  )
495
- event_paths = [state_root / "events.ndjson"]
496
- if state_root != contract_root:
497
- event_paths.append(contract_root / "events.ndjson")
498
602
  submit_started = any(
499
603
  item.get("phase") == "submit" and item.get("type") == "phase.start"
500
604
  for path in event_paths
@@ -909,13 +1013,19 @@ def bootstrap_plan(
909
1013
  import harness_state as hs
910
1014
 
911
1015
  project = Path(project).resolve()
912
- changes_root = project / ".harness" / "changes"
1016
+ harness_root = project / ".harness"
1017
+ changes_root = harness_root / "changes"
913
1018
  if not changes_root.is_dir():
914
- return {
915
- "ok": False,
916
- "code": "PROJECT_ROOT_INVALID",
917
- "error": f"{changes_root} 不存在——先在该项目运行 hunter-harness init",
918
- }
1019
+ if harness_root.is_dir():
1020
+ # 项目已 init 但从未建过 change——changes/ 目录此前没有任何脚本负责创建,
1021
+ # 不该把用户误导去重跑 init(init 是空白项目用的)
1022
+ changes_root.mkdir(parents=True, exist_ok=True)
1023
+ else:
1024
+ return {
1025
+ "ok": False,
1026
+ "code": "PROJECT_ROOT_INVALID",
1027
+ "error": f"{harness_root} 不存在——该项目尚未初始化,先运行 hunter-harness init",
1028
+ }
919
1029
 
920
1030
  # 新 change 的目录此前没有任何脚本负责创建,靠的是后续 Write 的副作用;
921
1031
  # 引导阶段显式建立骨架,prepare 才有 contract 可读
@@ -1433,6 +1543,134 @@ def _begin_transition_unlocked(
1433
1543
  }
1434
1544
 
1435
1545
 
1546
+ def handoff_transition(
1547
+ project: Path,
1548
+ change: str,
1549
+ *,
1550
+ to_phase: str,
1551
+ executor: str,
1552
+ from_phase: str | None = None,
1553
+ status: str = "OK",
1554
+ ) -> dict[str, Any]:
1555
+ """一条命令补齐缺失的阶段交接:补租约 → close → begin 确认。
1556
+
1557
+ 0.4.7 及之前的 plain gate close 不写交接,后继阶段 begin 撞
1558
+ CONTEXT_HANDOFF_REQUIRED 后,使用者要在 context close(缺租约报错)→
1559
+ change claim → context prepare → context close → context begin →
1560
+ change release 之间试错(2026-08-30 sales-insight-agent submit 实测,
1561
+ 六条命令)。本命令把合法恢复收敛为一步:来源阶段必须已关门
1562
+ (phase.end 落盘),缺失的 context 租约由 prepare 幂等重建,
1563
+ 交接收据与 begin 确认走与正常流程完全相同的状态机。阶段租约
1564
+ (harness_change)在 gate close 时已释放,无需也不应重新 claim。
1565
+ """
1566
+ project = Path(project).resolve()
1567
+ canonical_to = hpaths.resolve_phase_name(to_phase)
1568
+ if canonical_to is None:
1569
+ return _unknown_phase_error(to_phase)
1570
+ to_phase = canonical_to
1571
+ try:
1572
+ contract_root, _contract_data, state_root = _contract(project, change)
1573
+ except (OSError, ValueError, json.JSONDecodeError) as exc:
1574
+ return {"ok": False, "code": _contract_error_code(exc), "error": str(exc)}
1575
+ paths = _paths(state_root)
1576
+ transitions = _read_ndjson(paths["transitions"])
1577
+ latest = transitions[-1] if transitions else None
1578
+
1579
+ if from_phase is None:
1580
+ if (
1581
+ isinstance(latest, dict)
1582
+ and hpaths.resolve_phase_name(latest.get("toPhase")) == to_phase
1583
+ ):
1584
+ # 交接已写、只剩 begin 确认的半成品状态:来源从收据取
1585
+ from_phase = str(latest.get("fromPhase") or "")
1586
+ else:
1587
+ view = context_view(project, change)
1588
+ current = (
1589
+ view.get("current") if isinstance(view.get("current"), dict) else {}
1590
+ )
1591
+ from_phase = str(current.get("phase") or "").strip()
1592
+ canonical_from = hpaths.resolve_phase_name(from_phase or "")
1593
+ if canonical_from is None:
1594
+ return {
1595
+ "ok": False,
1596
+ "code": "HANDOFF_SOURCE_UNKNOWN",
1597
+ "message": "无法确定交接来源阶段;请显式传 --from-phase。",
1598
+ "toPhase": to_phase,
1599
+ }
1600
+ from_phase = canonical_from
1601
+
1602
+ closed: dict[str, Any]
1603
+ if (
1604
+ isinstance(latest, dict)
1605
+ and hpaths.resolve_phase_name(latest.get("fromPhase")) == from_phase
1606
+ and hpaths.resolve_phase_name(latest.get("toPhase")) == to_phase
1607
+ ):
1608
+ # 收据已存在——重放 close 只会写重复 receipt,直接进入 begin 确认
1609
+ closed = {
1610
+ "ok": True,
1611
+ "code": "TRANSITION_ALREADY_CLOSED",
1612
+ "idempotent": True,
1613
+ "receipt": latest,
1614
+ }
1615
+ else:
1616
+ event_paths = [state_root / "events.ndjson"]
1617
+ if state_root != contract_root:
1618
+ event_paths.append(contract_root / "events.ndjson")
1619
+ if not _phase_has_ended(event_paths, from_phase):
1620
+ return {
1621
+ "ok": False,
1622
+ "code": "HANDOFF_SOURCE_NOT_CLOSED",
1623
+ "message": (
1624
+ f"{from_phase} 阶段尚未关门(无 phase.end);handoff 只补齐"
1625
+ "已关门阶段的交接,不能截胡进行中的阶段。"
1626
+ ),
1627
+ "fromPhase": from_phase,
1628
+ "toPhase": to_phase,
1629
+ "recoveryAction": (
1630
+ f"先完成来源阶段关门:harness_gate.py close --phase {from_phase} "
1631
+ f"--change {change} --status <OK|WARN> --json"
1632
+ "(0.4.6+ 缺省 --to-phase 时自动派生交接),再重跑本命令。"
1633
+ ),
1634
+ }
1635
+ # 缺失/过期的 context 租约由 prepare 幂等重建;被他人持有则原样报错
1636
+ prepared = prepare_context(
1637
+ project,
1638
+ change=change,
1639
+ phase=from_phase,
1640
+ executor=executor,
1641
+ )
1642
+ if not prepared.get("ok"):
1643
+ return prepared
1644
+ closed = close_transition(
1645
+ project,
1646
+ change,
1647
+ from_phase=from_phase,
1648
+ to_phase=to_phase,
1649
+ executor=executor,
1650
+ status=status,
1651
+ )
1652
+ if not closed.get("ok"):
1653
+ return closed
1654
+ begun = begin_transition(project, change, phase=to_phase, executor=executor)
1655
+ if not begun.get("ok"):
1656
+ return {
1657
+ "ok": False,
1658
+ "code": "HANDOFF_BEGIN_FAILED",
1659
+ "message": "交接收据已写入,但 begin 确认失败。",
1660
+ "transition": closed,
1661
+ "begin": begun,
1662
+ }
1663
+ return {
1664
+ "ok": True,
1665
+ "code": "HANDOFF_COMPLETED",
1666
+ "changeName": change,
1667
+ "fromPhase": from_phase,
1668
+ "toPhase": to_phase,
1669
+ "transition": closed,
1670
+ "begin": begun,
1671
+ }
1672
+
1673
+
1436
1674
  def begin_transition(
1437
1675
  project: Path,
1438
1676
  change: str,
@@ -1689,6 +1927,20 @@ def build_parser() -> argparse.ArgumentParser:
1689
1927
  begin.add_argument("--change", required=True)
1690
1928
  begin.add_argument("--phase", required=True)
1691
1929
  begin.add_argument("--executor", required=True)
1930
+ handoff = sub.add_parser(
1931
+ "handoff",
1932
+ help="一条命令补齐缺失的阶段交接:补租约 → close → begin 确认",
1933
+ )
1934
+ handoff.add_argument("--json", action="store_true")
1935
+ handoff.add_argument("--project", required=True, type=Path)
1936
+ handoff.add_argument("--change", required=True)
1937
+ handoff.add_argument("--to-phase", required=True)
1938
+ handoff.add_argument(
1939
+ "--from-phase",
1940
+ help="缺省从当前上下文/最新交接收据推断",
1941
+ )
1942
+ handoff.add_argument("--executor", required=True)
1943
+ handoff.add_argument("--status", default="OK")
1692
1944
  renew = sub.add_parser(
1693
1945
  "renew", help="extend the current phase lease without touching context"
1694
1946
  )
@@ -1749,6 +2001,15 @@ def main(argv: list[str] | None = None) -> int:
1749
2001
  phase=args.phase,
1750
2002
  executor=args.executor,
1751
2003
  )
2004
+ elif args.command == "handoff":
2005
+ result = handoff_transition(
2006
+ args.project,
2007
+ args.change,
2008
+ to_phase=args.to_phase,
2009
+ from_phase=args.from_phase,
2010
+ executor=args.executor,
2011
+ status=args.status,
2012
+ )
1752
2013
  elif args.command == "renew":
1753
2014
  result = renew_lease(
1754
2015
  args.project,
@@ -927,7 +927,10 @@ def validate_append_event(args: argparse.Namespace) -> tuple[str, str] | None:
927
927
  if re.search(r"\bREVIEW_[A-Z0-9_]+\b", body):
928
928
  return (
929
929
  "EVENT_REVIEW_REASON_IN_BODY",
930
- "EVENT_REVIEW_REASON_IN_BODY: 评审原因码只能写入结构化字段,正文请使用中文说明",
930
+ "EVENT_REVIEW_REASON_IN_BODY: 评审原因码只能写入结构化字段"
931
+ "(--execution-mode delegated|inline,配合 --decision-reason-code "
932
+ "REVIEW_DELEGATED 或 --fallback-reason-code REVIEW_INLINE_*),"
933
+ "正文请使用中文说明",
931
934
  )
932
935
  execution_mode = str(getattr(args, "execution_mode", "") or "").strip()
933
936
  decision_code = str(