@hunter-harness/workflow-harness 0.2.55 → 0.2.56

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (246) hide show
  1. package/harness/bundles/general/claude-code/.harness-build.json +1 -1
  2. package/harness/bundles/general/claude-code/README.md +3 -3
  3. package/harness/bundles/general/claude-code/contracts/workflow-policy.json +1 -1
  4. package/harness/bundles/general/claude-code/harness-archive/SKILL.md +21 -33
  5. package/harness/bundles/general/claude-code/harness-archive/checklist.md +6 -10
  6. package/harness/bundles/general/claude-code/harness-codebase-map/SKILL.md +1 -1
  7. package/harness/bundles/general/claude-code/harness-knowledge-ingest/SKILL.md +1 -1
  8. package/harness/bundles/general/claude-code/harness-knowledge-query/SKILL.md +1 -1
  9. package/harness/bundles/general/claude-code/harness-plan/SKILL.md +7 -3
  10. package/harness/bundles/general/claude-code/harness-review/SKILL.md +6 -4
  11. package/harness/bundles/general/claude-code/harness-run/SKILL.md +7 -5
  12. package/harness/bundles/general/claude-code/harness-run/checklist.md +1 -1
  13. package/harness/bundles/general/claude-code/harness-run/reference.md +2 -2
  14. package/harness/bundles/general/claude-code/harness-submit/SKILL.md +6 -6
  15. package/harness/bundles/general/claude-code/harness-submit/checklist.md +1 -1
  16. package/harness/bundles/general/claude-code/harness-sync/SKILL.md +1 -1
  17. package/harness/bundles/general/claude-code/harness-test/SKILL.md +6 -6
  18. package/harness/bundles/general/claude-code/harness-test/checklist.md +1 -1
  19. package/harness/bundles/general/claude-code/harness-test/reference.md +2 -2
  20. package/harness/bundles/general/claude-code/protocols/ledger-protocol.md +3 -3
  21. package/harness/bundles/general/claude-code/scripts/harness_archive.py +276 -67
  22. package/harness/bundles/general/claude-code/scripts/harness_context.py +165 -41
  23. package/harness/bundles/general/claude-code/scripts/harness_events.py +46 -4
  24. package/harness/bundles/general/claude-code/scripts/harness_events_sync.py +108 -3
  25. package/harness/bundles/general/claude-code/scripts/harness_fixback.py +189 -1
  26. package/harness/bundles/general/claude-code/scripts/harness_gate.py +188 -2
  27. package/harness/bundles/general/claude-code/scripts/harness_ledger.py +193 -11
  28. package/harness/bundles/general/claude-code/scripts/harness_plan_finalize.py +80 -0
  29. package/harness/bundles/general/codebuddy/.harness-build.json +1 -1
  30. package/harness/bundles/general/codebuddy/README.md +3 -3
  31. package/harness/bundles/general/codebuddy/contracts/workflow-policy.json +1 -1
  32. package/harness/bundles/general/codebuddy/harness-archive/SKILL.md +21 -33
  33. package/harness/bundles/general/codebuddy/harness-archive/checklist.md +6 -10
  34. package/harness/bundles/general/codebuddy/harness-codebase-map/SKILL.md +1 -1
  35. package/harness/bundles/general/codebuddy/harness-knowledge-ingest/SKILL.md +1 -1
  36. package/harness/bundles/general/codebuddy/harness-knowledge-query/SKILL.md +1 -1
  37. package/harness/bundles/general/codebuddy/harness-plan/SKILL.md +7 -3
  38. package/harness/bundles/general/codebuddy/harness-review/SKILL.md +6 -4
  39. package/harness/bundles/general/codebuddy/harness-run/SKILL.md +7 -5
  40. package/harness/bundles/general/codebuddy/harness-run/checklist.md +1 -1
  41. package/harness/bundles/general/codebuddy/harness-run/reference.md +2 -2
  42. package/harness/bundles/general/codebuddy/harness-submit/SKILL.md +6 -6
  43. package/harness/bundles/general/codebuddy/harness-submit/checklist.md +1 -1
  44. package/harness/bundles/general/codebuddy/harness-sync/SKILL.md +1 -1
  45. package/harness/bundles/general/codebuddy/harness-test/SKILL.md +6 -6
  46. package/harness/bundles/general/codebuddy/harness-test/checklist.md +1 -1
  47. package/harness/bundles/general/codebuddy/harness-test/reference.md +2 -2
  48. package/harness/bundles/general/codebuddy/protocols/ledger-protocol.md +3 -3
  49. package/harness/bundles/general/codebuddy/scripts/harness_archive.py +276 -67
  50. package/harness/bundles/general/codebuddy/scripts/harness_context.py +165 -41
  51. package/harness/bundles/general/codebuddy/scripts/harness_events.py +46 -4
  52. package/harness/bundles/general/codebuddy/scripts/harness_events_sync.py +108 -3
  53. package/harness/bundles/general/codebuddy/scripts/harness_fixback.py +189 -1
  54. package/harness/bundles/general/codebuddy/scripts/harness_gate.py +188 -2
  55. package/harness/bundles/general/codebuddy/scripts/harness_ledger.py +193 -11
  56. package/harness/bundles/general/codebuddy/scripts/harness_plan_finalize.py +80 -0
  57. package/harness/bundles/general/codex/.harness-build.json +1 -1
  58. package/harness/bundles/general/codex/README.md +3 -3
  59. package/harness/bundles/general/codex/contracts/workflow-policy.json +1 -1
  60. package/harness/bundles/general/codex/harness-archive/SKILL.md +21 -33
  61. package/harness/bundles/general/codex/harness-archive/checklist.md +6 -10
  62. package/harness/bundles/general/codex/harness-codebase-map/SKILL.md +1 -1
  63. package/harness/bundles/general/codex/harness-knowledge-ingest/SKILL.md +1 -1
  64. package/harness/bundles/general/codex/harness-knowledge-query/SKILL.md +1 -1
  65. package/harness/bundles/general/codex/harness-plan/SKILL.md +7 -3
  66. package/harness/bundles/general/codex/harness-review/SKILL.md +4 -4
  67. package/harness/bundles/general/codex/harness-run/SKILL.md +7 -5
  68. package/harness/bundles/general/codex/harness-run/checklist.md +1 -1
  69. package/harness/bundles/general/codex/harness-run/reference.md +2 -2
  70. package/harness/bundles/general/codex/harness-submit/SKILL.md +6 -6
  71. package/harness/bundles/general/codex/harness-submit/checklist.md +1 -1
  72. package/harness/bundles/general/codex/harness-sync/SKILL.md +1 -1
  73. package/harness/bundles/general/codex/harness-test/SKILL.md +6 -6
  74. package/harness/bundles/general/codex/harness-test/checklist.md +1 -1
  75. package/harness/bundles/general/codex/harness-test/reference.md +2 -2
  76. package/harness/bundles/general/codex/protocols/ledger-protocol.md +3 -3
  77. package/harness/bundles/general/codex/scripts/harness_archive.py +276 -67
  78. package/harness/bundles/general/codex/scripts/harness_context.py +165 -41
  79. package/harness/bundles/general/codex/scripts/harness_events.py +46 -4
  80. package/harness/bundles/general/codex/scripts/harness_events_sync.py +108 -3
  81. package/harness/bundles/general/codex/scripts/harness_fixback.py +189 -1
  82. package/harness/bundles/general/codex/scripts/harness_gate.py +188 -2
  83. package/harness/bundles/general/codex/scripts/harness_ledger.py +193 -11
  84. package/harness/bundles/general/codex/scripts/harness_plan_finalize.py +80 -0
  85. package/harness/bundles/general/cursor/.harness-build.json +1 -1
  86. package/harness/bundles/general/cursor/README.md +3 -3
  87. package/harness/bundles/general/cursor/contracts/workflow-policy.json +1 -1
  88. package/harness/bundles/general/cursor/harness-archive/SKILL.md +21 -33
  89. package/harness/bundles/general/cursor/harness-archive/checklist.md +6 -10
  90. package/harness/bundles/general/cursor/harness-codebase-map/SKILL.md +1 -1
  91. package/harness/bundles/general/cursor/harness-knowledge-ingest/SKILL.md +1 -1
  92. package/harness/bundles/general/cursor/harness-knowledge-query/SKILL.md +1 -1
  93. package/harness/bundles/general/cursor/harness-plan/SKILL.md +7 -3
  94. package/harness/bundles/general/cursor/harness-review/SKILL.md +4 -4
  95. package/harness/bundles/general/cursor/harness-run/SKILL.md +7 -5
  96. package/harness/bundles/general/cursor/harness-run/checklist.md +1 -1
  97. package/harness/bundles/general/cursor/harness-run/reference.md +2 -2
  98. package/harness/bundles/general/cursor/harness-submit/SKILL.md +6 -6
  99. package/harness/bundles/general/cursor/harness-submit/checklist.md +1 -1
  100. package/harness/bundles/general/cursor/harness-sync/SKILL.md +1 -1
  101. package/harness/bundles/general/cursor/harness-test/SKILL.md +6 -6
  102. package/harness/bundles/general/cursor/harness-test/checklist.md +1 -1
  103. package/harness/bundles/general/cursor/harness-test/reference.md +2 -2
  104. package/harness/bundles/general/cursor/protocols/ledger-protocol.md +3 -3
  105. package/harness/bundles/general/cursor/scripts/harness_archive.py +276 -67
  106. package/harness/bundles/general/cursor/scripts/harness_context.py +165 -41
  107. package/harness/bundles/general/cursor/scripts/harness_events.py +46 -4
  108. package/harness/bundles/general/cursor/scripts/harness_events_sync.py +108 -3
  109. package/harness/bundles/general/cursor/scripts/harness_fixback.py +189 -1
  110. package/harness/bundles/general/cursor/scripts/harness_gate.py +188 -2
  111. package/harness/bundles/general/cursor/scripts/harness_ledger.py +193 -11
  112. package/harness/bundles/general/cursor/scripts/harness_plan_finalize.py +80 -0
  113. package/harness/bundles/java/claude-code/.harness-build.json +1 -1
  114. package/harness/bundles/java/claude-code/README.md +3 -3
  115. package/harness/bundles/java/claude-code/contracts/workflow-policy.json +1 -1
  116. package/harness/bundles/java/claude-code/harness-apidoc/SKILL.md +1 -1
  117. package/harness/bundles/java/claude-code/harness-archive/SKILL.md +21 -33
  118. package/harness/bundles/java/claude-code/harness-archive/checklist.md +6 -10
  119. package/harness/bundles/java/claude-code/harness-codebase-map/SKILL.md +1 -1
  120. package/harness/bundles/java/claude-code/harness-knowledge-ingest/SKILL.md +1 -1
  121. package/harness/bundles/java/claude-code/harness-knowledge-query/SKILL.md +1 -1
  122. package/harness/bundles/java/claude-code/harness-package/SKILL.md +1 -1
  123. package/harness/bundles/java/claude-code/harness-package/checklist.md +1 -1
  124. package/harness/bundles/java/claude-code/harness-plan/SKILL.md +7 -3
  125. package/harness/bundles/java/claude-code/harness-review/SKILL.md +6 -4
  126. package/harness/bundles/java/claude-code/harness-run/SKILL.md +7 -5
  127. package/harness/bundles/java/claude-code/harness-run/checklist.md +1 -1
  128. package/harness/bundles/java/claude-code/harness-run/reference.md +2 -2
  129. package/harness/bundles/java/claude-code/harness-submit/SKILL.md +6 -6
  130. package/harness/bundles/java/claude-code/harness-submit/checklist.md +1 -1
  131. package/harness/bundles/java/claude-code/harness-sync/SKILL.md +1 -1
  132. package/harness/bundles/java/claude-code/harness-test/SKILL.md +6 -6
  133. package/harness/bundles/java/claude-code/harness-test/checklist.md +1 -1
  134. package/harness/bundles/java/claude-code/harness-test/reference.md +1 -1
  135. package/harness/bundles/java/claude-code/protocols/ledger-protocol.md +3 -3
  136. package/harness/bundles/java/claude-code/scripts/harness_archive.py +276 -67
  137. package/harness/bundles/java/claude-code/scripts/harness_context.py +165 -41
  138. package/harness/bundles/java/claude-code/scripts/harness_events.py +46 -4
  139. package/harness/bundles/java/claude-code/scripts/harness_events_sync.py +108 -3
  140. package/harness/bundles/java/claude-code/scripts/harness_fixback.py +189 -1
  141. package/harness/bundles/java/claude-code/scripts/harness_gate.py +188 -2
  142. package/harness/bundles/java/claude-code/scripts/harness_ledger.py +193 -11
  143. package/harness/bundles/java/claude-code/scripts/harness_plan_finalize.py +80 -0
  144. package/harness/bundles/java/codebuddy/.harness-build.json +1 -1
  145. package/harness/bundles/java/codebuddy/README.md +3 -3
  146. package/harness/bundles/java/codebuddy/contracts/workflow-policy.json +1 -1
  147. package/harness/bundles/java/codebuddy/harness-apidoc/SKILL.md +1 -1
  148. package/harness/bundles/java/codebuddy/harness-archive/SKILL.md +21 -33
  149. package/harness/bundles/java/codebuddy/harness-archive/checklist.md +6 -10
  150. package/harness/bundles/java/codebuddy/harness-codebase-map/SKILL.md +1 -1
  151. package/harness/bundles/java/codebuddy/harness-knowledge-ingest/SKILL.md +1 -1
  152. package/harness/bundles/java/codebuddy/harness-knowledge-query/SKILL.md +1 -1
  153. package/harness/bundles/java/codebuddy/harness-package/SKILL.md +1 -1
  154. package/harness/bundles/java/codebuddy/harness-package/checklist.md +1 -1
  155. package/harness/bundles/java/codebuddy/harness-plan/SKILL.md +7 -3
  156. package/harness/bundles/java/codebuddy/harness-review/SKILL.md +6 -4
  157. package/harness/bundles/java/codebuddy/harness-run/SKILL.md +7 -5
  158. package/harness/bundles/java/codebuddy/harness-run/checklist.md +1 -1
  159. package/harness/bundles/java/codebuddy/harness-run/reference.md +2 -2
  160. package/harness/bundles/java/codebuddy/harness-submit/SKILL.md +6 -6
  161. package/harness/bundles/java/codebuddy/harness-submit/checklist.md +1 -1
  162. package/harness/bundles/java/codebuddy/harness-sync/SKILL.md +1 -1
  163. package/harness/bundles/java/codebuddy/harness-test/SKILL.md +6 -6
  164. package/harness/bundles/java/codebuddy/harness-test/checklist.md +1 -1
  165. package/harness/bundles/java/codebuddy/harness-test/reference.md +1 -1
  166. package/harness/bundles/java/codebuddy/protocols/ledger-protocol.md +3 -3
  167. package/harness/bundles/java/codebuddy/scripts/harness_archive.py +276 -67
  168. package/harness/bundles/java/codebuddy/scripts/harness_context.py +165 -41
  169. package/harness/bundles/java/codebuddy/scripts/harness_events.py +46 -4
  170. package/harness/bundles/java/codebuddy/scripts/harness_events_sync.py +108 -3
  171. package/harness/bundles/java/codebuddy/scripts/harness_fixback.py +189 -1
  172. package/harness/bundles/java/codebuddy/scripts/harness_gate.py +188 -2
  173. package/harness/bundles/java/codebuddy/scripts/harness_ledger.py +193 -11
  174. package/harness/bundles/java/codebuddy/scripts/harness_plan_finalize.py +80 -0
  175. package/harness/bundles/java/codex/.harness-build.json +1 -1
  176. package/harness/bundles/java/codex/README.md +3 -3
  177. package/harness/bundles/java/codex/contracts/workflow-policy.json +1 -1
  178. package/harness/bundles/java/codex/harness-apidoc/SKILL.md +1 -1
  179. package/harness/bundles/java/codex/harness-archive/SKILL.md +21 -33
  180. package/harness/bundles/java/codex/harness-archive/checklist.md +6 -10
  181. package/harness/bundles/java/codex/harness-codebase-map/SKILL.md +1 -1
  182. package/harness/bundles/java/codex/harness-knowledge-ingest/SKILL.md +1 -1
  183. package/harness/bundles/java/codex/harness-knowledge-query/SKILL.md +1 -1
  184. package/harness/bundles/java/codex/harness-package/SKILL.md +1 -1
  185. package/harness/bundles/java/codex/harness-package/checklist.md +1 -1
  186. package/harness/bundles/java/codex/harness-plan/SKILL.md +7 -3
  187. package/harness/bundles/java/codex/harness-review/SKILL.md +4 -4
  188. package/harness/bundles/java/codex/harness-run/SKILL.md +7 -5
  189. package/harness/bundles/java/codex/harness-run/checklist.md +1 -1
  190. package/harness/bundles/java/codex/harness-run/reference.md +2 -2
  191. package/harness/bundles/java/codex/harness-submit/SKILL.md +6 -6
  192. package/harness/bundles/java/codex/harness-submit/checklist.md +1 -1
  193. package/harness/bundles/java/codex/harness-sync/SKILL.md +1 -1
  194. package/harness/bundles/java/codex/harness-test/SKILL.md +6 -6
  195. package/harness/bundles/java/codex/harness-test/checklist.md +1 -1
  196. package/harness/bundles/java/codex/harness-test/reference.md +1 -1
  197. package/harness/bundles/java/codex/protocols/ledger-protocol.md +3 -3
  198. package/harness/bundles/java/codex/scripts/harness_archive.py +276 -67
  199. package/harness/bundles/java/codex/scripts/harness_context.py +165 -41
  200. package/harness/bundles/java/codex/scripts/harness_events.py +46 -4
  201. package/harness/bundles/java/codex/scripts/harness_events_sync.py +108 -3
  202. package/harness/bundles/java/codex/scripts/harness_fixback.py +189 -1
  203. package/harness/bundles/java/codex/scripts/harness_gate.py +188 -2
  204. package/harness/bundles/java/codex/scripts/harness_ledger.py +193 -11
  205. package/harness/bundles/java/codex/scripts/harness_plan_finalize.py +80 -0
  206. package/harness/bundles/java/cursor/.harness-build.json +1 -1
  207. package/harness/bundles/java/cursor/README.md +3 -3
  208. package/harness/bundles/java/cursor/contracts/workflow-policy.json +1 -1
  209. package/harness/bundles/java/cursor/harness-apidoc/SKILL.md +1 -1
  210. package/harness/bundles/java/cursor/harness-archive/SKILL.md +21 -33
  211. package/harness/bundles/java/cursor/harness-archive/checklist.md +6 -10
  212. package/harness/bundles/java/cursor/harness-codebase-map/SKILL.md +1 -1
  213. package/harness/bundles/java/cursor/harness-knowledge-ingest/SKILL.md +1 -1
  214. package/harness/bundles/java/cursor/harness-knowledge-query/SKILL.md +1 -1
  215. package/harness/bundles/java/cursor/harness-package/SKILL.md +1 -1
  216. package/harness/bundles/java/cursor/harness-package/checklist.md +1 -1
  217. package/harness/bundles/java/cursor/harness-plan/SKILL.md +7 -3
  218. package/harness/bundles/java/cursor/harness-review/SKILL.md +4 -4
  219. package/harness/bundles/java/cursor/harness-run/SKILL.md +7 -5
  220. package/harness/bundles/java/cursor/harness-run/checklist.md +1 -1
  221. package/harness/bundles/java/cursor/harness-run/reference.md +2 -2
  222. package/harness/bundles/java/cursor/harness-submit/SKILL.md +6 -6
  223. package/harness/bundles/java/cursor/harness-submit/checklist.md +1 -1
  224. package/harness/bundles/java/cursor/harness-sync/SKILL.md +1 -1
  225. package/harness/bundles/java/cursor/harness-test/SKILL.md +6 -6
  226. package/harness/bundles/java/cursor/harness-test/checklist.md +1 -1
  227. package/harness/bundles/java/cursor/harness-test/reference.md +1 -1
  228. package/harness/bundles/java/cursor/protocols/ledger-protocol.md +3 -3
  229. package/harness/bundles/java/cursor/scripts/harness_archive.py +276 -67
  230. package/harness/bundles/java/cursor/scripts/harness_context.py +165 -41
  231. package/harness/bundles/java/cursor/scripts/harness_events.py +46 -4
  232. package/harness/bundles/java/cursor/scripts/harness_events_sync.py +108 -3
  233. package/harness/bundles/java/cursor/scripts/harness_fixback.py +189 -1
  234. package/harness/bundles/java/cursor/scripts/harness_gate.py +188 -2
  235. package/harness/bundles/java/cursor/scripts/harness_ledger.py +193 -11
  236. package/harness/bundles/java/cursor/scripts/harness_plan_finalize.py +80 -0
  237. package/harness/manifests/general/claude-code.json +30 -30
  238. package/harness/manifests/general/codebuddy.json +30 -30
  239. package/harness/manifests/general/codex.json +30 -30
  240. package/harness/manifests/general/cursor.json +30 -30
  241. package/harness/manifests/java/claude-code.json +33 -33
  242. package/harness/manifests/java/codebuddy.json +33 -33
  243. package/harness/manifests/java/codex.json +33 -33
  244. package/harness/manifests/java/cursor.json +33 -33
  245. package/hunter-workflow-family.json +5 -5
  246. package/package.json +1 -1
@@ -23,6 +23,18 @@ PHASE_GRAPH = {
23
23
  "submit": (),
24
24
  }
25
25
 
26
+ WORKFLOW_PHASES = (
27
+ "plan",
28
+ "run",
29
+ "test",
30
+ "review",
31
+ "package",
32
+ "apidoc",
33
+ "submit",
34
+ "merge",
35
+ "archive",
36
+ )
37
+
26
38
  DISPLAY_TITLE_MAX_LENGTH = 80
27
39
 
28
40
 
@@ -206,6 +218,107 @@ def _contract(project: Path, change: str) -> tuple[Path, dict[str, Any], Path]:
206
218
  return contract_root, contract, state_root
207
219
 
208
220
 
221
+ def _phase_plan(contract_root: Path) -> tuple[list[str] | None, str]:
222
+ policy_path = contract_root / "meta" / "gate-policy.json"
223
+ if not policy_path.is_file():
224
+ return None, "legacy"
225
+ try:
226
+ policy = _read_json(policy_path)
227
+ except (OSError, ValueError, json.JSONDecodeError):
228
+ return None, "legacy"
229
+ for field, source in (
230
+ ("plannedPhases", "change"),
231
+ ("defaultPhases", "policy-default"),
232
+ ):
233
+ raw = policy.get(field)
234
+ if not isinstance(raw, list):
235
+ continue
236
+ phases = [str(item).strip() for item in raw if str(item).strip()]
237
+ if (
238
+ phases
239
+ and len(phases) == len(set(phases))
240
+ and all(item in WORKFLOW_PHASES for item in phases)
241
+ ):
242
+ return phases, source
243
+ return None, "legacy"
244
+
245
+
246
+ def _allowed_next_phases(contract_root: Path, from_phase: str) -> list[str]:
247
+ planned, _source = _phase_plan(contract_root)
248
+ if planned is None:
249
+ return list(PHASE_GRAPH.get(from_phase, ()))
250
+ if from_phase not in planned:
251
+ return []
252
+ index = planned.index(from_phase)
253
+ allowed = planned[index + 1 : index + 2]
254
+ if from_phase in {"test", "review"} and "run" in planned:
255
+ allowed = [*allowed, "run"]
256
+ return list(dict.fromkeys(allowed))
257
+
258
+
259
+ def configure_phase_plan(
260
+ project: Path,
261
+ change: str,
262
+ *,
263
+ phases: list[str],
264
+ operator: str,
265
+ reason: str,
266
+ ) -> dict[str, Any]:
267
+ """Persist the single phase plan consumed by Context, Archive and Platform."""
268
+ project = Path(project).resolve()
269
+ try:
270
+ contract_root, _contract_data, _state_root = _contract(project, change)
271
+ except (OSError, ValueError, json.JSONDecodeError) as exc:
272
+ return {"ok": False, "code": "CHANGE_NOT_FOUND", "error": str(exc)}
273
+ normalized = [str(item).strip() for item in phases if str(item).strip()]
274
+ if (
275
+ not normalized
276
+ or len(normalized) != len(set(normalized))
277
+ or any(item not in WORKFLOW_PHASES for item in normalized)
278
+ or normalized[0] != "plan"
279
+ or normalized[-1] != "archive"
280
+ ):
281
+ return {
282
+ "ok": False,
283
+ "code": "PHASE_PLAN_INVALID",
284
+ "message": "phase plan must be unique, start with plan, and end with archive",
285
+ "allowedPhases": list(WORKFLOW_PHASES),
286
+ }
287
+ if not operator.strip() or not reason.strip():
288
+ return {
289
+ "ok": False,
290
+ "code": "PHASE_PLAN_JUSTIFICATION_REQUIRED",
291
+ }
292
+ policy_path = contract_root / "meta" / "gate-policy.json"
293
+ policy = _read_json(policy_path) if policy_path.is_file() else {"schemaVersion": 1}
294
+ skipped = [
295
+ {
296
+ "phase": phase,
297
+ "reason": reason.strip(),
298
+ "operator": operator.strip(),
299
+ "decidedAt": _now().isoformat(),
300
+ }
301
+ for phase in WORKFLOW_PHASES
302
+ if phase not in normalized and phase != "merge"
303
+ ]
304
+ policy["plannedPhases"] = normalized
305
+ policy["skippedPhases"] = skipped
306
+ policy["phasePlan"] = {
307
+ "source": "change-override",
308
+ "operator": operator.strip(),
309
+ "reason": reason.strip(),
310
+ "updatedAt": _now().isoformat(),
311
+ }
312
+ _write_json_atomic(policy_path, policy)
313
+ return {
314
+ "ok": True,
315
+ "code": "PHASE_PLAN_CONFIGURED",
316
+ "plannedPhases": normalized,
317
+ "skippedPhases": skipped,
318
+ "path": str(policy_path),
319
+ }
320
+
321
+
209
322
  def _active_changes(project: Path) -> list[str]:
210
323
  root = project.resolve() / ".harness" / "changes"
211
324
  if not root.is_dir():
@@ -319,6 +432,14 @@ def prepare_context(
319
432
  and lifecycle.get("status") not in {None, "active"}
320
433
  ):
321
434
  return {"ok": False, "code": "CHANGE_NOT_ACTIVE", "changeName": change}
435
+ planned_phases, phase_plan_source = _phase_plan(contract_root)
436
+ if planned_phases is not None and phase not in planned_phases:
437
+ return {
438
+ "ok": False,
439
+ "code": "PHASE_NOT_PLANNED",
440
+ "phase": phase,
441
+ "plannedPhases": planned_phases,
442
+ }
322
443
  paths = _paths(state_root)
323
444
  paths["runtime"].mkdir(parents=True, exist_ok=True)
324
445
  recovery: dict[str, Any] | None = None
@@ -395,9 +516,13 @@ def prepare_context(
395
516
  },
396
517
  )
397
518
  next_phases = (
398
- list(PHASE_GRAPH.get(str(latest.get("toPhase")), ()))
399
- if latest
400
- else ["plan", "run"]
519
+ _allowed_next_phases(contract_root, phase)
520
+ if planned_phases is not None
521
+ else (
522
+ list(PHASE_GRAPH.get(str(latest.get("toPhase")), ()))
523
+ if latest
524
+ else ["plan", "run"]
525
+ )
401
526
  )
402
527
  return {
403
528
  "ok": True,
@@ -408,6 +533,8 @@ def prepare_context(
408
533
  "contractRoot": str(contract_root),
409
534
  "stateRoot": str(state_root),
410
535
  "nextPhases": next_phases,
536
+ "plannedPhases": planned_phases,
537
+ "phasePlanSource": phase_plan_source,
411
538
  "legacyBootstrap": not transitions,
412
539
  "latestTransition": latest,
413
540
  "lease": lease,
@@ -423,42 +550,19 @@ def _invalidate_for_fixback(
423
550
  from_phase: str,
424
551
  to_phase: str,
425
552
  ) -> dict[str, Any]:
426
- ledger_path = state_root / "evidence" / "verification-ledger.json"
427
- invalidated: list[str] = []
428
- if ledger_path.is_file():
429
- try:
430
- ledger = _read_json(ledger_path)
431
- except (OSError, ValueError, json.JSONDecodeError):
432
- ledger = {}
433
- targets = ledger.get("verificationTargets")
434
- if isinstance(targets, dict):
435
- for target_id, target in targets.items():
436
- if not isinstance(target, dict):
437
- continue
438
- target["reusable"] = False
439
- target["invalidation"] = {
440
- "code": "FIXBACK_INVALIDATED",
441
- "transitionHash": transition_hash,
442
- "fromPhase": from_phase,
443
- "toPhase": to_phase,
444
- "invalidatedAt": _now().isoformat(),
445
- }
446
- invalidated.append(str(target_id))
447
- validations = ledger.get("validations")
448
- if isinstance(validations, dict):
449
- for entry in validations.values():
450
- if isinstance(entry, dict):
451
- entry["reusable"] = False
452
- entry["invalidation"] = {
453
- "code": "FIXBACK_INVALIDATED",
454
- "transitionHash": transition_hash,
455
- }
456
- _write_json_atomic(ledger_path, ledger)
553
+ # A review→run handoff happens before product files are changed. Invalidating
554
+ # every target here made unrelated API/build evidence unusable. The Fixback
555
+ # resolver now invalidates only entries whose inputsFiles intersect the
556
+ # issue's actual changedFiles.
457
557
  record = {
458
558
  "schemaVersion": 1,
459
- "code": "FIXBACK_INVALIDATED",
559
+ "code": "FIXBACK_INVALIDATION_DEFERRED",
460
560
  "transitionHash": transition_hash,
461
- "targetIds": invalidated,
561
+ "fromPhase": from_phase,
562
+ "toPhase": to_phase,
563
+ "targetIds": [],
564
+ "deferred": True,
565
+ "reason": "changed-files-not-known",
462
566
  "createdAt": _now().isoformat(),
463
567
  }
464
568
  _append_ndjson(
@@ -479,17 +583,22 @@ def close_transition(
479
583
  status: str = "OK",
480
584
  ) -> dict[str, Any]:
481
585
  project = Path(project).resolve()
482
- if to_phase not in PHASE_GRAPH.get(from_phase, ()):
586
+ try:
587
+ contract_root, _contract_data, state_root = _contract(project, change)
588
+ except (OSError, ValueError, json.JSONDecodeError) as exc:
589
+ return {"ok": False, "code": "CHANGE_NOT_FOUND", "error": str(exc)}
590
+ allowed_next = _allowed_next_phases(contract_root, from_phase)
591
+ if to_phase not in allowed_next:
592
+ planned_phases, source = _phase_plan(contract_root)
483
593
  return {
484
594
  "ok": False,
485
595
  "code": "TRANSITION_ILLEGAL",
486
596
  "fromPhase": from_phase,
487
597
  "toPhase": to_phase,
598
+ "allowedNextPhases": allowed_next,
599
+ "plannedPhases": planned_phases,
600
+ "phasePlanSource": source,
488
601
  }
489
- try:
490
- contract_root, _contract_data, state_root = _contract(project, change)
491
- except (OSError, ValueError, json.JSONDecodeError) as exc:
492
- return {"ok": False, "code": "CHANGE_NOT_FOUND", "error": str(exc)}
493
602
  paths = _paths(state_root)
494
603
 
495
604
  artifact_entries: list[dict[str, Any]] = []
@@ -773,6 +882,13 @@ def build_parser() -> argparse.ArgumentParser:
773
882
  begin.add_argument("--change", required=True)
774
883
  begin.add_argument("--phase", required=True)
775
884
  begin.add_argument("--executor", required=True)
885
+ configure = sub.add_parser("configure-plan")
886
+ configure.add_argument("--json", action="store_true")
887
+ configure.add_argument("--project", required=True, type=Path)
888
+ configure.add_argument("--change", required=True)
889
+ configure.add_argument("--phases", required=True)
890
+ configure.add_argument("--operator", required=True)
891
+ configure.add_argument("--reason", required=True)
776
892
  view = sub.add_parser("view")
777
893
  view.add_argument("--json", action="store_true")
778
894
  view.add_argument("--project", required=True, type=Path)
@@ -808,6 +924,14 @@ def main(argv: list[str] | None = None) -> int:
808
924
  phase=args.phase,
809
925
  executor=args.executor,
810
926
  )
927
+ elif args.command == "configure-plan":
928
+ result = configure_phase_plan(
929
+ args.project,
930
+ args.change,
931
+ phases=[item.strip() for item in args.phases.split(",") if item.strip()],
932
+ operator=args.operator,
933
+ reason=args.reason,
934
+ )
811
935
  else:
812
936
  result = context_view(args.project, args.change)
813
937
  print(json.dumps(result, ensure_ascii=False, indent=2))
@@ -71,6 +71,8 @@ EVENT_TYPES = frozenset(
71
71
  "phase.start",
72
72
  "phase.end",
73
73
  "phase.auto_sealed",
74
+ "gate.blocked",
75
+ "gate.recovered",
74
76
  "command",
75
77
  "verification",
76
78
  "artifact",
@@ -165,6 +167,10 @@ _EVENT_ALLOWED_FIELDS = {
165
167
  )
166
168
  | _PROVENANCE_FIELDS,
167
169
  "phase.auto_sealed": frozenset({"status", "reason", "note"}) | _PROVENANCE_FIELDS,
170
+ "gate.blocked": frozenset({"status", "code", "reason", "note"})
171
+ | _PROVENANCE_FIELDS,
172
+ "gate.recovered": frozenset({"status", "code", "reason", "note"})
173
+ | _PROVENANCE_FIELDS,
168
174
  "command": frozenset({"command", "exit_code", "duration_ms", "note"})
169
175
  | _PROVENANCE_FIELDS,
170
176
  "verification": frozenset(
@@ -1798,18 +1804,45 @@ def cmd_append(args: argparse.Namespace) -> int:
1798
1804
  "warning: verification missing --name or --status",
1799
1805
  file=sys.stderr,
1800
1806
  )
1801
- line = json.dumps(event, ensure_ascii=False, separators=(",", ":"))
1802
1807
  phase_closed = False
1803
1808
  projection_error: str | None = None
1809
+ attempt_error: str | None = None
1804
1810
  auto_sealed_events: list[dict[str, Any]] = []
1805
1811
  try:
1806
1812
  with event_file_lock(lock_path):
1807
1813
  existing_events = (
1808
1814
  load_events(path)
1809
1815
  if args.type in {"phase.end", "correction", "phase.start"}
1816
+ or event.get("run_id") is not None
1817
+ or event.get("attempt") is not None
1810
1818
  else []
1811
1819
  )
1812
- if args.type == "phase.end":
1820
+ run_id = event.get("run_id")
1821
+ if run_id is not None:
1822
+ run_attempts = {
1823
+ int(item["attempt"])
1824
+ for item in existing_events
1825
+ if item.get("phase") == event.get("phase")
1826
+ and item.get("run_id") == run_id
1827
+ and isinstance(item.get("attempt"), int)
1828
+ and int(item["attempt"]) > 0
1829
+ }
1830
+ if run_attempts:
1831
+ expected_attempt = max(run_attempts)
1832
+ explicit_attempt = event.get("attempt")
1833
+ if (
1834
+ isinstance(explicit_attempt, int)
1835
+ and explicit_attempt != expected_attempt
1836
+ ):
1837
+ attempt_error = (
1838
+ "EVENT_ATTEMPT_CONFLICT: run_id is already bound to "
1839
+ f"attempt {expected_attempt}, received {explicit_attempt}"
1840
+ )
1841
+ else:
1842
+ event["attempt"] = expected_attempt
1843
+ if attempt_error is not None:
1844
+ pass
1845
+ elif args.type == "phase.end":
1813
1846
  phase_closed = phase_end_already_recorded(existing_events, event)
1814
1847
  elif args.type == "correction":
1815
1848
  try:
@@ -1830,13 +1863,16 @@ def cmd_append(args: argparse.Namespace) -> int:
1830
1863
  phase=args.phase,
1831
1864
  seal_reason=inferred_reason,
1832
1865
  )
1833
- if not phase_closed and projection_error is None:
1866
+ if not phase_closed and projection_error is None and attempt_error is None:
1834
1867
  for seal_event in auto_sealed_events:
1835
1868
  atomic_append_line(
1836
1869
  path,
1837
1870
  json.dumps(seal_event, ensure_ascii=False, separators=(",", ":")),
1838
1871
  )
1839
- atomic_append_line(path, line)
1872
+ atomic_append_line(
1873
+ path,
1874
+ json.dumps(event, ensure_ascii=False, separators=(",", ":")),
1875
+ )
1840
1876
  except (OSError, TimeoutError, ValueError) as exc:
1841
1877
  return emit_error(f"append failed: {exc}", as_json=as_json)
1842
1878
  if phase_closed:
@@ -1852,6 +1888,12 @@ def cmd_append(args: argparse.Namespace) -> int:
1852
1888
  as_json=as_json,
1853
1889
  error_code=error_code,
1854
1890
  )
1891
+ if attempt_error is not None:
1892
+ return emit_error(
1893
+ attempt_error,
1894
+ as_json=as_json,
1895
+ error_code="EVENT_ATTEMPT_CONFLICT",
1896
+ )
1855
1897
 
1856
1898
  _nudge_remote_sync(change_dir)
1857
1899
 
@@ -60,6 +60,12 @@ WHITELIST = {
60
60
  "trigger",
61
61
  "from_phase",
62
62
  "result_status",
63
+ "planned_phases",
64
+ "skipped_phases",
65
+ "next_phase",
66
+ "phase_plan_source",
67
+ "closure_disposition",
68
+ "closure_reason",
63
69
  }
64
70
 
65
71
  MAX_BATCH_SIZE = 100
@@ -79,6 +85,14 @@ PHASE_LABELS = {
79
85
  "archive": "归档",
80
86
  }
81
87
 
88
+ REVIEW_REASON_SUMMARIES = {
89
+ "REVIEW_DELEGATED": "已使用独立评审,主会话负责核验结果。",
90
+ "REVIEW_INLINE_UNAVAILABLE": "当前环境没有可用的隔离评审能力,已由主会话完成评审。",
91
+ "REVIEW_INLINE_SPAWN_FAILED": "独立评审启动失败,已由主会话完成评审。",
92
+ "REVIEW_INLINE_INVALID_RESULT": "独立评审未返回有效结果,已由主会话重新完成评审。",
93
+ "INLINE_BY_ADAPTER": "当前适配器未提供隔离评审,已由主会话完成评审。",
94
+ }
95
+
82
96
 
83
97
  def _yaml_scalar(text: str, key: str) -> str | None:
84
98
  match = re.search(rf"(?m)^[ \t]*{re.escape(key)}:\s*['\"]?([^'\"\n#]+)", text)
@@ -143,8 +157,21 @@ def _readable_summary_text(value: Any) -> str:
143
157
 
144
158
 
145
159
  def build_event_summary(event: dict[str, Any]) -> str:
160
+ reason_code = _summary_text(
161
+ event.get("fallback_reason_code") or event.get("decision_reason_code")
162
+ )
163
+ reason_summary = REVIEW_REASON_SUMMARIES.get(reason_code)
146
164
  explicit = _readable_summary_text(event.get("summary"))
147
- if explicit:
165
+ if reason_summary and (
166
+ not explicit
167
+ or explicit == reason_code
168
+ or re.search(r"REVIEW_[A-Z0-9_]+|INLINE_BY_ADAPTER", explicit)
169
+ ):
170
+ return reason_summary
171
+ if explicit and not re.search(
172
+ r"\b[A-Z][A-Z0-9_]{3,}\b|\b[a-z][\w.-]+\s*=\s*(?:true|false)\b",
173
+ explicit,
174
+ ):
148
175
  return explicit
149
176
 
150
177
  event_type = _summary_text(event.get("type"))
@@ -177,6 +204,10 @@ def build_event_summary(event: dict[str, Any]) -> str:
177
204
  return _summary_text(
178
205
  f"{phase_label}阶段已结束:{detail}" if detail else f"{phase_label}阶段已结束。"
179
206
  )
207
+ if event_type == "gate.blocked":
208
+ return note or message or "阶段门禁暂未通过,现场已保留,可按提示恢复。"
209
+ if event_type == "gate.recovered":
210
+ return note or message or "阶段门禁已恢复,流程可以继续。"
180
211
  if event_type == "command":
181
212
  detail = name or note
182
213
  return _summary_text(f"执行步骤:{detail}" if detail else "执行了一项工作步骤。")
@@ -205,6 +236,48 @@ def sanitize(event: dict[str, Any]) -> dict[str, Any]:
205
236
  return payload
206
237
 
207
238
 
239
+ def _workflow_plan(change_dir: Path) -> dict[str, Any] | None:
240
+ policy_path = change_dir / "meta" / "gate-policy.json"
241
+ if not policy_path.is_file():
242
+ return None
243
+ try:
244
+ policy = json.loads(policy_path.read_text(encoding="utf-8-sig"))
245
+ except (OSError, json.JSONDecodeError):
246
+ return None
247
+ if not isinstance(policy, dict):
248
+ return None
249
+ raw = policy.get("plannedPhases")
250
+ source = "change"
251
+ if not isinstance(raw, list) or not raw:
252
+ raw = policy.get("defaultPhases")
253
+ source = "policy-default"
254
+ if not isinstance(raw, list):
255
+ return None
256
+ phases = [str(item).strip() for item in raw if str(item).strip()]
257
+ if not phases:
258
+ return None
259
+ skipped = policy.get("skippedPhases")
260
+ return {
261
+ "planned_phases": list(dict.fromkeys(phases)),
262
+ "skipped_phases": skipped if isinstance(skipped, list) else [],
263
+ "phase_plan_source": source,
264
+ }
265
+
266
+
267
+ def _event_payload(event: dict[str, Any], phase_plan: dict[str, Any] | None) -> dict[str, Any]:
268
+ payload = sanitize(event)
269
+ if phase_plan is None:
270
+ return payload
271
+ payload.update(phase_plan)
272
+ if str(event.get("type") or "") in {"phase.end", "phase.auto_sealed"}:
273
+ phase = str(event.get("phase") or "").strip()
274
+ planned = phase_plan["planned_phases"]
275
+ if phase in planned:
276
+ index = planned.index(phase)
277
+ payload["next_phase"] = planned[index + 1] if index + 1 < len(planned) else None
278
+ return payload
279
+
280
+
208
281
  def _valid_display_title(value: Any) -> str | None:
209
282
  if not isinstance(value, str):
210
283
  return None
@@ -469,6 +542,7 @@ def sync_change(
469
542
  repair=True,
470
543
  )
471
544
  pending_raw = lines[acked:]
545
+ phase_plan = _workflow_plan(change_dir)
472
546
  uploaded = 0
473
547
  quarantined = 0
474
548
  last_result: dict[str, Any] | None = None
@@ -500,7 +574,7 @@ def sync_change(
500
574
  "producer_seq": line_number,
501
575
  "event_type": str(event.get("type") or "unknown"),
502
576
  "occurred_at": str(event.get("timestamp") or event.get("ts") or client_time),
503
- "payload": sanitize(event),
577
+ "payload": _event_payload(event, phase_plan),
504
578
  }
505
579
  if event.get("phase") is not None:
506
580
  item["phase"] = event.get("phase")
@@ -727,6 +801,31 @@ class _CrossProcessLock:
727
801
  self._stream = None
728
802
 
729
803
 
804
+ def _windowless_python_executable(
805
+ executable: str, base_executable: str | None
806
+ ) -> str:
807
+ """Return a Windows interpreter that cannot allocate a console window.
808
+
809
+ Virtual-environment ``python.exe`` files may be forwarding launchers. Even
810
+ when the first process is created with ``CREATE_NO_WINDOW``, such a launcher
811
+ can start the real console interpreter and make Windows Terminal flash. A
812
+ sibling ``pythonw.exe`` preserves the windowless subsystem across that hop.
813
+ """
814
+ executable_path = Path(executable)
815
+ candidates = [executable_path.with_name("pythonw.exe")]
816
+ if base_executable:
817
+ base_path = Path(base_executable)
818
+ base_windowless = base_path.with_name("pythonw.exe")
819
+ if base_windowless not in candidates:
820
+ candidates.append(base_windowless)
821
+ for candidate in candidates:
822
+ if candidate.is_file():
823
+ return str(candidate)
824
+ if base_executable and Path(base_executable).is_file():
825
+ return str(Path(base_executable))
826
+ return str(executable_path)
827
+
828
+
730
829
  def schedule_events_sync(project_root: Path, change_dir: Path) -> bool:
731
830
  """Wake a finite, project-local sync worker after an event is durable.
732
831
 
@@ -748,8 +847,14 @@ def schedule_events_sync(project_root: Path, change_dir: Path) -> bool:
748
847
  temporary = nudge.with_name(f".{nudge.name}.{uuid.uuid4().hex}.tmp")
749
848
  temporary.write_text(json.dumps(payload, separators=(",", ":")) + "\n", encoding="utf-8")
750
849
  os.replace(temporary, nudge)
850
+ interpreter = sys.executable
851
+ if os.name == "nt":
852
+ interpreter = _windowless_python_executable(
853
+ sys.executable,
854
+ getattr(sys, "_base_executable", None),
855
+ )
751
856
  command = [
752
- sys.executable,
857
+ interpreter,
753
858
  str(Path(__file__).resolve()),
754
859
  "--project",
755
860
  str(project_root),