@hunter-harness/workflow-harness 0.2.84 → 0.2.86

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 (226) 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 +11 -4
  3. package/harness/bundles/general/claude-code/harness-codebase-map/SKILL.md +1 -1
  4. package/harness/bundles/general/claude-code/harness-knowledge-ingest/SKILL.md +1 -1
  5. package/harness/bundles/general/claude-code/harness-knowledge-query/SKILL.md +1 -1
  6. package/harness/bundles/general/claude-code/harness-plan/SKILL.md +3 -2
  7. package/harness/bundles/general/claude-code/harness-plan/checklist.md +18 -79
  8. package/harness/bundles/general/claude-code/harness-plan/protocols.md +3 -1
  9. package/harness/bundles/general/claude-code/harness-plan/reference.md +37 -8
  10. package/harness/bundles/general/claude-code/harness-pull/SKILL.md +1 -1
  11. package/harness/bundles/general/claude-code/harness-push/SKILL.md +1 -1
  12. package/harness/bundles/general/claude-code/harness-review/SKILL.md +1 -1
  13. package/harness/bundles/general/claude-code/harness-run/SKILL.md +6 -4
  14. package/harness/bundles/general/claude-code/harness-submit/SKILL.md +1 -1
  15. package/harness/bundles/general/claude-code/harness-sync/SKILL.md +1 -1
  16. package/harness/bundles/general/claude-code/harness-test/SKILL.md +1 -1
  17. package/harness/bundles/general/claude-code/scripts/harness_archive.py +58 -25
  18. package/harness/bundles/general/claude-code/scripts/harness_change.py +25 -7
  19. package/harness/bundles/general/claude-code/scripts/harness_context.py +62 -19
  20. package/harness/bundles/general/claude-code/scripts/harness_fixback.py +4 -0
  21. package/harness/bundles/general/claude-code/scripts/harness_gate.py +208 -72
  22. package/harness/bundles/general/claude-code/scripts/harness_ledger.py +43 -0
  23. package/harness/bundles/general/claude-code/scripts/harness_paths.py +60 -0
  24. package/harness/bundles/general/claude-code/scripts/harness_phase.py +24 -6
  25. package/harness/bundles/general/claude-code/scripts/harness_plan_finalize.py +124 -1
  26. package/harness/bundles/general/claude-code/scripts/harness_review.py +25 -4
  27. package/harness/bundles/general/codebuddy/.harness-build.json +1 -1
  28. package/harness/bundles/general/codebuddy/harness-archive/SKILL.md +11 -4
  29. package/harness/bundles/general/codebuddy/harness-codebase-map/SKILL.md +1 -1
  30. package/harness/bundles/general/codebuddy/harness-knowledge-ingest/SKILL.md +1 -1
  31. package/harness/bundles/general/codebuddy/harness-knowledge-query/SKILL.md +1 -1
  32. package/harness/bundles/general/codebuddy/harness-plan/SKILL.md +3 -2
  33. package/harness/bundles/general/codebuddy/harness-plan/checklist.md +18 -79
  34. package/harness/bundles/general/codebuddy/harness-plan/protocols.md +3 -1
  35. package/harness/bundles/general/codebuddy/harness-plan/reference.md +37 -8
  36. package/harness/bundles/general/codebuddy/harness-pull/SKILL.md +1 -1
  37. package/harness/bundles/general/codebuddy/harness-push/SKILL.md +1 -1
  38. package/harness/bundles/general/codebuddy/harness-review/SKILL.md +1 -1
  39. package/harness/bundles/general/codebuddy/harness-run/SKILL.md +6 -4
  40. package/harness/bundles/general/codebuddy/harness-submit/SKILL.md +1 -1
  41. package/harness/bundles/general/codebuddy/harness-sync/SKILL.md +1 -1
  42. package/harness/bundles/general/codebuddy/harness-test/SKILL.md +1 -1
  43. package/harness/bundles/general/codebuddy/scripts/harness_archive.py +58 -25
  44. package/harness/bundles/general/codebuddy/scripts/harness_change.py +25 -7
  45. package/harness/bundles/general/codebuddy/scripts/harness_context.py +62 -19
  46. package/harness/bundles/general/codebuddy/scripts/harness_fixback.py +4 -0
  47. package/harness/bundles/general/codebuddy/scripts/harness_gate.py +208 -72
  48. package/harness/bundles/general/codebuddy/scripts/harness_ledger.py +43 -0
  49. package/harness/bundles/general/codebuddy/scripts/harness_paths.py +60 -0
  50. package/harness/bundles/general/codebuddy/scripts/harness_phase.py +24 -6
  51. package/harness/bundles/general/codebuddy/scripts/harness_plan_finalize.py +124 -1
  52. package/harness/bundles/general/codebuddy/scripts/harness_review.py +25 -4
  53. package/harness/bundles/general/codex/.harness-build.json +1 -1
  54. package/harness/bundles/general/codex/harness-archive/SKILL.md +11 -4
  55. package/harness/bundles/general/codex/harness-codebase-map/SKILL.md +1 -1
  56. package/harness/bundles/general/codex/harness-knowledge-ingest/SKILL.md +1 -1
  57. package/harness/bundles/general/codex/harness-knowledge-query/SKILL.md +1 -1
  58. package/harness/bundles/general/codex/harness-plan/SKILL.md +3 -2
  59. package/harness/bundles/general/codex/harness-plan/checklist.md +18 -79
  60. package/harness/bundles/general/codex/harness-plan/protocols.md +3 -1
  61. package/harness/bundles/general/codex/harness-plan/reference.md +37 -8
  62. package/harness/bundles/general/codex/harness-pull/SKILL.md +1 -1
  63. package/harness/bundles/general/codex/harness-push/SKILL.md +1 -1
  64. package/harness/bundles/general/codex/harness-review/SKILL.md +1 -1
  65. package/harness/bundles/general/codex/harness-run/SKILL.md +6 -4
  66. package/harness/bundles/general/codex/harness-submit/SKILL.md +1 -1
  67. package/harness/bundles/general/codex/harness-sync/SKILL.md +1 -1
  68. package/harness/bundles/general/codex/harness-test/SKILL.md +1 -1
  69. package/harness/bundles/general/codex/scripts/harness_archive.py +58 -25
  70. package/harness/bundles/general/codex/scripts/harness_change.py +25 -7
  71. package/harness/bundles/general/codex/scripts/harness_context.py +62 -19
  72. package/harness/bundles/general/codex/scripts/harness_fixback.py +4 -0
  73. package/harness/bundles/general/codex/scripts/harness_gate.py +208 -72
  74. package/harness/bundles/general/codex/scripts/harness_ledger.py +43 -0
  75. package/harness/bundles/general/codex/scripts/harness_paths.py +60 -0
  76. package/harness/bundles/general/codex/scripts/harness_phase.py +24 -6
  77. package/harness/bundles/general/codex/scripts/harness_plan_finalize.py +124 -1
  78. package/harness/bundles/general/codex/scripts/harness_review.py +25 -4
  79. package/harness/bundles/general/cursor/.harness-build.json +1 -1
  80. package/harness/bundles/general/cursor/harness-archive/SKILL.md +11 -4
  81. package/harness/bundles/general/cursor/harness-codebase-map/SKILL.md +1 -1
  82. package/harness/bundles/general/cursor/harness-knowledge-ingest/SKILL.md +1 -1
  83. package/harness/bundles/general/cursor/harness-knowledge-query/SKILL.md +1 -1
  84. package/harness/bundles/general/cursor/harness-plan/SKILL.md +3 -2
  85. package/harness/bundles/general/cursor/harness-plan/checklist.md +18 -79
  86. package/harness/bundles/general/cursor/harness-plan/protocols.md +3 -1
  87. package/harness/bundles/general/cursor/harness-plan/reference.md +37 -8
  88. package/harness/bundles/general/cursor/harness-pull/SKILL.md +1 -1
  89. package/harness/bundles/general/cursor/harness-push/SKILL.md +1 -1
  90. package/harness/bundles/general/cursor/harness-review/SKILL.md +1 -1
  91. package/harness/bundles/general/cursor/harness-run/SKILL.md +6 -4
  92. package/harness/bundles/general/cursor/harness-submit/SKILL.md +1 -1
  93. package/harness/bundles/general/cursor/harness-sync/SKILL.md +1 -1
  94. package/harness/bundles/general/cursor/harness-test/SKILL.md +1 -1
  95. package/harness/bundles/general/cursor/scripts/harness_archive.py +58 -25
  96. package/harness/bundles/general/cursor/scripts/harness_change.py +25 -7
  97. package/harness/bundles/general/cursor/scripts/harness_context.py +62 -19
  98. package/harness/bundles/general/cursor/scripts/harness_fixback.py +4 -0
  99. package/harness/bundles/general/cursor/scripts/harness_gate.py +208 -72
  100. package/harness/bundles/general/cursor/scripts/harness_ledger.py +43 -0
  101. package/harness/bundles/general/cursor/scripts/harness_paths.py +60 -0
  102. package/harness/bundles/general/cursor/scripts/harness_phase.py +24 -6
  103. package/harness/bundles/general/cursor/scripts/harness_plan_finalize.py +124 -1
  104. package/harness/bundles/general/cursor/scripts/harness_review.py +25 -4
  105. package/harness/bundles/java/claude-code/.harness-build.json +1 -1
  106. package/harness/bundles/java/claude-code/harness-apidoc/SKILL.md +1 -1
  107. package/harness/bundles/java/claude-code/harness-archive/SKILL.md +11 -4
  108. package/harness/bundles/java/claude-code/harness-codebase-map/SKILL.md +1 -1
  109. package/harness/bundles/java/claude-code/harness-knowledge-ingest/SKILL.md +1 -1
  110. package/harness/bundles/java/claude-code/harness-knowledge-query/SKILL.md +1 -1
  111. package/harness/bundles/java/claude-code/harness-package/SKILL.md +1 -1
  112. package/harness/bundles/java/claude-code/harness-plan/SKILL.md +3 -2
  113. package/harness/bundles/java/claude-code/harness-plan/checklist.md +18 -79
  114. package/harness/bundles/java/claude-code/harness-plan/protocols.md +3 -1
  115. package/harness/bundles/java/claude-code/harness-plan/reference.md +37 -8
  116. package/harness/bundles/java/claude-code/harness-pull/SKILL.md +1 -1
  117. package/harness/bundles/java/claude-code/harness-push/SKILL.md +1 -1
  118. package/harness/bundles/java/claude-code/harness-review/SKILL.md +1 -1
  119. package/harness/bundles/java/claude-code/harness-run/SKILL.md +6 -4
  120. package/harness/bundles/java/claude-code/harness-submit/SKILL.md +1 -1
  121. package/harness/bundles/java/claude-code/harness-sync/SKILL.md +1 -1
  122. package/harness/bundles/java/claude-code/harness-test/SKILL.md +1 -1
  123. package/harness/bundles/java/claude-code/scripts/harness_archive.py +58 -25
  124. package/harness/bundles/java/claude-code/scripts/harness_change.py +25 -7
  125. package/harness/bundles/java/claude-code/scripts/harness_context.py +62 -19
  126. package/harness/bundles/java/claude-code/scripts/harness_fixback.py +4 -0
  127. package/harness/bundles/java/claude-code/scripts/harness_gate.py +208 -72
  128. package/harness/bundles/java/claude-code/scripts/harness_ledger.py +43 -0
  129. package/harness/bundles/java/claude-code/scripts/harness_paths.py +60 -0
  130. package/harness/bundles/java/claude-code/scripts/harness_phase.py +24 -6
  131. package/harness/bundles/java/claude-code/scripts/harness_plan_finalize.py +124 -1
  132. package/harness/bundles/java/claude-code/scripts/harness_review.py +25 -4
  133. package/harness/bundles/java/codebuddy/.harness-build.json +1 -1
  134. package/harness/bundles/java/codebuddy/harness-apidoc/SKILL.md +1 -1
  135. package/harness/bundles/java/codebuddy/harness-archive/SKILL.md +11 -4
  136. package/harness/bundles/java/codebuddy/harness-codebase-map/SKILL.md +1 -1
  137. package/harness/bundles/java/codebuddy/harness-knowledge-ingest/SKILL.md +1 -1
  138. package/harness/bundles/java/codebuddy/harness-knowledge-query/SKILL.md +1 -1
  139. package/harness/bundles/java/codebuddy/harness-package/SKILL.md +1 -1
  140. package/harness/bundles/java/codebuddy/harness-plan/SKILL.md +3 -2
  141. package/harness/bundles/java/codebuddy/harness-plan/checklist.md +18 -79
  142. package/harness/bundles/java/codebuddy/harness-plan/protocols.md +3 -1
  143. package/harness/bundles/java/codebuddy/harness-plan/reference.md +37 -8
  144. package/harness/bundles/java/codebuddy/harness-pull/SKILL.md +1 -1
  145. package/harness/bundles/java/codebuddy/harness-push/SKILL.md +1 -1
  146. package/harness/bundles/java/codebuddy/harness-review/SKILL.md +1 -1
  147. package/harness/bundles/java/codebuddy/harness-run/SKILL.md +6 -4
  148. package/harness/bundles/java/codebuddy/harness-submit/SKILL.md +1 -1
  149. package/harness/bundles/java/codebuddy/harness-sync/SKILL.md +1 -1
  150. package/harness/bundles/java/codebuddy/harness-test/SKILL.md +1 -1
  151. package/harness/bundles/java/codebuddy/scripts/harness_archive.py +58 -25
  152. package/harness/bundles/java/codebuddy/scripts/harness_change.py +25 -7
  153. package/harness/bundles/java/codebuddy/scripts/harness_context.py +62 -19
  154. package/harness/bundles/java/codebuddy/scripts/harness_fixback.py +4 -0
  155. package/harness/bundles/java/codebuddy/scripts/harness_gate.py +208 -72
  156. package/harness/bundles/java/codebuddy/scripts/harness_ledger.py +43 -0
  157. package/harness/bundles/java/codebuddy/scripts/harness_paths.py +60 -0
  158. package/harness/bundles/java/codebuddy/scripts/harness_phase.py +24 -6
  159. package/harness/bundles/java/codebuddy/scripts/harness_plan_finalize.py +124 -1
  160. package/harness/bundles/java/codebuddy/scripts/harness_review.py +25 -4
  161. package/harness/bundles/java/codex/.harness-build.json +1 -1
  162. package/harness/bundles/java/codex/harness-apidoc/SKILL.md +1 -1
  163. package/harness/bundles/java/codex/harness-archive/SKILL.md +11 -4
  164. package/harness/bundles/java/codex/harness-codebase-map/SKILL.md +1 -1
  165. package/harness/bundles/java/codex/harness-knowledge-ingest/SKILL.md +1 -1
  166. package/harness/bundles/java/codex/harness-knowledge-query/SKILL.md +1 -1
  167. package/harness/bundles/java/codex/harness-package/SKILL.md +1 -1
  168. package/harness/bundles/java/codex/harness-plan/SKILL.md +3 -2
  169. package/harness/bundles/java/codex/harness-plan/checklist.md +18 -79
  170. package/harness/bundles/java/codex/harness-plan/protocols.md +3 -1
  171. package/harness/bundles/java/codex/harness-plan/reference.md +37 -8
  172. package/harness/bundles/java/codex/harness-pull/SKILL.md +1 -1
  173. package/harness/bundles/java/codex/harness-push/SKILL.md +1 -1
  174. package/harness/bundles/java/codex/harness-review/SKILL.md +1 -1
  175. package/harness/bundles/java/codex/harness-run/SKILL.md +6 -4
  176. package/harness/bundles/java/codex/harness-submit/SKILL.md +1 -1
  177. package/harness/bundles/java/codex/harness-sync/SKILL.md +1 -1
  178. package/harness/bundles/java/codex/harness-test/SKILL.md +1 -1
  179. package/harness/bundles/java/codex/scripts/harness_archive.py +58 -25
  180. package/harness/bundles/java/codex/scripts/harness_change.py +25 -7
  181. package/harness/bundles/java/codex/scripts/harness_context.py +62 -19
  182. package/harness/bundles/java/codex/scripts/harness_fixback.py +4 -0
  183. package/harness/bundles/java/codex/scripts/harness_gate.py +208 -72
  184. package/harness/bundles/java/codex/scripts/harness_ledger.py +43 -0
  185. package/harness/bundles/java/codex/scripts/harness_paths.py +60 -0
  186. package/harness/bundles/java/codex/scripts/harness_phase.py +24 -6
  187. package/harness/bundles/java/codex/scripts/harness_plan_finalize.py +124 -1
  188. package/harness/bundles/java/codex/scripts/harness_review.py +25 -4
  189. package/harness/bundles/java/cursor/.harness-build.json +1 -1
  190. package/harness/bundles/java/cursor/harness-apidoc/SKILL.md +1 -1
  191. package/harness/bundles/java/cursor/harness-archive/SKILL.md +11 -4
  192. package/harness/bundles/java/cursor/harness-codebase-map/SKILL.md +1 -1
  193. package/harness/bundles/java/cursor/harness-knowledge-ingest/SKILL.md +1 -1
  194. package/harness/bundles/java/cursor/harness-knowledge-query/SKILL.md +1 -1
  195. package/harness/bundles/java/cursor/harness-package/SKILL.md +1 -1
  196. package/harness/bundles/java/cursor/harness-plan/SKILL.md +3 -2
  197. package/harness/bundles/java/cursor/harness-plan/checklist.md +18 -79
  198. package/harness/bundles/java/cursor/harness-plan/protocols.md +3 -1
  199. package/harness/bundles/java/cursor/harness-plan/reference.md +37 -8
  200. package/harness/bundles/java/cursor/harness-pull/SKILL.md +1 -1
  201. package/harness/bundles/java/cursor/harness-push/SKILL.md +1 -1
  202. package/harness/bundles/java/cursor/harness-review/SKILL.md +1 -1
  203. package/harness/bundles/java/cursor/harness-run/SKILL.md +6 -4
  204. package/harness/bundles/java/cursor/harness-submit/SKILL.md +1 -1
  205. package/harness/bundles/java/cursor/harness-sync/SKILL.md +1 -1
  206. package/harness/bundles/java/cursor/harness-test/SKILL.md +1 -1
  207. package/harness/bundles/java/cursor/scripts/harness_archive.py +58 -25
  208. package/harness/bundles/java/cursor/scripts/harness_change.py +25 -7
  209. package/harness/bundles/java/cursor/scripts/harness_context.py +62 -19
  210. package/harness/bundles/java/cursor/scripts/harness_fixback.py +4 -0
  211. package/harness/bundles/java/cursor/scripts/harness_gate.py +208 -72
  212. package/harness/bundles/java/cursor/scripts/harness_ledger.py +43 -0
  213. package/harness/bundles/java/cursor/scripts/harness_paths.py +60 -0
  214. package/harness/bundles/java/cursor/scripts/harness_phase.py +24 -6
  215. package/harness/bundles/java/cursor/scripts/harness_plan_finalize.py +124 -1
  216. package/harness/bundles/java/cursor/scripts/harness_review.py +25 -4
  217. package/harness/manifests/general/claude-code.json +27 -27
  218. package/harness/manifests/general/codebuddy.json +27 -27
  219. package/harness/manifests/general/codex.json +27 -27
  220. package/harness/manifests/general/cursor.json +27 -27
  221. package/harness/manifests/java/claude-code.json +29 -29
  222. package/harness/manifests/java/codebuddy.json +29 -29
  223. package/harness/manifests/java/codex.json +29 -29
  224. package/harness/manifests/java/cursor.json +29 -29
  225. package/hunter-workflow-family.json +4 -4
  226. package/package.json +1 -1
@@ -3,7 +3,7 @@ name: harness-test
3
3
  description: 测试执行:读取场景表,执行单元测试+API接口测试+数据兼容验证,输出测试报告。仅当用户显式调用 /harness-test
4
4
  时使用;不得在 run 结束后自动接续执行。
5
5
  ---
6
- <!-- generated by harness_deploy.py; core=c0f4b71956cfc910; overlay=java; agent=codebuddy; do not edit -->
6
+ <!-- generated by harness_deploy.py; core=0130f72da1a50434; overlay=java; agent=codebuddy; do not edit -->
7
7
  # harness-test — 测试执行
8
8
 
9
9
  ## Purpose
@@ -2599,23 +2599,12 @@ def check_status(
2599
2599
  }
2600
2600
  )
2601
2601
 
2602
- # 发布内容预检:与服务端同源的规则,扫的是真正会进 ZIP 的那批文件。
2603
- # 服务端策略不归本地裁决,所以只预警不阻断——但必须在归档开始时就说出来,
2604
- # 而不是等跑完全流程由 upload 返回 422。
2605
- content_scan = precheck_publication_content(change_dir)
2606
- checks["publication_content_scan"] = content_scan
2607
- if content_scan.get("blocked"):
2608
- warnings.append({
2609
- "code": "PUBLICATION_CONTENT_SCAN_FLAGGED",
2610
- "message": str(content_scan.get("nextAction") or ""),
2611
- })
2612
- elif not content_scan.get("ok"):
2613
- warnings.append({
2614
- "code": str(
2615
- content_scan.get("reasonCode") or "PUBLICATION_CONTENT_SCAN_UNAVAILABLE"
2616
- ),
2617
- "message": str(content_scan.get("message") or ""),
2618
- })
2602
+ checks["publication_content_scan"] = {
2603
+ "ok": True,
2604
+ "scan_performed": False,
2605
+ "findings": [],
2606
+ "message": "上传归档链路已停用内容敏感扫描;保留路径、凭据与 ZIP 安全校验。",
2607
+ }
2619
2608
 
2620
2609
  sensitive_gate = validate_sensitive_evidence_publication_gate(
2621
2610
  change_dir,
@@ -9408,6 +9397,9 @@ def resolve_hunter_cli_command(project_root: Path) -> list[str]:
9408
9397
  """Command prefix that runs the hunter-harness CLI, cheapest route first."""
9409
9398
  node = shutil.which("node")
9410
9399
  if node:
9400
+ current_entry = os.environ.get("HUNTER_HARNESS_CLI_ENTRY", "").strip()
9401
+ if current_entry and Path(current_entry).is_file():
9402
+ return [node, str(Path(current_entry).resolve())]
9411
9403
  entry = _installed_cli_entry(project_root)
9412
9404
  if entry is not None:
9413
9405
  return [node, str(entry)]
@@ -9774,6 +9766,7 @@ def auto_push_archive_core(
9774
9766
  change_key: str | None = None,
9775
9767
  extra_entries: dict[str, tuple[str, str, bytes]] | None = None,
9776
9768
  reuse_retained_package: bool = False,
9769
+ prepared_package: dict[str, Any] | None = None,
9777
9770
  ) -> dict[str, Any]:
9778
9771
  """Build and upload one deterministic core ZIP after finalize.
9779
9772
 
@@ -9786,11 +9779,13 @@ def auto_push_archive_core(
9786
9779
  """
9787
9780
  project_root = project_root.resolve()
9788
9781
  effective_change_key = change_key or archive_dir.name
9789
- package = (
9790
- load_retained_package(project_root, effective_change_key)
9791
- if reuse_retained_package
9792
- else None
9793
- )
9782
+ package = prepared_package
9783
+ if package is None:
9784
+ package = (
9785
+ load_retained_package(project_root, effective_change_key)
9786
+ if reuse_retained_package
9787
+ else None
9788
+ )
9794
9789
  if package is None:
9795
9790
  try:
9796
9791
  package = build_archive_package(
@@ -10637,6 +10632,28 @@ def resolve_archive_dir_for_change(
10637
10632
  return archive_root / matches[-1], matches
10638
10633
 
10639
10634
 
10635
+ def resolve_latest_unpublished_archive(project_root: Path) -> tuple[str | None, list[str]]:
10636
+ """Choose the newest sealed archive without a durable local receipt."""
10637
+ archive_root = project_root / ".harness" / "archive"
10638
+ if not archive_root.is_dir():
10639
+ return None, []
10640
+ candidates: list[tuple[str, str]] = []
10641
+ for entry in archive_root.iterdir():
10642
+ if not entry.is_dir():
10643
+ continue
10644
+ change = entry.name
10645
+ try:
10646
+ summary = read_json(entry / "reports" / "final" / "summary-data.json")
10647
+ if isinstance(summary, dict) and isinstance(summary.get("changeName"), str):
10648
+ change = str(summary["changeName"]).strip() or change
10649
+ except (OSError, ValueError, json.JSONDecodeError):
10650
+ pass
10651
+ if read_durable_archive_receipt(project_root, change) is None:
10652
+ candidates.append((entry.name, change))
10653
+ candidates.sort(reverse=True)
10654
+ return (candidates[0][1] if candidates else None), [item[1] for item in candidates]
10655
+
10656
+
10640
10657
  def _republish_knowledge_entry(archive_dir: Path) -> dict[str, tuple[str, str, bytes]]:
10641
10658
  """Knowledge candidates for archives sealed before the candidates step.
10642
10659
 
@@ -10707,12 +10724,26 @@ def cmd_republish(
10707
10724
  "changeKey": change_key,
10708
10725
  "dryRun": dry_run,
10709
10726
  }
10727
+ root = resolve_republish_project_root(project_root, archive_dir)
10728
+ if change_key == "latest":
10729
+ selected, available = resolve_latest_unpublished_archive(root)
10730
+ if selected is None:
10731
+ payload.update({
10732
+ "ok": True,
10733
+ "reasonCode": "ARCHIVE_NO_UNPUBLISHED_ARCHIVE",
10734
+ "noChanges": True,
10735
+ "availableChanges": available,
10736
+ "projectRoot": str(root)
10737
+ })
10738
+ return 0, payload
10739
+ change_key = selected
10740
+ payload["changeKey"] = change_key
10741
+ payload["selectedChange"] = selected
10710
10742
  if re.fullmatch(r"[A-Za-z0-9][A-Za-z0-9._-]{0,159}", change_key) is None:
10711
10743
  payload["error"] = "change key must be a portable path segment"
10712
10744
  payload["reasonCode"] = "ARCHIVE_CHANGE_KEY_INVALID"
10713
10745
  return 2, payload
10714
10746
 
10715
- root = resolve_republish_project_root(project_root, archive_dir)
10716
10747
  payload["projectRoot"] = str(root)
10717
10748
  matches: list[str] = []
10718
10749
  if archive_dir is None:
@@ -10821,7 +10852,7 @@ def cmd_republish(
10821
10852
  # distinct name; it must never be mistaken for a pending upload.
10822
10853
  preview_path = (
10823
10854
  root / ".harness" / "state" / "local" / "archive-packages"
10824
- / f"{change_key}.preview.zip"
10855
+ / f"{change_key}{'.preview.zip' if dry_run else '.zip'}"
10825
10856
  )
10826
10857
  try:
10827
10858
  package = build_archive_package(
@@ -10837,7 +10868,8 @@ def cmd_republish(
10837
10868
  return 1, payload
10838
10869
  finally:
10839
10870
  try:
10840
- preview_path.unlink(missing_ok=True)
10871
+ if dry_run:
10872
+ preview_path.unlink(missing_ok=True)
10841
10873
  except OSError:
10842
10874
  pass
10843
10875
 
@@ -10907,6 +10939,7 @@ def cmd_republish(
10907
10939
  archive_dir,
10908
10940
  change_key=change_key,
10909
10941
  extra_entries=extra_entries,
10942
+ prepared_package=package,
10910
10943
  )
10911
10944
  payload["push"] = result
10912
10945
  payload["ok"] = bool(result.get("ok"))
@@ -564,18 +564,36 @@ def _lease_expired(lease: dict[str, Any]) -> bool:
564
564
  return dt.datetime.now().astimezone() >= exp_dt
565
565
 
566
566
 
567
- def inspect_lease(project_root: Path, change_id: str) -> dict[str, Any] | None:
568
- """Return the current non-expired lease without mutating it."""
567
+ def inspect_lease_state(project_root: Path, change_id: str) -> dict[str, Any]:
568
+ """Read the lease without mutating it, keeping the three failure modes apart.
569
+
570
+ ``inspect_lease`` collapses absent / expired / corrupt into ``None``, so a
571
+ caller cannot tell "nobody ever held this" from "the phase outlived its
572
+ TTL". Those need different answers: an expired lease still carries the
573
+ runId that proves who owns the phase, a corrupt one proves nothing, and an
574
+ absent one may just mean the phase already closed.
575
+
576
+ state is one of ``active`` | ``expired`` | ``absent`` | ``corrupt``. The
577
+ lease dict is returned for ``active`` and ``expired``; ``None`` otherwise.
578
+ """
569
579
  path = _lease_path(project_root, change_id)
570
580
  if not path.is_file():
571
- return None
581
+ return {"state": "absent", "lease": None}
572
582
  try:
573
583
  lease = _read_json(path)
574
584
  except (OSError, json.JSONDecodeError):
575
- return None
576
- if not isinstance(lease, dict) or _lease_expired(lease):
577
- return None
578
- return lease
585
+ return {"state": "corrupt", "lease": None}
586
+ if not isinstance(lease, dict):
587
+ return {"state": "corrupt", "lease": None}
588
+ if _lease_expired(lease):
589
+ return {"state": "expired", "lease": lease}
590
+ return {"state": "active", "lease": lease}
591
+
592
+
593
+ def inspect_lease(project_root: Path, change_id: str) -> dict[str, Any] | None:
594
+ """Return the current non-expired lease without mutating it."""
595
+ state = inspect_lease_state(project_root, change_id)
596
+ return state["lease"] if state["state"] == "active" else None
579
597
 
580
598
 
581
599
  def _claim_lease_locked(
@@ -21,6 +21,7 @@ if str(SCRIPTS_DIR) not in sys.path:
21
21
  sys.path.insert(0, str(SCRIPTS_DIR))
22
22
 
23
23
  import harness_ledger as hl # noqa: E402
24
+ import harness_paths as hpaths # noqa: E402
24
25
 
25
26
 
26
27
  PHASE_GRAPH = {
@@ -31,17 +32,8 @@ PHASE_GRAPH = {
31
32
  "submit": (),
32
33
  }
33
34
 
34
- WORKFLOW_PHASES = (
35
- "plan",
36
- "run",
37
- "test",
38
- "review",
39
- "package",
40
- "apidoc",
41
- "submit",
42
- "merge",
43
- "archive",
44
- )
35
+ # 唯一权威清单在 harness_paths(叶子模块,harness_phase 引的是同一个对象)。
36
+ WORKFLOW_PHASES = hpaths.WORKFLOW_PHASES
45
37
 
46
38
  DISPLAY_TITLE_MAX_LENGTH = 80
47
39
 
@@ -270,13 +262,24 @@ def _contract(project: Path, change: str) -> tuple[Path, dict[str, Any], Path]:
270
262
 
271
263
 
272
264
  def _phase_plan(contract_root: Path) -> tuple[list[str] | None, str]:
265
+ """读出阶段计划;无法采信时说明原因,而不是悄悄退成 legacy。
266
+
267
+ 以前这里遇到未知阶段名一律返回 (None, "legacy"):阶段计划整个失效,
268
+ `_allowed_next_phases` 退回硬编码的 PHASE_GRAPH,而调用方看不出发生过这件事。
269
+ 未知阶段名不是假想问题——阶段清单少一个 merge 时,worktree 变更的
270
+ plannedPhases 就会带着 merge 走到这里。
271
+
272
+ 未知名先过 LEGACY_PHASE_ALIASES 读时映射(阶段改名后历史 change 靠它继续可读),
273
+ 仍然解析不了才降级,并把原因与具体阶段名带在 source 里。
274
+ """
273
275
  policy_path = contract_root / "meta" / "gate-policy.json"
274
276
  if not policy_path.is_file():
275
277
  return None, "legacy"
276
278
  try:
277
279
  policy = _read_json(policy_path)
278
280
  except (OSError, ValueError, json.JSONDecodeError):
279
- return None, "legacy"
281
+ return None, "legacy:policy-unreadable"
282
+ unknown: list[str] = []
280
283
  for field, source in (
281
284
  ("plannedPhases", "change"),
282
285
  ("defaultPhases", "policy-default"),
@@ -285,12 +288,17 @@ def _phase_plan(contract_root: Path) -> tuple[list[str] | None, str]:
285
288
  if not isinstance(raw, list):
286
289
  continue
287
290
  phases = [str(item).strip() for item in raw if str(item).strip()]
288
- if (
289
- phases
290
- and len(phases) == len(set(phases))
291
- and all(item in WORKFLOW_PHASES for item in phases)
292
- ):
293
- return phases, source
291
+ if not phases or len(phases) != len(set(phases)):
292
+ continue
293
+ resolved = [hpaths.resolve_phase_name(item) for item in phases]
294
+ missing = [item for item, hit in zip(phases, resolved) if hit is None]
295
+ if missing:
296
+ unknown.extend(missing)
297
+ continue
298
+ # 映射后可能出现重复(两个旧名合并成同一个新阶段),保序去重。
299
+ return list(dict.fromkeys(str(item) for item in resolved)), source
300
+ if unknown:
301
+ return None, "legacy:unknown-phases=" + ",".join(dict.fromkeys(unknown))
294
302
  return None, "legacy"
295
303
 
296
304
 
@@ -405,6 +413,7 @@ def _paths(state_root: Path) -> dict[str, Path]:
405
413
  "begins": runtime / "transition-begins.ndjson",
406
414
  "current": runtime / "current-context.json",
407
415
  "lease": runtime / "context-lease.json",
416
+ "adoptions": runtime / "context-adoptions.ndjson",
408
417
  }
409
418
 
410
419
 
@@ -604,8 +613,14 @@ def _claim_prepared_context(
604
613
  )
605
614
  except (OSError, ValueError, json.JSONDecodeError):
606
615
  return {"ok": False, "code": "CONTEXT_CURRENT_INVALID"}
616
+ # 跨阶段推进必须有 receipt 证明它合法;同一个阶段换执行者不需要。
617
+ # 以前两者都要 receipt,于是上一阶段崩溃没能正常 close 时,接手的工具
618
+ # 连"继续把这个阶段干完"都做不到。并发保护不受影响:租约未过期而
619
+ # owner 不同,上面 CONTEXT_LEASE_HELD 已经拦下了,能走到这里只剩
620
+ # 「租约已过期」和「租约已被上一阶段 close 删除」两种情况。
621
+ adoption: dict[str, Any] | None = None
607
622
  if recovery is None and isinstance(current, dict) and (
608
- current.get("executor") != executor or current.get("phase") != phase
623
+ current.get("phase") != phase
609
624
  ):
610
625
  valid_receipt = (
611
626
  isinstance(latest, dict)
@@ -621,6 +636,19 @@ def _claim_prepared_context(
621
636
  "toExecutor": executor,
622
637
  "toPhase": phase,
623
638
  }
639
+ executor_changed = (
640
+ isinstance(current, dict) and current.get("executor") != executor
641
+ )
642
+ if recovery is not None or executor_changed:
643
+ adoption = {
644
+ "previousExecutor": (
645
+ current.get("executor") if isinstance(current, dict) else None
646
+ ),
647
+ "newExecutor": executor,
648
+ "reason": (
649
+ "lease_expired" if recovery is not None else "same_phase_handoff"
650
+ ),
651
+ }
624
652
 
625
653
  try:
626
654
  execution_root = _execution_root(project, contract_root, state_root)
@@ -657,6 +685,20 @@ def _claim_prepared_context(
657
685
  current_context["preparationId"] = preparation_id
658
686
  _write_json_atomic(paths["lease"], lease)
659
687
  _write_json_atomic(paths["current"], current_context)
688
+ if adoption is not None:
689
+ # 接管必须留痕。过期租约本来就允许任何执行者顶替,但以前只在返回体
690
+ # 里挂一个 recovery,退出进程就没了——事后查"这个阶段是谁接手的"
691
+ # 无据可依。收据形状照 harness_archive.adopt_existing_range。
692
+ adoption = {
693
+ "schemaVersion": 1,
694
+ "changeName": change,
695
+ "phase": phase,
696
+ **adoption,
697
+ "previousOwner": recovery.get("previousOwner") if recovery else None,
698
+ "expiredAt": recovery.get("expiredAt") if recovery else None,
699
+ "adoptedAt": now.isoformat(),
700
+ }
701
+ _append_ndjson(paths["adoptions"], adoption)
660
702
  return {
661
703
  "ok": True,
662
704
  "lease": lease,
@@ -664,6 +706,7 @@ def _claim_prepared_context(
664
706
  "transitions": transitions,
665
707
  "latestTransition": latest,
666
708
  "recovery": recovery,
709
+ "adoption": adoption,
667
710
  "executionRoot": str(execution_root),
668
711
  }
669
712
 
@@ -405,6 +405,10 @@ def _default_gate_begin(**kwargs: Any) -> dict[str, Any]:
405
405
  str(kwargs["run_id"]),
406
406
  "--note",
407
407
  "开始处理评审中确认需要修改的代码问题。",
408
+ # 显式标记这一轮是 review fixback。以前靠 gate 嗅探 note 里有没有
409
+ # "fixback" 字样——而上面这条 note 一个都没有,于是 fixback 的 run 事件
410
+ # 从来没被打上 trigger/from_phase。
411
+ "--fixback",
408
412
  ]
409
413
  for option, key in (
410
414
  ("--executor-tool", "executor_tool"),