@hunter-harness/workflow-harness 0.2.11 → 0.2.12

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/contracts/ci-metrics.schema.json +36 -0
  3. package/harness/bundles/general/claude-code/contracts/workflow-policy.json +30 -0
  4. package/harness/bundles/general/claude-code/harness-archive/SKILL.md +1 -1
  5. package/harness/bundles/general/claude-code/harness-codebase-map/SKILL.md +1 -1
  6. package/harness/bundles/general/claude-code/harness-knowledge-ingest/SKILL.md +1 -1
  7. package/harness/bundles/general/claude-code/harness-knowledge-query/SKILL.md +1 -1
  8. package/harness/bundles/general/claude-code/harness-plan/SKILL.md +4 -4
  9. package/harness/bundles/general/claude-code/harness-plan/checklist.md +6 -0
  10. package/harness/bundles/general/claude-code/harness-plan/reference.md +7 -2
  11. package/harness/bundles/general/claude-code/harness-review/SKILL.md +1 -1
  12. package/harness/bundles/general/claude-code/harness-run/SKILL.md +1 -1
  13. package/harness/bundles/general/claude-code/harness-run/checklist.md +3 -2
  14. package/harness/bundles/general/claude-code/harness-run/reference.md +6 -4
  15. package/harness/bundles/general/claude-code/harness-submit/SKILL.md +1 -1
  16. package/harness/bundles/general/claude-code/harness-sync/SKILL.md +1 -1
  17. package/harness/bundles/general/claude-code/harness-test/SKILL.md +1 -1
  18. package/harness/bundles/general/claude-code/scripts/harness_archive.py +282 -120
  19. package/harness/bundles/general/claude-code/scripts/harness_events.py +400 -24
  20. package/harness/bundles/general/claude-code/scripts/harness_gate.py +585 -21
  21. package/harness/bundles/general/claude-code/scripts/harness_integration.py +403 -18
  22. package/harness/bundles/general/claude-code/scripts/harness_ledger.py +225 -29
  23. package/harness/bundles/general/claude-code/scripts/harness_phase.py +926 -0
  24. package/harness/bundles/general/claude-code/scripts/harness_plan_finalize.py +361 -0
  25. package/harness/bundles/general/claude-code/scripts/harness_profile.py +50 -0
  26. package/harness/bundles/general/claude-code/scripts/harness_runtime.py +248 -0
  27. package/harness/bundles/general/claude-code/scripts/harness_state.py +15 -2
  28. package/harness/bundles/general/claude-code/scripts/harness_test_guard.py +188 -0
  29. package/harness/bundles/general/claude-code/scripts/harness_workflow_policy.py +19 -0
  30. package/harness/bundles/general/codebuddy/.harness-build.json +1 -1
  31. package/harness/bundles/general/codebuddy/contracts/ci-metrics.schema.json +36 -0
  32. package/harness/bundles/general/codebuddy/contracts/workflow-policy.json +30 -0
  33. package/harness/bundles/general/codebuddy/harness-archive/SKILL.md +1 -1
  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 +4 -4
  38. package/harness/bundles/general/codebuddy/harness-plan/checklist.md +6 -0
  39. package/harness/bundles/general/codebuddy/harness-plan/reference.md +7 -2
  40. package/harness/bundles/general/codebuddy/harness-review/SKILL.md +1 -1
  41. package/harness/bundles/general/codebuddy/harness-run/SKILL.md +1 -1
  42. package/harness/bundles/general/codebuddy/harness-run/checklist.md +3 -2
  43. package/harness/bundles/general/codebuddy/harness-run/reference.md +6 -4
  44. package/harness/bundles/general/codebuddy/harness-submit/SKILL.md +1 -1
  45. package/harness/bundles/general/codebuddy/harness-sync/SKILL.md +1 -1
  46. package/harness/bundles/general/codebuddy/harness-test/SKILL.md +1 -1
  47. package/harness/bundles/general/codebuddy/scripts/harness_archive.py +282 -120
  48. package/harness/bundles/general/codebuddy/scripts/harness_events.py +400 -24
  49. package/harness/bundles/general/codebuddy/scripts/harness_gate.py +585 -21
  50. package/harness/bundles/general/codebuddy/scripts/harness_integration.py +403 -18
  51. package/harness/bundles/general/codebuddy/scripts/harness_ledger.py +225 -29
  52. package/harness/bundles/general/codebuddy/scripts/harness_phase.py +926 -0
  53. package/harness/bundles/general/codebuddy/scripts/harness_plan_finalize.py +361 -0
  54. package/harness/bundles/general/codebuddy/scripts/harness_profile.py +50 -0
  55. package/harness/bundles/general/codebuddy/scripts/harness_runtime.py +248 -0
  56. package/harness/bundles/general/codebuddy/scripts/harness_state.py +15 -2
  57. package/harness/bundles/general/codebuddy/scripts/harness_test_guard.py +188 -0
  58. package/harness/bundles/general/codebuddy/scripts/harness_workflow_policy.py +19 -0
  59. package/harness/bundles/general/codex/.harness-build.json +1 -1
  60. package/harness/bundles/general/codex/contracts/ci-metrics.schema.json +36 -0
  61. package/harness/bundles/general/codex/contracts/workflow-policy.json +30 -0
  62. package/harness/bundles/general/codex/harness-archive/SKILL.md +1 -1
  63. package/harness/bundles/general/codex/harness-codebase-map/SKILL.md +1 -1
  64. package/harness/bundles/general/codex/harness-knowledge-ingest/SKILL.md +1 -1
  65. package/harness/bundles/general/codex/harness-knowledge-query/SKILL.md +1 -1
  66. package/harness/bundles/general/codex/harness-plan/SKILL.md +4 -4
  67. package/harness/bundles/general/codex/harness-plan/checklist.md +6 -0
  68. package/harness/bundles/general/codex/harness-plan/reference.md +7 -2
  69. package/harness/bundles/general/codex/harness-review/SKILL.md +1 -1
  70. package/harness/bundles/general/codex/harness-run/SKILL.md +1 -1
  71. package/harness/bundles/general/codex/harness-run/checklist.md +3 -2
  72. package/harness/bundles/general/codex/harness-run/reference.md +6 -4
  73. package/harness/bundles/general/codex/harness-submit/SKILL.md +1 -1
  74. package/harness/bundles/general/codex/harness-sync/SKILL.md +1 -1
  75. package/harness/bundles/general/codex/harness-test/SKILL.md +1 -1
  76. package/harness/bundles/general/codex/scripts/harness_archive.py +282 -120
  77. package/harness/bundles/general/codex/scripts/harness_events.py +400 -24
  78. package/harness/bundles/general/codex/scripts/harness_gate.py +585 -21
  79. package/harness/bundles/general/codex/scripts/harness_integration.py +403 -18
  80. package/harness/bundles/general/codex/scripts/harness_ledger.py +225 -29
  81. package/harness/bundles/general/codex/scripts/harness_phase.py +926 -0
  82. package/harness/bundles/general/codex/scripts/harness_plan_finalize.py +361 -0
  83. package/harness/bundles/general/codex/scripts/harness_profile.py +50 -0
  84. package/harness/bundles/general/codex/scripts/harness_runtime.py +248 -0
  85. package/harness/bundles/general/codex/scripts/harness_state.py +15 -2
  86. package/harness/bundles/general/codex/scripts/harness_test_guard.py +188 -0
  87. package/harness/bundles/general/codex/scripts/harness_workflow_policy.py +19 -0
  88. package/harness/bundles/general/cursor/.harness-build.json +1 -1
  89. package/harness/bundles/general/cursor/contracts/ci-metrics.schema.json +36 -0
  90. package/harness/bundles/general/cursor/contracts/workflow-policy.json +30 -0
  91. package/harness/bundles/general/cursor/harness-archive/SKILL.md +1 -1
  92. package/harness/bundles/general/cursor/harness-codebase-map/SKILL.md +1 -1
  93. package/harness/bundles/general/cursor/harness-knowledge-ingest/SKILL.md +1 -1
  94. package/harness/bundles/general/cursor/harness-knowledge-query/SKILL.md +1 -1
  95. package/harness/bundles/general/cursor/harness-plan/SKILL.md +4 -4
  96. package/harness/bundles/general/cursor/harness-plan/checklist.md +6 -0
  97. package/harness/bundles/general/cursor/harness-plan/reference.md +7 -2
  98. package/harness/bundles/general/cursor/harness-review/SKILL.md +1 -1
  99. package/harness/bundles/general/cursor/harness-run/SKILL.md +1 -1
  100. package/harness/bundles/general/cursor/harness-run/checklist.md +3 -2
  101. package/harness/bundles/general/cursor/harness-run/reference.md +6 -4
  102. package/harness/bundles/general/cursor/harness-submit/SKILL.md +1 -1
  103. package/harness/bundles/general/cursor/harness-sync/SKILL.md +1 -1
  104. package/harness/bundles/general/cursor/harness-test/SKILL.md +1 -1
  105. package/harness/bundles/general/cursor/scripts/harness_archive.py +282 -120
  106. package/harness/bundles/general/cursor/scripts/harness_events.py +400 -24
  107. package/harness/bundles/general/cursor/scripts/harness_gate.py +585 -21
  108. package/harness/bundles/general/cursor/scripts/harness_integration.py +403 -18
  109. package/harness/bundles/general/cursor/scripts/harness_ledger.py +225 -29
  110. package/harness/bundles/general/cursor/scripts/harness_phase.py +926 -0
  111. package/harness/bundles/general/cursor/scripts/harness_plan_finalize.py +361 -0
  112. package/harness/bundles/general/cursor/scripts/harness_profile.py +50 -0
  113. package/harness/bundles/general/cursor/scripts/harness_runtime.py +248 -0
  114. package/harness/bundles/general/cursor/scripts/harness_state.py +15 -2
  115. package/harness/bundles/general/cursor/scripts/harness_test_guard.py +188 -0
  116. package/harness/bundles/general/cursor/scripts/harness_workflow_policy.py +19 -0
  117. package/harness/bundles/java/claude-code/.harness-build.json +1 -1
  118. package/harness/bundles/java/claude-code/contracts/ci-metrics.schema.json +36 -0
  119. package/harness/bundles/java/claude-code/contracts/workflow-policy.json +30 -0
  120. package/harness/bundles/java/claude-code/harness-apidoc/SKILL.md +1 -1
  121. package/harness/bundles/java/claude-code/harness-archive/SKILL.md +1 -1
  122. package/harness/bundles/java/claude-code/harness-codebase-map/SKILL.md +1 -1
  123. package/harness/bundles/java/claude-code/harness-knowledge-ingest/SKILL.md +1 -1
  124. package/harness/bundles/java/claude-code/harness-knowledge-query/SKILL.md +1 -1
  125. package/harness/bundles/java/claude-code/harness-package/SKILL.md +1 -1
  126. package/harness/bundles/java/claude-code/harness-plan/SKILL.md +4 -4
  127. package/harness/bundles/java/claude-code/harness-plan/checklist.md +6 -0
  128. package/harness/bundles/java/claude-code/harness-plan/reference.md +7 -2
  129. package/harness/bundles/java/claude-code/harness-review/SKILL.md +1 -1
  130. package/harness/bundles/java/claude-code/harness-run/SKILL.md +1 -1
  131. package/harness/bundles/java/claude-code/harness-submit/SKILL.md +1 -1
  132. package/harness/bundles/java/claude-code/harness-sync/SKILL.md +1 -1
  133. package/harness/bundles/java/claude-code/harness-test/SKILL.md +1 -1
  134. package/harness/bundles/java/claude-code/scripts/harness_archive.py +282 -120
  135. package/harness/bundles/java/claude-code/scripts/harness_events.py +400 -24
  136. package/harness/bundles/java/claude-code/scripts/harness_gate.py +585 -21
  137. package/harness/bundles/java/claude-code/scripts/harness_integration.py +403 -18
  138. package/harness/bundles/java/claude-code/scripts/harness_ledger.py +225 -29
  139. package/harness/bundles/java/claude-code/scripts/harness_phase.py +926 -0
  140. package/harness/bundles/java/claude-code/scripts/harness_plan_finalize.py +361 -0
  141. package/harness/bundles/java/claude-code/scripts/harness_profile.py +50 -0
  142. package/harness/bundles/java/claude-code/scripts/harness_runtime.py +248 -0
  143. package/harness/bundles/java/claude-code/scripts/harness_state.py +15 -2
  144. package/harness/bundles/java/claude-code/scripts/harness_test_guard.py +188 -0
  145. package/harness/bundles/java/claude-code/scripts/harness_workflow_policy.py +19 -0
  146. package/harness/bundles/java/codebuddy/.harness-build.json +1 -1
  147. package/harness/bundles/java/codebuddy/contracts/ci-metrics.schema.json +36 -0
  148. package/harness/bundles/java/codebuddy/contracts/workflow-policy.json +30 -0
  149. package/harness/bundles/java/codebuddy/harness-apidoc/SKILL.md +1 -1
  150. package/harness/bundles/java/codebuddy/harness-archive/SKILL.md +1 -1
  151. package/harness/bundles/java/codebuddy/harness-codebase-map/SKILL.md +1 -1
  152. package/harness/bundles/java/codebuddy/harness-knowledge-ingest/SKILL.md +1 -1
  153. package/harness/bundles/java/codebuddy/harness-knowledge-query/SKILL.md +1 -1
  154. package/harness/bundles/java/codebuddy/harness-package/SKILL.md +1 -1
  155. package/harness/bundles/java/codebuddy/harness-plan/SKILL.md +4 -4
  156. package/harness/bundles/java/codebuddy/harness-plan/checklist.md +6 -0
  157. package/harness/bundles/java/codebuddy/harness-plan/reference.md +7 -2
  158. package/harness/bundles/java/codebuddy/harness-review/SKILL.md +1 -1
  159. package/harness/bundles/java/codebuddy/harness-run/SKILL.md +1 -1
  160. package/harness/bundles/java/codebuddy/harness-submit/SKILL.md +1 -1
  161. package/harness/bundles/java/codebuddy/harness-sync/SKILL.md +1 -1
  162. package/harness/bundles/java/codebuddy/harness-test/SKILL.md +1 -1
  163. package/harness/bundles/java/codebuddy/scripts/harness_archive.py +282 -120
  164. package/harness/bundles/java/codebuddy/scripts/harness_events.py +400 -24
  165. package/harness/bundles/java/codebuddy/scripts/harness_gate.py +585 -21
  166. package/harness/bundles/java/codebuddy/scripts/harness_integration.py +403 -18
  167. package/harness/bundles/java/codebuddy/scripts/harness_ledger.py +225 -29
  168. package/harness/bundles/java/codebuddy/scripts/harness_phase.py +926 -0
  169. package/harness/bundles/java/codebuddy/scripts/harness_plan_finalize.py +361 -0
  170. package/harness/bundles/java/codebuddy/scripts/harness_profile.py +50 -0
  171. package/harness/bundles/java/codebuddy/scripts/harness_runtime.py +248 -0
  172. package/harness/bundles/java/codebuddy/scripts/harness_state.py +15 -2
  173. package/harness/bundles/java/codebuddy/scripts/harness_test_guard.py +188 -0
  174. package/harness/bundles/java/codebuddy/scripts/harness_workflow_policy.py +19 -0
  175. package/harness/bundles/java/codex/.harness-build.json +1 -1
  176. package/harness/bundles/java/codex/contracts/ci-metrics.schema.json +36 -0
  177. package/harness/bundles/java/codex/contracts/workflow-policy.json +30 -0
  178. package/harness/bundles/java/codex/harness-apidoc/SKILL.md +1 -1
  179. package/harness/bundles/java/codex/harness-archive/SKILL.md +1 -1
  180. package/harness/bundles/java/codex/harness-codebase-map/SKILL.md +1 -1
  181. package/harness/bundles/java/codex/harness-knowledge-ingest/SKILL.md +1 -1
  182. package/harness/bundles/java/codex/harness-knowledge-query/SKILL.md +1 -1
  183. package/harness/bundles/java/codex/harness-package/SKILL.md +1 -1
  184. package/harness/bundles/java/codex/harness-plan/SKILL.md +4 -4
  185. package/harness/bundles/java/codex/harness-plan/checklist.md +6 -0
  186. package/harness/bundles/java/codex/harness-plan/reference.md +7 -2
  187. package/harness/bundles/java/codex/harness-review/SKILL.md +1 -1
  188. package/harness/bundles/java/codex/harness-run/SKILL.md +1 -1
  189. package/harness/bundles/java/codex/harness-submit/SKILL.md +1 -1
  190. package/harness/bundles/java/codex/harness-sync/SKILL.md +1 -1
  191. package/harness/bundles/java/codex/harness-test/SKILL.md +1 -1
  192. package/harness/bundles/java/codex/scripts/harness_archive.py +282 -120
  193. package/harness/bundles/java/codex/scripts/harness_events.py +400 -24
  194. package/harness/bundles/java/codex/scripts/harness_gate.py +585 -21
  195. package/harness/bundles/java/codex/scripts/harness_integration.py +403 -18
  196. package/harness/bundles/java/codex/scripts/harness_ledger.py +225 -29
  197. package/harness/bundles/java/codex/scripts/harness_phase.py +926 -0
  198. package/harness/bundles/java/codex/scripts/harness_plan_finalize.py +361 -0
  199. package/harness/bundles/java/codex/scripts/harness_profile.py +50 -0
  200. package/harness/bundles/java/codex/scripts/harness_runtime.py +248 -0
  201. package/harness/bundles/java/codex/scripts/harness_state.py +15 -2
  202. package/harness/bundles/java/codex/scripts/harness_test_guard.py +188 -0
  203. package/harness/bundles/java/codex/scripts/harness_workflow_policy.py +19 -0
  204. package/harness/bundles/java/cursor/.harness-build.json +1 -1
  205. package/harness/bundles/java/cursor/contracts/ci-metrics.schema.json +36 -0
  206. package/harness/bundles/java/cursor/contracts/workflow-policy.json +30 -0
  207. package/harness/bundles/java/cursor/harness-apidoc/SKILL.md +1 -1
  208. package/harness/bundles/java/cursor/harness-archive/SKILL.md +1 -1
  209. package/harness/bundles/java/cursor/harness-codebase-map/SKILL.md +1 -1
  210. package/harness/bundles/java/cursor/harness-knowledge-ingest/SKILL.md +1 -1
  211. package/harness/bundles/java/cursor/harness-knowledge-query/SKILL.md +1 -1
  212. package/harness/bundles/java/cursor/harness-package/SKILL.md +1 -1
  213. package/harness/bundles/java/cursor/harness-plan/SKILL.md +4 -4
  214. package/harness/bundles/java/cursor/harness-plan/checklist.md +6 -0
  215. package/harness/bundles/java/cursor/harness-plan/reference.md +7 -2
  216. package/harness/bundles/java/cursor/harness-review/SKILL.md +1 -1
  217. package/harness/bundles/java/cursor/harness-run/SKILL.md +1 -1
  218. package/harness/bundles/java/cursor/harness-submit/SKILL.md +1 -1
  219. package/harness/bundles/java/cursor/harness-sync/SKILL.md +1 -1
  220. package/harness/bundles/java/cursor/harness-test/SKILL.md +1 -1
  221. package/harness/bundles/java/cursor/scripts/harness_archive.py +282 -120
  222. package/harness/bundles/java/cursor/scripts/harness_events.py +400 -24
  223. package/harness/bundles/java/cursor/scripts/harness_gate.py +585 -21
  224. package/harness/bundles/java/cursor/scripts/harness_integration.py +403 -18
  225. package/harness/bundles/java/cursor/scripts/harness_ledger.py +225 -29
  226. package/harness/bundles/java/cursor/scripts/harness_phase.py +926 -0
  227. package/harness/bundles/java/cursor/scripts/harness_plan_finalize.py +361 -0
  228. package/harness/bundles/java/cursor/scripts/harness_profile.py +50 -0
  229. package/harness/bundles/java/cursor/scripts/harness_runtime.py +248 -0
  230. package/harness/bundles/java/cursor/scripts/harness_state.py +15 -2
  231. package/harness/bundles/java/cursor/scripts/harness_test_guard.py +188 -0
  232. package/harness/bundles/java/cursor/scripts/harness_workflow_policy.py +19 -0
  233. package/harness/manifests/general/claude-code.json +42 -26
  234. package/harness/manifests/general/codebuddy.json +42 -26
  235. package/harness/manifests/general/codex.json +42 -26
  236. package/harness/manifests/general/cursor.json +42 -26
  237. package/harness/manifests/java/claude-code.json +42 -26
  238. package/harness/manifests/java/codebuddy.json +42 -26
  239. package/harness/manifests/java/codex.json +42 -26
  240. package/harness/manifests/java/cursor.json +42 -26
  241. package/hunter-workflow-family.json +2 -2
  242. package/package.json +1 -1
@@ -0,0 +1,361 @@
1
+ #!/usr/bin/env python3
2
+ """Validate and publish a complete Harness plan artifact set transactionally."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import argparse
7
+ import contextlib
8
+ import hashlib
9
+ import io
10
+ import json
11
+ import os
12
+ import re
13
+ import shutil
14
+ import sys
15
+ import tempfile
16
+ from pathlib import Path
17
+ from typing import Any
18
+
19
+ SCRIPTS_DIR = Path(__file__).resolve().parent
20
+ if str(SCRIPTS_DIR) not in sys.path:
21
+ sys.path.insert(0, str(SCRIPTS_DIR))
22
+
23
+ import harness_events # noqa: E402
24
+
25
+ if hasattr(sys.stdout, "reconfigure"):
26
+ sys.stdout.reconfigure(encoding="utf-8")
27
+ if hasattr(sys.stderr, "reconfigure"):
28
+ sys.stderr.reconfigure(encoding="utf-8")
29
+
30
+ SCHEMA_VERSION = 1
31
+ _LINK = re.compile(r"\[[^\]]+\]\(([^)]+)\)")
32
+
33
+
34
+ def _result_error(code: str, message: str) -> dict[str, Any]:
35
+ return {"ok": False, "code": code, "error": message}
36
+
37
+
38
+ def _atomic_write_json(path: Path, payload: dict[str, Any]) -> None:
39
+ path.parent.mkdir(parents=True, exist_ok=True)
40
+ fd, raw_tmp = tempfile.mkstemp(
41
+ prefix=f".{path.name}.", suffix=".tmp", dir=str(path.parent)
42
+ )
43
+ tmp = Path(raw_tmp)
44
+ try:
45
+ with os.fdopen(fd, "w", encoding="utf-8", newline="\n") as handle:
46
+ json.dump(payload, handle, ensure_ascii=False, indent=2)
47
+ handle.write("\n")
48
+ handle.flush()
49
+ os.fsync(handle.fileno())
50
+ os.replace(tmp, path)
51
+ finally:
52
+ tmp.unlink(missing_ok=True)
53
+
54
+
55
+ def _frontmatter(text: str) -> dict[str, str] | None:
56
+ lines = text.splitlines()
57
+ if not lines or lines[0].strip() != "---":
58
+ return None
59
+ data: dict[str, str] = {}
60
+ for line in lines[1:]:
61
+ if line.strip() == "---":
62
+ return data
63
+ if ":" not in line:
64
+ return None
65
+ key, value = line.split(":", 1)
66
+ data[key.strip()] = value.strip()
67
+ return None
68
+
69
+
70
+ def _artifact_files(staging: Path) -> list[Path]:
71
+ files: list[Path] = []
72
+ for path in staging.rglob("*"):
73
+ if path.is_symlink():
74
+ raise ValueError(f"PLAN_ARTIFACT_SYMLINK: {path}")
75
+ if path.is_file():
76
+ files.append(path)
77
+ return sorted(files, key=lambda item: item.relative_to(staging).as_posix())
78
+
79
+
80
+ def validate_staging(staging: Path, change_name: str) -> dict[str, Any]:
81
+ staging = staging.resolve()
82
+ required = {
83
+ Path("spec") / f"{change_name}-design.md",
84
+ Path("plans") / f"{change_name}-plan.md",
85
+ Path("plans") / f"{change_name}-implementation-detail.md",
86
+ Path("plans") / f"{change_name}-test-scenarios.md",
87
+ Path("meta") / "gate-policy.json",
88
+ Path("meta") / "worktree.json",
89
+ }
90
+ try:
91
+ files = _artifact_files(staging)
92
+ except ValueError as exc:
93
+ code = str(exc).split(":", 1)[0]
94
+ return _result_error(code, str(exc))
95
+ rel_files = {path.relative_to(staging) for path in files}
96
+ missing = sorted(path.as_posix() for path in required - rel_files)
97
+ if missing:
98
+ return _result_error(
99
+ "PLAN_ARTIFACT_MISSING", "missing required artifacts: " + ", ".join(missing)
100
+ )
101
+
102
+ for path in files:
103
+ rel = path.relative_to(staging)
104
+ if rel.parts[0] not in {"spec", "plans", "meta"}:
105
+ return _result_error(
106
+ "PLAN_ARTIFACT_PATH_INVALID", f"unexpected artifact root: {rel.as_posix()}"
107
+ )
108
+ if path.suffix.lower() == ".json":
109
+ try:
110
+ payload = json.loads(path.read_text(encoding="utf-8-sig"))
111
+ except (OSError, json.JSONDecodeError) as exc:
112
+ return _result_error(
113
+ "PLAN_ARTIFACT_INVALID_JSON", f"{rel.as_posix()}: {exc}"
114
+ )
115
+ if not isinstance(payload, dict):
116
+ return _result_error(
117
+ "PLAN_ARTIFACT_INVALID_JSON",
118
+ f"{rel.as_posix()}: top-level JSON must be an object",
119
+ )
120
+ elif path.suffix.lower() == ".md":
121
+ try:
122
+ text = path.read_text(encoding="utf-8-sig")
123
+ except OSError as exc:
124
+ return _result_error("PLAN_ARTIFACT_UNREADABLE", str(exc))
125
+ frontmatter = _frontmatter(text)
126
+ if frontmatter is None or frontmatter.get("change-name") != change_name:
127
+ return _result_error(
128
+ "PLAN_ARTIFACT_FRONTMATTER_INVALID",
129
+ f"{rel.as_posix()}: change-name frontmatter mismatch",
130
+ )
131
+ for raw_link in _LINK.findall(text):
132
+ link = raw_link.strip().split("#", 1)[0]
133
+ if not link or "://" in link or link.startswith("mailto:"):
134
+ continue
135
+ target = (path.parent / link).resolve()
136
+ try:
137
+ target.relative_to(staging)
138
+ except ValueError:
139
+ return _result_error(
140
+ "PLAN_ARTIFACT_REFERENCE_INVALID",
141
+ f"{rel.as_posix()}: reference escapes staging: {raw_link}",
142
+ )
143
+ if not target.is_file():
144
+ return _result_error(
145
+ "PLAN_ARTIFACT_REFERENCE_MISSING",
146
+ f"{rel.as_posix()}: missing reference {raw_link}",
147
+ )
148
+
149
+ digest = hashlib.sha256()
150
+ artifact_names: list[str] = []
151
+ for path in files:
152
+ rel = path.relative_to(staging).as_posix()
153
+ artifact_names.append(rel)
154
+ digest.update(rel.encode("utf-8"))
155
+ digest.update(b"\0")
156
+ digest.update(path.read_bytes())
157
+ digest.update(b"\0")
158
+ return {
159
+ "ok": True,
160
+ "files": artifact_names,
161
+ "artifactsHash": "sha256:" + digest.hexdigest(),
162
+ }
163
+
164
+
165
+ def _terminal_exists(change_dir: Path, run_id: str, attempt: int) -> bool:
166
+ try:
167
+ events = harness_events.load_events(harness_events.events_path(change_dir))
168
+ except (OSError, ValueError):
169
+ return False
170
+ return any(
171
+ event.get("phase") == "plan"
172
+ and event.get("type") == "phase.end"
173
+ and event.get("run_id") == run_id
174
+ and event.get("attempt") == attempt
175
+ and str(event.get("status") or "").upper() == "OK"
176
+ for event in events
177
+ )
178
+
179
+
180
+ def _append_terminal(change_dir: Path, run_id: str, attempt: int) -> tuple[int, str]:
181
+ stdout = io.StringIO()
182
+ stderr = io.StringIO()
183
+ with contextlib.redirect_stdout(stdout), contextlib.redirect_stderr(stderr):
184
+ code = harness_events.main(
185
+ [
186
+ "append",
187
+ "--change-dir",
188
+ str(change_dir),
189
+ "--phase",
190
+ "plan",
191
+ "--type",
192
+ "phase.end",
193
+ "--status",
194
+ "OK",
195
+ "--reason",
196
+ "plan artifacts validated and published",
197
+ "--run-id",
198
+ run_id,
199
+ "--attempt",
200
+ str(attempt),
201
+ "--json",
202
+ ]
203
+ )
204
+ return code, stderr.getvalue().strip()
205
+
206
+
207
+ def finalize_plan(
208
+ change_dir: Path,
209
+ staging: Path,
210
+ *,
211
+ change_name: str,
212
+ run_id: str,
213
+ attempt: int,
214
+ ) -> dict[str, Any]:
215
+ change_dir = change_dir.resolve()
216
+ staging = staging.resolve()
217
+ validation = validate_staging(staging, change_name)
218
+ if not validation["ok"]:
219
+ return validation
220
+
221
+ receipt_path = change_dir / "meta" / "plan-finalization.json"
222
+ lock_path = change_dir / "meta" / "plan-finalize.lock"
223
+ receipt: dict[str, Any] | None = None
224
+ if receipt_path.is_file():
225
+ try:
226
+ loaded = json.loads(receipt_path.read_text(encoding="utf-8-sig"))
227
+ receipt = loaded if isinstance(loaded, dict) else None
228
+ except (OSError, json.JSONDecodeError) as exc:
229
+ return _result_error("PLAN_FINALIZATION_RECEIPT_INVALID", str(exc))
230
+ if receipt and receipt.get("artifactsHash") != validation["artifactsHash"]:
231
+ return _result_error(
232
+ "PLAN_FINALIZATION_HASH_CONFLICT",
233
+ "finalizer was already invoked with a different artifact set",
234
+ )
235
+ if (
236
+ receipt
237
+ and receipt.get("status") == "finalized"
238
+ and _terminal_exists(change_dir, run_id, attempt)
239
+ ):
240
+ return {
241
+ "ok": True,
242
+ "action": "finalize",
243
+ "idempotent": True,
244
+ "artifactsHash": validation["artifactsHash"],
245
+ "files": validation["files"],
246
+ "receiptPath": str(receipt_path),
247
+ }
248
+
249
+ lock_path.parent.mkdir(parents=True, exist_ok=True)
250
+ try:
251
+ lock_fd = os.open(str(lock_path), os.O_CREAT | os.O_EXCL | os.O_WRONLY, 0o600)
252
+ os.close(lock_fd)
253
+ except FileExistsError:
254
+ return _result_error("PLAN_FINALIZATION_LOCKED", f"lock exists: {lock_path}")
255
+
256
+ created: list[Path] = []
257
+ terminal_committed = False
258
+ try:
259
+ for rel_text in validation["files"]:
260
+ source = staging / rel_text
261
+ target = change_dir / rel_text
262
+ if target.exists() and target.read_bytes() != source.read_bytes():
263
+ return _result_error(
264
+ "PLAN_TARGET_CONFLICT", f"refusing to overwrite {rel_text}"
265
+ )
266
+
267
+ for rel_text in validation["files"]:
268
+ source = staging / rel_text
269
+ target = change_dir / rel_text
270
+ if target.exists():
271
+ continue
272
+ target.parent.mkdir(parents=True, exist_ok=True)
273
+ fd, raw_tmp = tempfile.mkstemp(
274
+ prefix=f".{target.name}.", suffix=".tmp", dir=str(target.parent)
275
+ )
276
+ os.close(fd)
277
+ tmp = Path(raw_tmp)
278
+ try:
279
+ shutil.copyfile(source, tmp)
280
+ os.replace(tmp, target)
281
+ created.append(target)
282
+ finally:
283
+ tmp.unlink(missing_ok=True)
284
+
285
+ pending_receipt = {
286
+ "schemaVersion": SCHEMA_VERSION,
287
+ "changeName": change_name,
288
+ "status": "publishing",
289
+ "artifactsHash": validation["artifactsHash"],
290
+ "files": validation["files"],
291
+ "runId": run_id,
292
+ "attempt": attempt,
293
+ }
294
+ _atomic_write_json(receipt_path, pending_receipt)
295
+ terminal_code, terminal_error = _append_terminal(change_dir, run_id, attempt)
296
+ terminal_committed = _terminal_exists(change_dir, run_id, attempt)
297
+ if terminal_code != 0 and not terminal_committed:
298
+ for target in reversed(created):
299
+ target.unlink(missing_ok=True)
300
+ receipt_path.unlink(missing_ok=True)
301
+ return _result_error(
302
+ "PLAN_TERMINAL_APPEND_FAILED",
303
+ terminal_error or "phase.end append failed",
304
+ )
305
+ pending_receipt["status"] = "finalized"
306
+ _atomic_write_json(receipt_path, pending_receipt)
307
+ return {
308
+ "ok": True,
309
+ "action": "finalize",
310
+ "idempotent": False,
311
+ "artifactsHash": validation["artifactsHash"],
312
+ "files": validation["files"],
313
+ "receiptPath": str(receipt_path),
314
+ "executionLogPath": str(harness_events.execution_log_path(change_dir)),
315
+ }
316
+ except OSError as exc:
317
+ if terminal_committed or _terminal_exists(change_dir, run_id, attempt):
318
+ return _result_error(
319
+ "PLAN_FINALIZATION_RECOVERY_REQUIRED",
320
+ f"terminal committed; retry finalization to complete receipt: {exc}",
321
+ )
322
+ for target in reversed(created):
323
+ target.unlink(missing_ok=True)
324
+ receipt_path.unlink(missing_ok=True)
325
+ return _result_error("PLAN_FINALIZATION_IO_ERROR", str(exc))
326
+ finally:
327
+ lock_path.unlink(missing_ok=True)
328
+
329
+
330
+ def build_parser() -> argparse.ArgumentParser:
331
+ parser = argparse.ArgumentParser(prog="harness_plan_finalize.py")
332
+ sub = parser.add_subparsers(dest="command", required=True)
333
+ finalize = sub.add_parser("finalize")
334
+ finalize.add_argument("--change-dir", required=True)
335
+ finalize.add_argument("--staging-dir", required=True)
336
+ finalize.add_argument("--change", required=True)
337
+ finalize.add_argument("--run-id", required=True)
338
+ finalize.add_argument("--attempt", required=True, type=int)
339
+ finalize.add_argument("--json", action="store_true")
340
+ return parser
341
+
342
+
343
+ def main(argv: list[str] | None = None) -> int:
344
+ args = build_parser().parse_args(argv)
345
+ result = finalize_plan(
346
+ Path(args.change_dir),
347
+ Path(args.staging_dir),
348
+ change_name=args.change,
349
+ run_id=args.run_id,
350
+ attempt=args.attempt,
351
+ )
352
+ stream = sys.stdout if result["ok"] else sys.stderr
353
+ if args.json:
354
+ stream.write(json.dumps(result, ensure_ascii=False, indent=2) + "\n")
355
+ else:
356
+ stream.write((result.get("action") or result.get("code") or "error") + "\n")
357
+ return 0 if result["ok"] else 1
358
+
359
+
360
+ if __name__ == "__main__":
361
+ raise SystemExit(main())
@@ -43,7 +43,9 @@ DEFAULT_EXCLUDED_ROOTS: tuple[str, ...] = (
43
43
  ".git",
44
44
  ".harness",
45
45
  ".claude/worktrees",
46
+ ".codex/worktrees",
46
47
  ".cursor/worktrees",
48
+ ".codebuddy/worktrees",
47
49
  ".codeium/worktrees",
48
50
  "target",
49
51
  "build",
@@ -134,6 +136,27 @@ def detect_project_type(project: Path) -> str:
134
136
  return "unknown"
135
137
 
136
138
 
139
+ def discover_nested_components(
140
+ project: Path, excluded: tuple[str, ...] | list[str]
141
+ ) -> list[str]:
142
+ """Return deterministic type:path labels for supported nested projects."""
143
+ markers = {
144
+ "pyproject.toml": "python",
145
+ "package.json": "node",
146
+ "pom.xml": "java-maven",
147
+ }
148
+ found: list[tuple[str, str]] = []
149
+ for marker, kind in markers.items():
150
+ for path in project.rglob(marker):
151
+ if not path.is_file():
152
+ continue
153
+ rel = path.relative_to(project)
154
+ if rel.parent == Path(".") or is_path_excluded(rel.as_posix(), excluded):
155
+ continue
156
+ found.append((rel.parent.as_posix(), kind))
157
+ return [f"{kind}:{rel}" for rel, kind in sorted(set(found))]
158
+
159
+
137
160
  def which_tool(name: str) -> str:
138
161
  found = shutil.which(name)
139
162
  if found:
@@ -386,6 +409,32 @@ def detect(project: Path) -> dict[str, Any]:
386
409
  existing = load_profile(project)
387
410
  excluded = DEFAULT_EXCLUDED_ROOTS
388
411
  project_type = detect_project_type(project)
412
+ components = discover_nested_components(project, excluded)
413
+ component_types = {item.split(":", 1)[0] for item in components}
414
+ if project_type != "unknown":
415
+ component_types.add(project_type)
416
+ if (project_type == "unknown" and components) or len(component_types) > 1:
417
+ return {
418
+ "ok": False,
419
+ "code": "DETECTION_AMBIGUOUS",
420
+ "action": "detect",
421
+ "project": str(project),
422
+ "profilePath": str(project / PROFILE_REL),
423
+ "applied": False,
424
+ "detectedComponents": components,
425
+ "message": "multiple or nested project components require an explicit profile",
426
+ }
427
+ if project_type == "unknown" and existing:
428
+ return {
429
+ "ok": False,
430
+ "code": "DETECTION_INCONCLUSIVE",
431
+ "action": "detect",
432
+ "project": str(project),
433
+ "profilePath": str(project / PROFILE_REL),
434
+ "applied": False,
435
+ "detectedComponents": [],
436
+ "message": "detection was inconclusive; existing profile was preserved",
437
+ }
389
438
 
390
439
  profile = empty_profile_skeleton(excluded)
391
440
  profile["projectType"] = project_type
@@ -439,6 +488,7 @@ def detect(project: Path) -> dict[str, Any]:
439
488
  "profile": profile,
440
489
  "created": existing is None,
441
490
  "updated": True,
491
+ "applied": True,
442
492
  }
443
493
 
444
494
 
@@ -0,0 +1,248 @@
1
+ #!/usr/bin/env python3
2
+ """Resolve Harness runtimes into a reusable, argv-based phase capsule."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import argparse
7
+ import datetime as dt
8
+ import json
9
+ import os
10
+ import re
11
+ import shutil
12
+ import subprocess
13
+ import sys
14
+ import tempfile
15
+ from pathlib import Path
16
+ from typing import Any, Callable
17
+
18
+ if hasattr(sys.stdout, "reconfigure"):
19
+ sys.stdout.reconfigure(encoding="utf-8")
20
+ if hasattr(sys.stderr, "reconfigure"):
21
+ sys.stderr.reconfigure(encoding="utf-8")
22
+
23
+ SCHEMA_VERSION = 1
24
+ _CHANGE_ID = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$")
25
+ _ADAPTERS = {
26
+ "claude-code": (".claude/worktrees", "claude/"),
27
+ "codex": (".codex/worktrees", "codex/"),
28
+ "cursor": (".cursor/worktrees", "cursor/"),
29
+ "codebuddy": (".codebuddy/worktrees", "codebuddy/"),
30
+ }
31
+
32
+
33
+ def now_iso() -> str:
34
+ return dt.datetime.now().astimezone().isoformat(timespec="milliseconds")
35
+
36
+
37
+ def atomic_write_json(path: Path, payload: dict[str, Any]) -> None:
38
+ path.parent.mkdir(parents=True, exist_ok=True)
39
+ text = json.dumps(payload, ensure_ascii=False, indent=2) + "\n"
40
+ fd, raw_tmp = tempfile.mkstemp(
41
+ prefix=f".{path.name}.", suffix=".tmp", dir=str(path.parent)
42
+ )
43
+ tmp = Path(raw_tmp)
44
+ try:
45
+ with os.fdopen(fd, "w", encoding="utf-8", newline="\n") as handle:
46
+ handle.write(text)
47
+ handle.flush()
48
+ os.fsync(handle.fileno())
49
+ os.replace(tmp, path)
50
+ finally:
51
+ tmp.unlink(missing_ok=True)
52
+
53
+
54
+ def adapter_worktree(agent: str, change_id: str) -> dict[str, str]:
55
+ if agent not in _ADAPTERS:
56
+ raise ValueError(f"ADAPTER_UNKNOWN: {agent}")
57
+ if not _CHANGE_ID.fullmatch(change_id) or change_id in {".", ".."}:
58
+ raise ValueError(f"ADAPTER_CHANGE_ID_INVALID: {change_id}")
59
+ root, prefix = _ADAPTERS[agent]
60
+ return {
61
+ "agent": agent,
62
+ "worktreeRoot": root,
63
+ "path": f"{root}/{change_id}",
64
+ "branchPrefix": prefix,
65
+ "branch": f"{prefix}{change_id}",
66
+ }
67
+
68
+
69
+ def _absolute_executable(value: str | Path) -> str:
70
+ return str(Path(value).expanduser().resolve())
71
+
72
+
73
+ def _run_version(executable: str, *args: str) -> dict[str, Any]:
74
+ try:
75
+ proc = subprocess.run(
76
+ [executable, *args],
77
+ capture_output=True,
78
+ text=True,
79
+ encoding="utf-8",
80
+ errors="replace",
81
+ check=False,
82
+ timeout=10,
83
+ )
84
+ except (OSError, subprocess.TimeoutExpired) as exc:
85
+ return {"available": False, "error": str(exc)}
86
+ version = (proc.stdout or proc.stderr).strip().splitlines()
87
+ return {
88
+ "available": proc.returncode == 0,
89
+ "version": version[0] if version else "",
90
+ "exitCode": proc.returncode,
91
+ }
92
+
93
+
94
+ def probe_powershell(
95
+ executable: Path,
96
+ *,
97
+ runner: Callable[..., subprocess.CompletedProcess[str]] = subprocess.run,
98
+ ) -> dict[str, Any]:
99
+ """Probe Windows PowerShell/Pwsh without relying on PowerShell 6 Test-Json."""
100
+ script = (
101
+ "$value = [ordered]@{"
102
+ "edition = [string]$PSVersionTable.PSEdition;"
103
+ "version = [string]$PSVersionTable.PSVersion.ToString()"
104
+ "}; $value | ConvertTo-Json -Compress"
105
+ )
106
+ argv = [
107
+ _absolute_executable(executable),
108
+ "-NoLogo",
109
+ "-NoProfile",
110
+ "-NonInteractive",
111
+ "-Command",
112
+ script,
113
+ ]
114
+ try:
115
+ proc = runner(
116
+ argv,
117
+ capture_output=True,
118
+ text=True,
119
+ encoding="utf-8",
120
+ errors="replace",
121
+ check=False,
122
+ timeout=10,
123
+ )
124
+ if proc.returncode != 0:
125
+ return {
126
+ "available": False,
127
+ "executable": argv[0],
128
+ "argvPrefix": [argv[0], "-NoLogo", "-NoProfile", "-NonInteractive"],
129
+ "error": proc.stderr.strip(),
130
+ "jsonCapability": "convert-to-json",
131
+ }
132
+ raw = json.loads(proc.stdout.strip())
133
+ return {
134
+ "available": True,
135
+ "executable": argv[0],
136
+ "argvPrefix": [argv[0], "-NoLogo", "-NoProfile", "-NonInteractive"],
137
+ "edition": str(raw.get("edition") or "Desktop"),
138
+ "version": str(raw.get("version") or ""),
139
+ "jsonCapability": "convert-to-json",
140
+ }
141
+ except (OSError, subprocess.TimeoutExpired, json.JSONDecodeError) as exc:
142
+ return {
143
+ "available": False,
144
+ "executable": argv[0],
145
+ "argvPrefix": [argv[0], "-NoLogo", "-NoProfile", "-NonInteractive"],
146
+ "error": str(exc),
147
+ "jsonCapability": "convert-to-json",
148
+ }
149
+
150
+
151
+ def _optional_runtime(name: str, version_arg: str = "--version") -> dict[str, Any]:
152
+ found = shutil.which(name)
153
+ if not found:
154
+ return {"available": False, "executable": None, "argvPrefix": []}
155
+ executable = _absolute_executable(found)
156
+ return {
157
+ "executable": executable,
158
+ "argvPrefix": [executable],
159
+ **_run_version(executable, version_arg),
160
+ }
161
+
162
+
163
+ def doctor(project: Path, change_dir: Path, *, agent: str) -> dict[str, Any]:
164
+ project = project.expanduser().resolve()
165
+ change_dir = change_dir.expanduser().resolve()
166
+ python_executable = _absolute_executable(sys.executable)
167
+ python_version = _run_version(python_executable, "--version")
168
+
169
+ powershell_path = shutil.which("pwsh") or shutil.which("powershell")
170
+ powershell = (
171
+ probe_powershell(Path(powershell_path))
172
+ if powershell_path
173
+ else {
174
+ "available": False,
175
+ "executable": None,
176
+ "argvPrefix": [],
177
+ "edition": None,
178
+ "version": None,
179
+ "jsonCapability": "python-json",
180
+ }
181
+ )
182
+ sample = {"path": "E:/示例/计划", "ok": True}
183
+ json_round_trip = json.loads(json.dumps(sample, ensure_ascii=False)) == sample
184
+ payload = {
185
+ "schemaVersion": SCHEMA_VERSION,
186
+ "generatedAt": now_iso(),
187
+ "projectRoot": str(project),
188
+ "changeDir": str(change_dir),
189
+ "adapter": adapter_worktree(agent, change_dir.name),
190
+ "runtimes": {
191
+ "python": {
192
+ "available": python_version.get("available", True),
193
+ "executable": python_executable,
194
+ "argvPrefix": [python_executable],
195
+ "version": python_version.get("version") or sys.version.split()[0],
196
+ "stdioEncoding": "utf-8",
197
+ "filesystemEncoding": sys.getfilesystemencoding(),
198
+ },
199
+ "node": _optional_runtime("node"),
200
+ "powershell": powershell,
201
+ },
202
+ "capabilities": {
203
+ "jsonRoundTrip": json_round_trip,
204
+ "argvArrays": True,
205
+ "utf8NoBom": True,
206
+ },
207
+ }
208
+ atomic_write_json(change_dir / "meta" / "runtime.json", payload)
209
+ return {"ok": True, "action": "doctor", **payload}
210
+
211
+
212
+ def build_parser() -> argparse.ArgumentParser:
213
+ parser = argparse.ArgumentParser(prog="harness_runtime.py")
214
+ sub = parser.add_subparsers(dest="command", required=True)
215
+ p_doctor = sub.add_parser("doctor")
216
+ p_doctor.add_argument("--project", required=True)
217
+ p_doctor.add_argument("--change-dir", required=True)
218
+ p_doctor.add_argument("--agent", choices=sorted(_ADAPTERS), required=True)
219
+ p_doctor.add_argument("--json", action="store_true")
220
+ p_adapter = sub.add_parser("adapter")
221
+ p_adapter.add_argument("--agent", choices=sorted(_ADAPTERS), required=True)
222
+ p_adapter.add_argument("--change", required=True)
223
+ p_adapter.add_argument("--json", action="store_true")
224
+ return parser
225
+
226
+
227
+ def main(argv: list[str] | None = None) -> int:
228
+ args = build_parser().parse_args(argv)
229
+ try:
230
+ if args.command == "doctor":
231
+ result = doctor(Path(args.project), Path(args.change_dir), agent=args.agent)
232
+ else:
233
+ result = {"ok": True, "action": "adapter", **adapter_worktree(args.agent, args.change)}
234
+ except (OSError, ValueError) as exc:
235
+ code = str(exc).split(":", 1)[0]
236
+ result = {"ok": False, "code": code, "error": str(exc)}
237
+ stream = sys.stderr
238
+ stream.write(json.dumps(result, ensure_ascii=False) + "\n")
239
+ return 1
240
+ if args.json:
241
+ sys.stdout.write(json.dumps(result, ensure_ascii=False, indent=2) + "\n")
242
+ else:
243
+ sys.stdout.write(str(result.get("action")) + "\n")
244
+ return 0
245
+
246
+
247
+ if __name__ == "__main__":
248
+ raise SystemExit(main())