@hunter-harness/workflow-harness 0.2.10 → 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 (250) hide show
  1. package/harness/bundles/general/claude-code/.harness-build.json +1 -1
  2. package/harness/bundles/general/claude-code/contracts/ci-metrics.schema.json +36 -0
  3. package/harness/bundles/general/claude-code/contracts/workflow-policy.json +30 -0
  4. package/harness/bundles/general/claude-code/harness-archive/SKILL.md +1 -1
  5. package/harness/bundles/general/claude-code/harness-codebase-map/SKILL.md +1 -1
  6. package/harness/bundles/general/claude-code/harness-knowledge-ingest/SKILL.md +1 -1
  7. package/harness/bundles/general/claude-code/harness-knowledge-ingest/scripts/harness_knowledge.py +24 -2
  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 +4 -4
  10. package/harness/bundles/general/claude-code/harness-plan/checklist.md +6 -0
  11. package/harness/bundles/general/claude-code/harness-plan/reference.md +7 -2
  12. package/harness/bundles/general/claude-code/harness-review/SKILL.md +1 -1
  13. package/harness/bundles/general/claude-code/harness-run/SKILL.md +1 -1
  14. package/harness/bundles/general/claude-code/harness-run/checklist.md +3 -2
  15. package/harness/bundles/general/claude-code/harness-run/reference.md +6 -4
  16. package/harness/bundles/general/claude-code/harness-submit/SKILL.md +1 -1
  17. package/harness/bundles/general/claude-code/harness-sync/SKILL.md +1 -1
  18. package/harness/bundles/general/claude-code/harness-test/SKILL.md +1 -1
  19. package/harness/bundles/general/claude-code/scripts/harness_archive.py +282 -120
  20. package/harness/bundles/general/claude-code/scripts/harness_events.py +400 -24
  21. package/harness/bundles/general/claude-code/scripts/harness_gate.py +585 -21
  22. package/harness/bundles/general/claude-code/scripts/harness_integration.py +403 -18
  23. package/harness/bundles/general/claude-code/scripts/harness_ledger.py +225 -29
  24. package/harness/bundles/general/claude-code/scripts/harness_phase.py +926 -0
  25. package/harness/bundles/general/claude-code/scripts/harness_plan_finalize.py +361 -0
  26. package/harness/bundles/general/claude-code/scripts/harness_profile.py +50 -0
  27. package/harness/bundles/general/claude-code/scripts/harness_runtime.py +248 -0
  28. package/harness/bundles/general/claude-code/scripts/harness_state.py +15 -2
  29. package/harness/bundles/general/claude-code/scripts/harness_test_guard.py +188 -0
  30. package/harness/bundles/general/claude-code/scripts/harness_workflow_policy.py +19 -0
  31. package/harness/bundles/general/codebuddy/.harness-build.json +1 -1
  32. package/harness/bundles/general/codebuddy/contracts/ci-metrics.schema.json +36 -0
  33. package/harness/bundles/general/codebuddy/contracts/workflow-policy.json +30 -0
  34. package/harness/bundles/general/codebuddy/harness-archive/SKILL.md +1 -1
  35. package/harness/bundles/general/codebuddy/harness-codebase-map/SKILL.md +1 -1
  36. package/harness/bundles/general/codebuddy/harness-knowledge-ingest/SKILL.md +1 -1
  37. package/harness/bundles/general/codebuddy/harness-knowledge-ingest/scripts/harness_knowledge.py +24 -2
  38. package/harness/bundles/general/codebuddy/harness-knowledge-query/SKILL.md +1 -1
  39. package/harness/bundles/general/codebuddy/harness-plan/SKILL.md +4 -4
  40. package/harness/bundles/general/codebuddy/harness-plan/checklist.md +6 -0
  41. package/harness/bundles/general/codebuddy/harness-plan/reference.md +7 -2
  42. package/harness/bundles/general/codebuddy/harness-review/SKILL.md +1 -1
  43. package/harness/bundles/general/codebuddy/harness-run/SKILL.md +1 -1
  44. package/harness/bundles/general/codebuddy/harness-run/checklist.md +3 -2
  45. package/harness/bundles/general/codebuddy/harness-run/reference.md +6 -4
  46. package/harness/bundles/general/codebuddy/harness-submit/SKILL.md +1 -1
  47. package/harness/bundles/general/codebuddy/harness-sync/SKILL.md +1 -1
  48. package/harness/bundles/general/codebuddy/harness-test/SKILL.md +1 -1
  49. package/harness/bundles/general/codebuddy/scripts/harness_archive.py +282 -120
  50. package/harness/bundles/general/codebuddy/scripts/harness_events.py +400 -24
  51. package/harness/bundles/general/codebuddy/scripts/harness_gate.py +585 -21
  52. package/harness/bundles/general/codebuddy/scripts/harness_integration.py +403 -18
  53. package/harness/bundles/general/codebuddy/scripts/harness_ledger.py +225 -29
  54. package/harness/bundles/general/codebuddy/scripts/harness_phase.py +926 -0
  55. package/harness/bundles/general/codebuddy/scripts/harness_plan_finalize.py +361 -0
  56. package/harness/bundles/general/codebuddy/scripts/harness_profile.py +50 -0
  57. package/harness/bundles/general/codebuddy/scripts/harness_runtime.py +248 -0
  58. package/harness/bundles/general/codebuddy/scripts/harness_state.py +15 -2
  59. package/harness/bundles/general/codebuddy/scripts/harness_test_guard.py +188 -0
  60. package/harness/bundles/general/codebuddy/scripts/harness_workflow_policy.py +19 -0
  61. package/harness/bundles/general/codex/.harness-build.json +1 -1
  62. package/harness/bundles/general/codex/contracts/ci-metrics.schema.json +36 -0
  63. package/harness/bundles/general/codex/contracts/workflow-policy.json +30 -0
  64. package/harness/bundles/general/codex/harness-archive/SKILL.md +1 -1
  65. package/harness/bundles/general/codex/harness-codebase-map/SKILL.md +1 -1
  66. package/harness/bundles/general/codex/harness-knowledge-ingest/SKILL.md +1 -1
  67. package/harness/bundles/general/codex/harness-knowledge-ingest/scripts/harness_knowledge.py +24 -2
  68. package/harness/bundles/general/codex/harness-knowledge-query/SKILL.md +1 -1
  69. package/harness/bundles/general/codex/harness-plan/SKILL.md +4 -4
  70. package/harness/bundles/general/codex/harness-plan/checklist.md +6 -0
  71. package/harness/bundles/general/codex/harness-plan/reference.md +7 -2
  72. package/harness/bundles/general/codex/harness-review/SKILL.md +1 -1
  73. package/harness/bundles/general/codex/harness-run/SKILL.md +1 -1
  74. package/harness/bundles/general/codex/harness-run/checklist.md +3 -2
  75. package/harness/bundles/general/codex/harness-run/reference.md +6 -4
  76. package/harness/bundles/general/codex/harness-submit/SKILL.md +1 -1
  77. package/harness/bundles/general/codex/harness-sync/SKILL.md +1 -1
  78. package/harness/bundles/general/codex/harness-test/SKILL.md +1 -1
  79. package/harness/bundles/general/codex/scripts/harness_archive.py +282 -120
  80. package/harness/bundles/general/codex/scripts/harness_events.py +400 -24
  81. package/harness/bundles/general/codex/scripts/harness_gate.py +585 -21
  82. package/harness/bundles/general/codex/scripts/harness_integration.py +403 -18
  83. package/harness/bundles/general/codex/scripts/harness_ledger.py +225 -29
  84. package/harness/bundles/general/codex/scripts/harness_phase.py +926 -0
  85. package/harness/bundles/general/codex/scripts/harness_plan_finalize.py +361 -0
  86. package/harness/bundles/general/codex/scripts/harness_profile.py +50 -0
  87. package/harness/bundles/general/codex/scripts/harness_runtime.py +248 -0
  88. package/harness/bundles/general/codex/scripts/harness_state.py +15 -2
  89. package/harness/bundles/general/codex/scripts/harness_test_guard.py +188 -0
  90. package/harness/bundles/general/codex/scripts/harness_workflow_policy.py +19 -0
  91. package/harness/bundles/general/cursor/.harness-build.json +1 -1
  92. package/harness/bundles/general/cursor/contracts/ci-metrics.schema.json +36 -0
  93. package/harness/bundles/general/cursor/contracts/workflow-policy.json +30 -0
  94. package/harness/bundles/general/cursor/harness-archive/SKILL.md +1 -1
  95. package/harness/bundles/general/cursor/harness-codebase-map/SKILL.md +1 -1
  96. package/harness/bundles/general/cursor/harness-knowledge-ingest/SKILL.md +1 -1
  97. package/harness/bundles/general/cursor/harness-knowledge-ingest/scripts/harness_knowledge.py +24 -2
  98. package/harness/bundles/general/cursor/harness-knowledge-query/SKILL.md +1 -1
  99. package/harness/bundles/general/cursor/harness-plan/SKILL.md +4 -4
  100. package/harness/bundles/general/cursor/harness-plan/checklist.md +6 -0
  101. package/harness/bundles/general/cursor/harness-plan/reference.md +7 -2
  102. package/harness/bundles/general/cursor/harness-review/SKILL.md +1 -1
  103. package/harness/bundles/general/cursor/harness-run/SKILL.md +1 -1
  104. package/harness/bundles/general/cursor/harness-run/checklist.md +3 -2
  105. package/harness/bundles/general/cursor/harness-run/reference.md +6 -4
  106. package/harness/bundles/general/cursor/harness-submit/SKILL.md +1 -1
  107. package/harness/bundles/general/cursor/harness-sync/SKILL.md +1 -1
  108. package/harness/bundles/general/cursor/harness-test/SKILL.md +1 -1
  109. package/harness/bundles/general/cursor/scripts/harness_archive.py +282 -120
  110. package/harness/bundles/general/cursor/scripts/harness_events.py +400 -24
  111. package/harness/bundles/general/cursor/scripts/harness_gate.py +585 -21
  112. package/harness/bundles/general/cursor/scripts/harness_integration.py +403 -18
  113. package/harness/bundles/general/cursor/scripts/harness_ledger.py +225 -29
  114. package/harness/bundles/general/cursor/scripts/harness_phase.py +926 -0
  115. package/harness/bundles/general/cursor/scripts/harness_plan_finalize.py +361 -0
  116. package/harness/bundles/general/cursor/scripts/harness_profile.py +50 -0
  117. package/harness/bundles/general/cursor/scripts/harness_runtime.py +248 -0
  118. package/harness/bundles/general/cursor/scripts/harness_state.py +15 -2
  119. package/harness/bundles/general/cursor/scripts/harness_test_guard.py +188 -0
  120. package/harness/bundles/general/cursor/scripts/harness_workflow_policy.py +19 -0
  121. package/harness/bundles/java/claude-code/.harness-build.json +1 -1
  122. package/harness/bundles/java/claude-code/contracts/ci-metrics.schema.json +36 -0
  123. package/harness/bundles/java/claude-code/contracts/workflow-policy.json +30 -0
  124. package/harness/bundles/java/claude-code/harness-apidoc/SKILL.md +1 -1
  125. package/harness/bundles/java/claude-code/harness-archive/SKILL.md +1 -1
  126. package/harness/bundles/java/claude-code/harness-codebase-map/SKILL.md +1 -1
  127. package/harness/bundles/java/claude-code/harness-knowledge-ingest/SKILL.md +1 -1
  128. package/harness/bundles/java/claude-code/harness-knowledge-ingest/scripts/harness_knowledge.py +24 -2
  129. package/harness/bundles/java/claude-code/harness-knowledge-query/SKILL.md +1 -1
  130. package/harness/bundles/java/claude-code/harness-package/SKILL.md +1 -1
  131. package/harness/bundles/java/claude-code/harness-plan/SKILL.md +4 -4
  132. package/harness/bundles/java/claude-code/harness-plan/checklist.md +6 -0
  133. package/harness/bundles/java/claude-code/harness-plan/reference.md +7 -2
  134. package/harness/bundles/java/claude-code/harness-review/SKILL.md +1 -1
  135. package/harness/bundles/java/claude-code/harness-run/SKILL.md +1 -1
  136. package/harness/bundles/java/claude-code/harness-submit/SKILL.md +1 -1
  137. package/harness/bundles/java/claude-code/harness-sync/SKILL.md +1 -1
  138. package/harness/bundles/java/claude-code/harness-test/SKILL.md +1 -1
  139. package/harness/bundles/java/claude-code/scripts/harness_archive.py +282 -120
  140. package/harness/bundles/java/claude-code/scripts/harness_events.py +400 -24
  141. package/harness/bundles/java/claude-code/scripts/harness_gate.py +585 -21
  142. package/harness/bundles/java/claude-code/scripts/harness_integration.py +403 -18
  143. package/harness/bundles/java/claude-code/scripts/harness_ledger.py +225 -29
  144. package/harness/bundles/java/claude-code/scripts/harness_phase.py +926 -0
  145. package/harness/bundles/java/claude-code/scripts/harness_plan_finalize.py +361 -0
  146. package/harness/bundles/java/claude-code/scripts/harness_profile.py +50 -0
  147. package/harness/bundles/java/claude-code/scripts/harness_runtime.py +248 -0
  148. package/harness/bundles/java/claude-code/scripts/harness_state.py +15 -2
  149. package/harness/bundles/java/claude-code/scripts/harness_test_guard.py +188 -0
  150. package/harness/bundles/java/claude-code/scripts/harness_workflow_policy.py +19 -0
  151. package/harness/bundles/java/codebuddy/.harness-build.json +1 -1
  152. package/harness/bundles/java/codebuddy/contracts/ci-metrics.schema.json +36 -0
  153. package/harness/bundles/java/codebuddy/contracts/workflow-policy.json +30 -0
  154. package/harness/bundles/java/codebuddy/harness-apidoc/SKILL.md +1 -1
  155. package/harness/bundles/java/codebuddy/harness-archive/SKILL.md +1 -1
  156. package/harness/bundles/java/codebuddy/harness-codebase-map/SKILL.md +1 -1
  157. package/harness/bundles/java/codebuddy/harness-knowledge-ingest/SKILL.md +1 -1
  158. package/harness/bundles/java/codebuddy/harness-knowledge-ingest/scripts/harness_knowledge.py +24 -2
  159. package/harness/bundles/java/codebuddy/harness-knowledge-query/SKILL.md +1 -1
  160. package/harness/bundles/java/codebuddy/harness-package/SKILL.md +1 -1
  161. package/harness/bundles/java/codebuddy/harness-plan/SKILL.md +4 -4
  162. package/harness/bundles/java/codebuddy/harness-plan/checklist.md +6 -0
  163. package/harness/bundles/java/codebuddy/harness-plan/reference.md +7 -2
  164. package/harness/bundles/java/codebuddy/harness-review/SKILL.md +1 -1
  165. package/harness/bundles/java/codebuddy/harness-run/SKILL.md +1 -1
  166. package/harness/bundles/java/codebuddy/harness-submit/SKILL.md +1 -1
  167. package/harness/bundles/java/codebuddy/harness-sync/SKILL.md +1 -1
  168. package/harness/bundles/java/codebuddy/harness-test/SKILL.md +1 -1
  169. package/harness/bundles/java/codebuddy/scripts/harness_archive.py +282 -120
  170. package/harness/bundles/java/codebuddy/scripts/harness_events.py +400 -24
  171. package/harness/bundles/java/codebuddy/scripts/harness_gate.py +585 -21
  172. package/harness/bundles/java/codebuddy/scripts/harness_integration.py +403 -18
  173. package/harness/bundles/java/codebuddy/scripts/harness_ledger.py +225 -29
  174. package/harness/bundles/java/codebuddy/scripts/harness_phase.py +926 -0
  175. package/harness/bundles/java/codebuddy/scripts/harness_plan_finalize.py +361 -0
  176. package/harness/bundles/java/codebuddy/scripts/harness_profile.py +50 -0
  177. package/harness/bundles/java/codebuddy/scripts/harness_runtime.py +248 -0
  178. package/harness/bundles/java/codebuddy/scripts/harness_state.py +15 -2
  179. package/harness/bundles/java/codebuddy/scripts/harness_test_guard.py +188 -0
  180. package/harness/bundles/java/codebuddy/scripts/harness_workflow_policy.py +19 -0
  181. package/harness/bundles/java/codex/.harness-build.json +1 -1
  182. package/harness/bundles/java/codex/contracts/ci-metrics.schema.json +36 -0
  183. package/harness/bundles/java/codex/contracts/workflow-policy.json +30 -0
  184. package/harness/bundles/java/codex/harness-apidoc/SKILL.md +1 -1
  185. package/harness/bundles/java/codex/harness-archive/SKILL.md +1 -1
  186. package/harness/bundles/java/codex/harness-codebase-map/SKILL.md +1 -1
  187. package/harness/bundles/java/codex/harness-knowledge-ingest/SKILL.md +1 -1
  188. package/harness/bundles/java/codex/harness-knowledge-ingest/scripts/harness_knowledge.py +24 -2
  189. package/harness/bundles/java/codex/harness-knowledge-query/SKILL.md +1 -1
  190. package/harness/bundles/java/codex/harness-package/SKILL.md +1 -1
  191. package/harness/bundles/java/codex/harness-plan/SKILL.md +4 -4
  192. package/harness/bundles/java/codex/harness-plan/checklist.md +6 -0
  193. package/harness/bundles/java/codex/harness-plan/reference.md +7 -2
  194. package/harness/bundles/java/codex/harness-review/SKILL.md +1 -1
  195. package/harness/bundles/java/codex/harness-run/SKILL.md +1 -1
  196. package/harness/bundles/java/codex/harness-submit/SKILL.md +1 -1
  197. package/harness/bundles/java/codex/harness-sync/SKILL.md +1 -1
  198. package/harness/bundles/java/codex/harness-test/SKILL.md +1 -1
  199. package/harness/bundles/java/codex/scripts/harness_archive.py +282 -120
  200. package/harness/bundles/java/codex/scripts/harness_events.py +400 -24
  201. package/harness/bundles/java/codex/scripts/harness_gate.py +585 -21
  202. package/harness/bundles/java/codex/scripts/harness_integration.py +403 -18
  203. package/harness/bundles/java/codex/scripts/harness_ledger.py +225 -29
  204. package/harness/bundles/java/codex/scripts/harness_phase.py +926 -0
  205. package/harness/bundles/java/codex/scripts/harness_plan_finalize.py +361 -0
  206. package/harness/bundles/java/codex/scripts/harness_profile.py +50 -0
  207. package/harness/bundles/java/codex/scripts/harness_runtime.py +248 -0
  208. package/harness/bundles/java/codex/scripts/harness_state.py +15 -2
  209. package/harness/bundles/java/codex/scripts/harness_test_guard.py +188 -0
  210. package/harness/bundles/java/codex/scripts/harness_workflow_policy.py +19 -0
  211. package/harness/bundles/java/cursor/.harness-build.json +1 -1
  212. package/harness/bundles/java/cursor/contracts/ci-metrics.schema.json +36 -0
  213. package/harness/bundles/java/cursor/contracts/workflow-policy.json +30 -0
  214. package/harness/bundles/java/cursor/harness-apidoc/SKILL.md +1 -1
  215. package/harness/bundles/java/cursor/harness-archive/SKILL.md +1 -1
  216. package/harness/bundles/java/cursor/harness-codebase-map/SKILL.md +1 -1
  217. package/harness/bundles/java/cursor/harness-knowledge-ingest/SKILL.md +1 -1
  218. package/harness/bundles/java/cursor/harness-knowledge-ingest/scripts/harness_knowledge.py +24 -2
  219. package/harness/bundles/java/cursor/harness-knowledge-query/SKILL.md +1 -1
  220. package/harness/bundles/java/cursor/harness-package/SKILL.md +1 -1
  221. package/harness/bundles/java/cursor/harness-plan/SKILL.md +4 -4
  222. package/harness/bundles/java/cursor/harness-plan/checklist.md +6 -0
  223. package/harness/bundles/java/cursor/harness-plan/reference.md +7 -2
  224. package/harness/bundles/java/cursor/harness-review/SKILL.md +1 -1
  225. package/harness/bundles/java/cursor/harness-run/SKILL.md +1 -1
  226. package/harness/bundles/java/cursor/harness-submit/SKILL.md +1 -1
  227. package/harness/bundles/java/cursor/harness-sync/SKILL.md +1 -1
  228. package/harness/bundles/java/cursor/harness-test/SKILL.md +1 -1
  229. package/harness/bundles/java/cursor/scripts/harness_archive.py +282 -120
  230. package/harness/bundles/java/cursor/scripts/harness_events.py +400 -24
  231. package/harness/bundles/java/cursor/scripts/harness_gate.py +585 -21
  232. package/harness/bundles/java/cursor/scripts/harness_integration.py +403 -18
  233. package/harness/bundles/java/cursor/scripts/harness_ledger.py +225 -29
  234. package/harness/bundles/java/cursor/scripts/harness_phase.py +926 -0
  235. package/harness/bundles/java/cursor/scripts/harness_plan_finalize.py +361 -0
  236. package/harness/bundles/java/cursor/scripts/harness_profile.py +50 -0
  237. package/harness/bundles/java/cursor/scripts/harness_runtime.py +248 -0
  238. package/harness/bundles/java/cursor/scripts/harness_state.py +15 -2
  239. package/harness/bundles/java/cursor/scripts/harness_test_guard.py +188 -0
  240. package/harness/bundles/java/cursor/scripts/harness_workflow_policy.py +19 -0
  241. package/harness/manifests/general/claude-code.json +43 -27
  242. package/harness/manifests/general/codebuddy.json +43 -27
  243. package/harness/manifests/general/codex.json +43 -27
  244. package/harness/manifests/general/cursor.json +43 -27
  245. package/harness/manifests/java/claude-code.json +43 -27
  246. package/harness/manifests/java/codebuddy.json +43 -27
  247. package/harness/manifests/java/codex.json +43 -27
  248. package/harness/manifests/java/cursor.json +43 -27
  249. package/hunter-workflow-family.json +2 -2
  250. package/package.json +1 -1
@@ -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())