@hunter-harness/workflow-harness 0.3.1 → 0.4.1

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 (402) 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 +1 -1
  3. package/harness/bundles/general/claude-code/harness-archive/reference.md +30 -1
  4. package/harness/bundles/general/claude-code/harness-archive/templates/summary-data-template.json +1 -0
  5. package/harness/bundles/general/claude-code/harness-codebase-map/SKILL.md +1 -1
  6. package/harness/bundles/general/claude-code/harness-execute/SKILL.md +1 -1
  7. package/harness/bundles/general/claude-code/harness-knowledge-ingest/SKILL.md +1 -1
  8. package/harness/bundles/general/claude-code/harness-knowledge-query/SKILL.md +1 -1
  9. package/harness/bundles/general/claude-code/harness-plan/SKILL.md +1 -1
  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 +1 -1
  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 +146 -8
  18. package/harness/bundles/general/claude-code/scripts/harness_deploy.py +1 -1
  19. package/harness/bundles/general/claude-code/scripts/harness_knowledge_candidates.py +538 -4
  20. package/harness/bundles/general/codebuddy/.harness-build.json +1 -1
  21. package/harness/bundles/general/codebuddy/harness-archive/SKILL.md +1 -1
  22. package/harness/bundles/general/codebuddy/harness-archive/reference.md +30 -1
  23. package/harness/bundles/general/codebuddy/harness-archive/templates/summary-data-template.json +1 -0
  24. package/harness/bundles/general/codebuddy/harness-codebase-map/SKILL.md +1 -1
  25. package/harness/bundles/general/codebuddy/harness-execute/SKILL.md +1 -1
  26. package/harness/bundles/general/codebuddy/harness-knowledge-ingest/SKILL.md +1 -1
  27. package/harness/bundles/general/codebuddy/harness-knowledge-query/SKILL.md +1 -1
  28. package/harness/bundles/general/codebuddy/harness-plan/SKILL.md +1 -1
  29. package/harness/bundles/general/codebuddy/harness-pull/SKILL.md +1 -1
  30. package/harness/bundles/general/codebuddy/harness-push/SKILL.md +1 -1
  31. package/harness/bundles/general/codebuddy/harness-review/SKILL.md +1 -1
  32. package/harness/bundles/general/codebuddy/harness-run/SKILL.md +1 -1
  33. package/harness/bundles/general/codebuddy/harness-submit/SKILL.md +1 -1
  34. package/harness/bundles/general/codebuddy/harness-sync/SKILL.md +1 -1
  35. package/harness/bundles/general/codebuddy/harness-test/SKILL.md +1 -1
  36. package/harness/bundles/general/codebuddy/scripts/harness_archive.py +146 -8
  37. package/harness/bundles/general/codebuddy/scripts/harness_deploy.py +1 -1
  38. package/harness/bundles/general/codebuddy/scripts/harness_knowledge_candidates.py +538 -4
  39. package/harness/bundles/general/codex/.harness-build.json +1 -1
  40. package/harness/bundles/general/codex/harness-archive/SKILL.md +1 -1
  41. package/harness/bundles/general/codex/harness-archive/reference.md +30 -1
  42. package/harness/bundles/general/codex/harness-archive/templates/summary-data-template.json +1 -0
  43. package/harness/bundles/general/codex/harness-codebase-map/SKILL.md +1 -1
  44. package/harness/bundles/general/codex/harness-execute/SKILL.md +1 -1
  45. package/harness/bundles/general/codex/harness-knowledge-ingest/SKILL.md +1 -1
  46. package/harness/bundles/general/codex/harness-knowledge-query/SKILL.md +1 -1
  47. package/harness/bundles/general/codex/harness-plan/SKILL.md +1 -1
  48. package/harness/bundles/general/codex/harness-pull/SKILL.md +1 -1
  49. package/harness/bundles/general/codex/harness-push/SKILL.md +1 -1
  50. package/harness/bundles/general/codex/harness-review/SKILL.md +1 -1
  51. package/harness/bundles/general/codex/harness-run/SKILL.md +1 -1
  52. package/harness/bundles/general/codex/harness-submit/SKILL.md +1 -1
  53. package/harness/bundles/general/codex/harness-sync/SKILL.md +1 -1
  54. package/harness/bundles/general/codex/harness-test/SKILL.md +1 -1
  55. package/harness/bundles/general/codex/scripts/harness_archive.py +146 -8
  56. package/harness/bundles/general/codex/scripts/harness_deploy.py +1 -1
  57. package/harness/bundles/general/codex/scripts/harness_knowledge_candidates.py +538 -4
  58. package/harness/bundles/general/cursor/.harness-build.json +1 -1
  59. package/harness/bundles/general/cursor/harness-archive/SKILL.md +1 -1
  60. package/harness/bundles/general/cursor/harness-archive/reference.md +30 -1
  61. package/harness/bundles/general/cursor/harness-archive/templates/summary-data-template.json +1 -0
  62. package/harness/bundles/general/cursor/harness-codebase-map/SKILL.md +1 -1
  63. package/harness/bundles/general/cursor/harness-execute/SKILL.md +1 -1
  64. package/harness/bundles/general/cursor/harness-knowledge-ingest/SKILL.md +1 -1
  65. package/harness/bundles/general/cursor/harness-knowledge-query/SKILL.md +1 -1
  66. package/harness/bundles/general/cursor/harness-plan/SKILL.md +1 -1
  67. package/harness/bundles/general/cursor/harness-pull/SKILL.md +1 -1
  68. package/harness/bundles/general/cursor/harness-push/SKILL.md +1 -1
  69. package/harness/bundles/general/cursor/harness-review/SKILL.md +1 -1
  70. package/harness/bundles/general/cursor/harness-run/SKILL.md +1 -1
  71. package/harness/bundles/general/cursor/harness-submit/SKILL.md +1 -1
  72. package/harness/bundles/general/cursor/harness-sync/SKILL.md +1 -1
  73. package/harness/bundles/general/cursor/harness-test/SKILL.md +1 -1
  74. package/harness/bundles/general/cursor/scripts/harness_archive.py +146 -8
  75. package/harness/bundles/general/cursor/scripts/harness_deploy.py +1 -1
  76. package/harness/bundles/general/cursor/scripts/harness_knowledge_candidates.py +538 -4
  77. package/harness/bundles/general/pi/.harness-build.json +6 -0
  78. package/harness/bundles/general/pi/CONTEXT.md +98 -0
  79. package/harness/bundles/general/pi/README.md +478 -0
  80. package/harness/bundles/general/pi/contracts/build-profile-v3.schema.json +302 -0
  81. package/harness/bundles/general/pi/contracts/ci-metrics.schema.json +36 -0
  82. package/harness/bundles/general/pi/contracts/doctor-capability.schema.json +43 -0
  83. package/harness/bundles/general/pi/contracts/external-platform-observation.schema.json +62 -0
  84. package/harness/bundles/general/pi/contracts/fixtures/managed-execution.json +341 -0
  85. package/harness/bundles/general/pi/contracts/process-identity.schema.json +132 -0
  86. package/harness/bundles/general/pi/contracts/run-session.schema.json +142 -0
  87. package/harness/bundles/general/pi/contracts/service-retirement-receipt.schema.json +85 -0
  88. package/harness/bundles/general/pi/contracts/service-session.schema.json +147 -0
  89. package/harness/bundles/general/pi/contracts/workflow-policy.json +574 -0
  90. package/harness/bundles/general/pi/harness-archive/SKILL.md +322 -0
  91. package/harness/bundles/general/pi/harness-archive/checklist.md +71 -0
  92. package/harness/bundles/general/pi/harness-archive/reference.md +150 -0
  93. package/harness/bundles/general/pi/harness-archive/scripts/gen-manifest.ps1 +31 -0
  94. package/harness/bundles/general/pi/harness-archive/templates/summary-data-template.json +218 -0
  95. package/harness/bundles/general/pi/harness-codebase-map/SKILL.md +116 -0
  96. package/harness/bundles/general/pi/harness-codebase-map/checklist.md +126 -0
  97. package/harness/bundles/general/pi/harness-codebase-map/reference.md +364 -0
  98. package/harness/bundles/general/pi/harness-codebase-map/templates/ARCHITECTURE.md +57 -0
  99. package/harness/bundles/general/pi/harness-codebase-map/templates/CONCERNS.md +49 -0
  100. package/harness/bundles/general/pi/harness-codebase-map/templates/CONVENTIONS.md +57 -0
  101. package/harness/bundles/general/pi/harness-codebase-map/templates/INTEGRATIONS.md +52 -0
  102. package/harness/bundles/general/pi/harness-codebase-map/templates/STACK.md +55 -0
  103. package/harness/bundles/general/pi/harness-codebase-map/templates/STRUCTURE.md +69 -0
  104. package/harness/bundles/general/pi/harness-codebase-map/templates/TESTING.md +64 -0
  105. package/harness/bundles/general/pi/harness-codebase-map/templates/map-manifest.schema.json +73 -0
  106. package/harness/bundles/general/pi/harness-codebase-map/templates/map-summary.md +64 -0
  107. package/harness/bundles/general/pi/harness-execute/SKILL.md +170 -0
  108. package/harness/bundles/general/pi/harness-knowledge-ingest/SKILL.md +103 -0
  109. package/harness/bundles/general/pi/harness-knowledge-query/SKILL.md +97 -0
  110. package/harness/bundles/general/pi/harness-plan/SKILL.md +158 -0
  111. package/harness/bundles/general/pi/harness-plan/checklist.md +243 -0
  112. package/harness/bundles/general/pi/harness-plan/protocols.md +133 -0
  113. package/harness/bundles/general/pi/harness-plan/reference.md +468 -0
  114. package/harness/bundles/general/pi/harness-pull/SKILL.md +55 -0
  115. package/harness/bundles/general/pi/harness-push/SKILL.md +83 -0
  116. package/harness/bundles/general/pi/harness-review/SKILL.md +175 -0
  117. package/harness/bundles/general/pi/harness-review/checklist.md +124 -0
  118. package/harness/bundles/general/pi/harness-review/protocols.md +69 -0
  119. package/harness/bundles/general/pi/harness-review/reference.md +114 -0
  120. package/harness/bundles/general/pi/harness-run/SKILL.md +188 -0
  121. package/harness/bundles/general/pi/harness-run/checklist.md +279 -0
  122. package/harness/bundles/general/pi/harness-run/protocols.md +141 -0
  123. package/harness/bundles/general/pi/harness-run/reference.md +886 -0
  124. package/harness/bundles/general/pi/harness-submit/SKILL.md +182 -0
  125. package/harness/bundles/general/pi/harness-submit/checklist.md +452 -0
  126. package/harness/bundles/general/pi/harness-submit/reference.md +152 -0
  127. package/harness/bundles/general/pi/harness-sync/SKILL.md +72 -0
  128. package/harness/bundles/general/pi/harness-sync/reference.md +176 -0
  129. package/harness/bundles/general/pi/harness-test/SKILL.md +250 -0
  130. package/harness/bundles/general/pi/harness-test/checklist.md +359 -0
  131. package/harness/bundles/general/pi/harness-test/pitfalls-java.md +14 -0
  132. package/harness/bundles/general/pi/harness-test/pitfalls.md +307 -0
  133. package/harness/bundles/general/pi/harness-test/reference.md +880 -0
  134. package/harness/bundles/general/pi/harness-test/scripts/runtime-helpers.mjs +216 -0
  135. package/harness/bundles/general/pi/protocols/archive-report-protocol.md +196 -0
  136. package/harness/bundles/general/pi/protocols/ci-layering-protocol.md +22 -0
  137. package/harness/bundles/general/pi/protocols/evidence-based-reporting-protocol.md +134 -0
  138. package/harness/bundles/general/pi/protocols/execution-session-protocol.md +52 -0
  139. package/harness/bundles/general/pi/protocols/external-platform-protocol.md +27 -0
  140. package/harness/bundles/general/pi/protocols/ledger-protocol.md +365 -0
  141. package/harness/bundles/general/pi/protocols/memory-closeout-protocol.md +21 -0
  142. package/harness/bundles/general/pi/protocols/model-routing-protocol.md +22 -0
  143. package/harness/bundles/general/pi/protocols/powershell-protocol.md +238 -0
  144. package/harness/bundles/general/pi/protocols/registry-governance-protocol.md +31 -0
  145. package/harness/bundles/general/pi/protocols/report-pipeline-protocol.md +172 -0
  146. package/harness/bundles/general/pi/protocols/sensitive-info-protocol.md +110 -0
  147. package/harness/bundles/general/pi/protocols/state-layout-protocol.md +209 -0
  148. package/harness/bundles/general/pi/protocols/submit-protocol.md +78 -0
  149. package/harness/bundles/general/pi/protocols/verification-graph-protocol.md +32 -0
  150. package/harness/bundles/general/pi/scripts/harness_adoption_metrics.py +391 -0
  151. package/harness/bundles/general/pi/scripts/harness_apidoc.py +53 -0
  152. package/harness/bundles/general/pi/scripts/harness_archive.py +11345 -0
  153. package/harness/bundles/general/pi/scripts/harness_change.py +1626 -0
  154. package/harness/bundles/general/pi/scripts/harness_check_gate.py +113 -0
  155. package/harness/bundles/general/pi/scripts/harness_context.py +1774 -0
  156. package/harness/bundles/general/pi/scripts/harness_deploy.py +968 -0
  157. package/harness/bundles/general/pi/scripts/harness_efficiency.py +443 -0
  158. package/harness/bundles/general/pi/scripts/harness_environment.py +1690 -0
  159. package/harness/bundles/general/pi/scripts/harness_events.py +2234 -0
  160. package/harness/bundles/general/pi/scripts/harness_events_sync.py +1425 -0
  161. package/harness/bundles/general/pi/scripts/harness_execution_contracts.py +596 -0
  162. package/harness/bundles/general/pi/scripts/harness_fixback.py +1975 -0
  163. package/harness/bundles/general/pi/scripts/harness_gate.py +3816 -0
  164. package/harness/bundles/general/pi/scripts/harness_headless.py +37 -0
  165. package/harness/bundles/general/pi/scripts/harness_integration.py +2564 -0
  166. package/harness/bundles/general/pi/scripts/harness_knowledge_candidates.py +796 -0
  167. package/harness/bundles/general/pi/scripts/harness_ledger.py +3556 -0
  168. package/harness/bundles/general/pi/scripts/harness_migration_head.py +220 -0
  169. package/harness/bundles/general/pi/scripts/harness_orchestration.py +344 -0
  170. package/harness/bundles/general/pi/scripts/harness_paths.py +676 -0
  171. package/harness/bundles/general/pi/scripts/harness_phase.py +1726 -0
  172. package/harness/bundles/general/pi/scripts/harness_plan_aggregate.py +249 -0
  173. package/harness/bundles/general/pi/scripts/harness_plan_finalize.py +1016 -0
  174. package/harness/bundles/general/pi/scripts/harness_preflight.py +650 -0
  175. package/harness/bundles/general/pi/scripts/harness_process.py +1065 -0
  176. package/harness/bundles/general/pi/scripts/harness_profile.py +1817 -0
  177. package/harness/bundles/general/pi/scripts/harness_report_model.py +442 -0
  178. package/harness/bundles/general/pi/scripts/harness_retry.py +175 -0
  179. package/harness/bundles/general/pi/scripts/harness_review.py +631 -0
  180. package/harness/bundles/general/pi/scripts/harness_runtime.py +2243 -0
  181. package/harness/bundles/general/pi/scripts/harness_service.py +3355 -0
  182. package/harness/bundles/general/pi/scripts/harness_state.py +365 -0
  183. package/harness/bundles/general/pi/scripts/harness_sync.py +407 -0
  184. package/harness/bundles/general/pi/scripts/harness_test_cleanup.py +175 -0
  185. package/harness/bundles/general/pi/scripts/harness_test_guard.py +1899 -0
  186. package/harness/bundles/general/pi/scripts/harness_test_runner.py +1586 -0
  187. package/harness/bundles/general/pi/scripts/harness_verification.py +593 -0
  188. package/harness/bundles/general/pi/scripts/harness_workflow_policy.py +266 -0
  189. package/harness/bundles/java/claude-code/.harness-build.json +1 -1
  190. package/harness/bundles/java/claude-code/harness-apidoc/SKILL.md +1 -1
  191. package/harness/bundles/java/claude-code/harness-archive/SKILL.md +1 -1
  192. package/harness/bundles/java/claude-code/harness-archive/reference.md +30 -1
  193. package/harness/bundles/java/claude-code/harness-archive/templates/summary-data-template.json +1 -0
  194. package/harness/bundles/java/claude-code/harness-codebase-map/SKILL.md +1 -1
  195. package/harness/bundles/java/claude-code/harness-execute/SKILL.md +1 -1
  196. package/harness/bundles/java/claude-code/harness-knowledge-ingest/SKILL.md +1 -1
  197. package/harness/bundles/java/claude-code/harness-knowledge-query/SKILL.md +1 -1
  198. package/harness/bundles/java/claude-code/harness-package/SKILL.md +1 -1
  199. package/harness/bundles/java/claude-code/harness-plan/SKILL.md +1 -1
  200. package/harness/bundles/java/claude-code/harness-pull/SKILL.md +1 -1
  201. package/harness/bundles/java/claude-code/harness-push/SKILL.md +1 -1
  202. package/harness/bundles/java/claude-code/harness-review/SKILL.md +1 -1
  203. package/harness/bundles/java/claude-code/harness-run/SKILL.md +1 -1
  204. package/harness/bundles/java/claude-code/harness-submit/SKILL.md +1 -1
  205. package/harness/bundles/java/claude-code/harness-sync/SKILL.md +1 -1
  206. package/harness/bundles/java/claude-code/harness-test/SKILL.md +1 -1
  207. package/harness/bundles/java/claude-code/scripts/harness_archive.py +146 -8
  208. package/harness/bundles/java/claude-code/scripts/harness_deploy.py +1 -1
  209. package/harness/bundles/java/claude-code/scripts/harness_knowledge_candidates.py +538 -4
  210. package/harness/bundles/java/codebuddy/.harness-build.json +1 -1
  211. package/harness/bundles/java/codebuddy/harness-apidoc/SKILL.md +1 -1
  212. package/harness/bundles/java/codebuddy/harness-archive/SKILL.md +1 -1
  213. package/harness/bundles/java/codebuddy/harness-archive/reference.md +30 -1
  214. package/harness/bundles/java/codebuddy/harness-archive/templates/summary-data-template.json +1 -0
  215. package/harness/bundles/java/codebuddy/harness-codebase-map/SKILL.md +1 -1
  216. package/harness/bundles/java/codebuddy/harness-execute/SKILL.md +1 -1
  217. package/harness/bundles/java/codebuddy/harness-knowledge-ingest/SKILL.md +1 -1
  218. package/harness/bundles/java/codebuddy/harness-knowledge-query/SKILL.md +1 -1
  219. package/harness/bundles/java/codebuddy/harness-package/SKILL.md +1 -1
  220. package/harness/bundles/java/codebuddy/harness-plan/SKILL.md +1 -1
  221. package/harness/bundles/java/codebuddy/harness-pull/SKILL.md +1 -1
  222. package/harness/bundles/java/codebuddy/harness-push/SKILL.md +1 -1
  223. package/harness/bundles/java/codebuddy/harness-review/SKILL.md +1 -1
  224. package/harness/bundles/java/codebuddy/harness-run/SKILL.md +1 -1
  225. package/harness/bundles/java/codebuddy/harness-submit/SKILL.md +1 -1
  226. package/harness/bundles/java/codebuddy/harness-sync/SKILL.md +1 -1
  227. package/harness/bundles/java/codebuddy/harness-test/SKILL.md +1 -1
  228. package/harness/bundles/java/codebuddy/scripts/harness_archive.py +146 -8
  229. package/harness/bundles/java/codebuddy/scripts/harness_deploy.py +1 -1
  230. package/harness/bundles/java/codebuddy/scripts/harness_knowledge_candidates.py +538 -4
  231. package/harness/bundles/java/codex/.harness-build.json +1 -1
  232. package/harness/bundles/java/codex/harness-apidoc/SKILL.md +1 -1
  233. package/harness/bundles/java/codex/harness-archive/SKILL.md +1 -1
  234. package/harness/bundles/java/codex/harness-archive/reference.md +30 -1
  235. package/harness/bundles/java/codex/harness-archive/templates/summary-data-template.json +1 -0
  236. package/harness/bundles/java/codex/harness-codebase-map/SKILL.md +1 -1
  237. package/harness/bundles/java/codex/harness-execute/SKILL.md +1 -1
  238. package/harness/bundles/java/codex/harness-knowledge-ingest/SKILL.md +1 -1
  239. package/harness/bundles/java/codex/harness-knowledge-query/SKILL.md +1 -1
  240. package/harness/bundles/java/codex/harness-package/SKILL.md +1 -1
  241. package/harness/bundles/java/codex/harness-plan/SKILL.md +1 -1
  242. package/harness/bundles/java/codex/harness-pull/SKILL.md +1 -1
  243. package/harness/bundles/java/codex/harness-push/SKILL.md +1 -1
  244. package/harness/bundles/java/codex/harness-review/SKILL.md +1 -1
  245. package/harness/bundles/java/codex/harness-run/SKILL.md +1 -1
  246. package/harness/bundles/java/codex/harness-submit/SKILL.md +1 -1
  247. package/harness/bundles/java/codex/harness-sync/SKILL.md +1 -1
  248. package/harness/bundles/java/codex/harness-test/SKILL.md +1 -1
  249. package/harness/bundles/java/codex/scripts/harness_archive.py +146 -8
  250. package/harness/bundles/java/codex/scripts/harness_deploy.py +1 -1
  251. package/harness/bundles/java/codex/scripts/harness_knowledge_candidates.py +538 -4
  252. package/harness/bundles/java/cursor/.harness-build.json +1 -1
  253. package/harness/bundles/java/cursor/harness-apidoc/SKILL.md +1 -1
  254. package/harness/bundles/java/cursor/harness-archive/SKILL.md +1 -1
  255. package/harness/bundles/java/cursor/harness-archive/reference.md +30 -1
  256. package/harness/bundles/java/cursor/harness-archive/templates/summary-data-template.json +1 -0
  257. package/harness/bundles/java/cursor/harness-codebase-map/SKILL.md +1 -1
  258. package/harness/bundles/java/cursor/harness-execute/SKILL.md +1 -1
  259. package/harness/bundles/java/cursor/harness-knowledge-ingest/SKILL.md +1 -1
  260. package/harness/bundles/java/cursor/harness-knowledge-query/SKILL.md +1 -1
  261. package/harness/bundles/java/cursor/harness-package/SKILL.md +1 -1
  262. package/harness/bundles/java/cursor/harness-plan/SKILL.md +1 -1
  263. package/harness/bundles/java/cursor/harness-pull/SKILL.md +1 -1
  264. package/harness/bundles/java/cursor/harness-push/SKILL.md +1 -1
  265. package/harness/bundles/java/cursor/harness-review/SKILL.md +1 -1
  266. package/harness/bundles/java/cursor/harness-run/SKILL.md +1 -1
  267. package/harness/bundles/java/cursor/harness-submit/SKILL.md +1 -1
  268. package/harness/bundles/java/cursor/harness-sync/SKILL.md +1 -1
  269. package/harness/bundles/java/cursor/harness-test/SKILL.md +1 -1
  270. package/harness/bundles/java/cursor/scripts/harness_archive.py +146 -8
  271. package/harness/bundles/java/cursor/scripts/harness_deploy.py +1 -1
  272. package/harness/bundles/java/cursor/scripts/harness_knowledge_candidates.py +538 -4
  273. package/harness/bundles/java/pi/.harness-build.json +6 -0
  274. package/harness/bundles/java/pi/CONTEXT.md +98 -0
  275. package/harness/bundles/java/pi/README.md +478 -0
  276. package/harness/bundles/java/pi/contracts/build-profile-v3.schema.json +302 -0
  277. package/harness/bundles/java/pi/contracts/ci-metrics.schema.json +36 -0
  278. package/harness/bundles/java/pi/contracts/doctor-capability.schema.json +43 -0
  279. package/harness/bundles/java/pi/contracts/external-platform-observation.schema.json +62 -0
  280. package/harness/bundles/java/pi/contracts/fixtures/managed-execution.json +341 -0
  281. package/harness/bundles/java/pi/contracts/process-identity.schema.json +132 -0
  282. package/harness/bundles/java/pi/contracts/run-session.schema.json +142 -0
  283. package/harness/bundles/java/pi/contracts/service-retirement-receipt.schema.json +85 -0
  284. package/harness/bundles/java/pi/contracts/service-session.schema.json +147 -0
  285. package/harness/bundles/java/pi/contracts/workflow-policy.json +574 -0
  286. package/harness/bundles/java/pi/harness-apidoc/SKILL.md +97 -0
  287. package/harness/bundles/java/pi/harness-apidoc/checklist.md +142 -0
  288. package/harness/bundles/java/pi/harness-apidoc/reference.md +233 -0
  289. package/harness/bundles/java/pi/harness-archive/SKILL.md +322 -0
  290. package/harness/bundles/java/pi/harness-archive/checklist.md +71 -0
  291. package/harness/bundles/java/pi/harness-archive/reference.md +150 -0
  292. package/harness/bundles/java/pi/harness-archive/scripts/gen-manifest.ps1 +31 -0
  293. package/harness/bundles/java/pi/harness-archive/templates/summary-data-template.json +218 -0
  294. package/harness/bundles/java/pi/harness-codebase-map/SKILL.md +116 -0
  295. package/harness/bundles/java/pi/harness-codebase-map/checklist.md +126 -0
  296. package/harness/bundles/java/pi/harness-codebase-map/reference.md +364 -0
  297. package/harness/bundles/java/pi/harness-codebase-map/templates/ARCHITECTURE.md +57 -0
  298. package/harness/bundles/java/pi/harness-codebase-map/templates/CONCERNS.md +49 -0
  299. package/harness/bundles/java/pi/harness-codebase-map/templates/CONVENTIONS.md +57 -0
  300. package/harness/bundles/java/pi/harness-codebase-map/templates/INTEGRATIONS.md +52 -0
  301. package/harness/bundles/java/pi/harness-codebase-map/templates/STACK.md +55 -0
  302. package/harness/bundles/java/pi/harness-codebase-map/templates/STRUCTURE.md +69 -0
  303. package/harness/bundles/java/pi/harness-codebase-map/templates/TESTING.md +64 -0
  304. package/harness/bundles/java/pi/harness-codebase-map/templates/map-manifest.schema.json +73 -0
  305. package/harness/bundles/java/pi/harness-codebase-map/templates/map-summary.md +64 -0
  306. package/harness/bundles/java/pi/harness-execute/SKILL.md +170 -0
  307. package/harness/bundles/java/pi/harness-knowledge-ingest/SKILL.md +103 -0
  308. package/harness/bundles/java/pi/harness-knowledge-query/SKILL.md +97 -0
  309. package/harness/bundles/java/pi/harness-package/SKILL.md +94 -0
  310. package/harness/bundles/java/pi/harness-package/checklist.md +322 -0
  311. package/harness/bundles/java/pi/harness-package/reference.md +210 -0
  312. package/harness/bundles/java/pi/harness-plan/SKILL.md +158 -0
  313. package/harness/bundles/java/pi/harness-plan/checklist.md +243 -0
  314. package/harness/bundles/java/pi/harness-plan/protocols.md +133 -0
  315. package/harness/bundles/java/pi/harness-plan/reference.md +468 -0
  316. package/harness/bundles/java/pi/harness-pull/SKILL.md +55 -0
  317. package/harness/bundles/java/pi/harness-push/SKILL.md +83 -0
  318. package/harness/bundles/java/pi/harness-review/SKILL.md +175 -0
  319. package/harness/bundles/java/pi/harness-review/checklist.md +124 -0
  320. package/harness/bundles/java/pi/harness-review/protocols.md +69 -0
  321. package/harness/bundles/java/pi/harness-review/reference.md +114 -0
  322. package/harness/bundles/java/pi/harness-run/SKILL.md +206 -0
  323. package/harness/bundles/java/pi/harness-run/checklist.md +252 -0
  324. package/harness/bundles/java/pi/harness-run/protocols.md +141 -0
  325. package/harness/bundles/java/pi/harness-run/reference.md +820 -0
  326. package/harness/bundles/java/pi/harness-submit/SKILL.md +189 -0
  327. package/harness/bundles/java/pi/harness-submit/checklist.md +452 -0
  328. package/harness/bundles/java/pi/harness-submit/reference.md +152 -0
  329. package/harness/bundles/java/pi/harness-sync/SKILL.md +72 -0
  330. package/harness/bundles/java/pi/harness-sync/reference.md +176 -0
  331. package/harness/bundles/java/pi/harness-test/SKILL.md +264 -0
  332. package/harness/bundles/java/pi/harness-test/checklist.md +320 -0
  333. package/harness/bundles/java/pi/harness-test/pitfalls-java.md +16 -0
  334. package/harness/bundles/java/pi/harness-test/pitfalls.md +307 -0
  335. package/harness/bundles/java/pi/harness-test/reference.md +792 -0
  336. package/harness/bundles/java/pi/harness-test/scripts/runtime-helpers.mjs +216 -0
  337. package/harness/bundles/java/pi/protocols/archive-report-protocol.md +196 -0
  338. package/harness/bundles/java/pi/protocols/ci-layering-protocol.md +22 -0
  339. package/harness/bundles/java/pi/protocols/evidence-based-reporting-protocol.md +134 -0
  340. package/harness/bundles/java/pi/protocols/execution-session-protocol.md +52 -0
  341. package/harness/bundles/java/pi/protocols/external-platform-protocol.md +27 -0
  342. package/harness/bundles/java/pi/protocols/ledger-protocol.md +365 -0
  343. package/harness/bundles/java/pi/protocols/memory-closeout-protocol.md +21 -0
  344. package/harness/bundles/java/pi/protocols/model-routing-protocol.md +22 -0
  345. package/harness/bundles/java/pi/protocols/powershell-protocol.md +238 -0
  346. package/harness/bundles/java/pi/protocols/registry-governance-protocol.md +31 -0
  347. package/harness/bundles/java/pi/protocols/report-pipeline-protocol.md +172 -0
  348. package/harness/bundles/java/pi/protocols/sensitive-info-protocol.md +110 -0
  349. package/harness/bundles/java/pi/protocols/state-layout-protocol.md +209 -0
  350. package/harness/bundles/java/pi/protocols/submit-protocol.md +78 -0
  351. package/harness/bundles/java/pi/protocols/verification-graph-protocol.md +32 -0
  352. package/harness/bundles/java/pi/scripts/harness_adoption_metrics.py +391 -0
  353. package/harness/bundles/java/pi/scripts/harness_apidoc.py +53 -0
  354. package/harness/bundles/java/pi/scripts/harness_archive.py +11345 -0
  355. package/harness/bundles/java/pi/scripts/harness_change.py +1626 -0
  356. package/harness/bundles/java/pi/scripts/harness_check_gate.py +113 -0
  357. package/harness/bundles/java/pi/scripts/harness_context.py +1774 -0
  358. package/harness/bundles/java/pi/scripts/harness_deploy.py +968 -0
  359. package/harness/bundles/java/pi/scripts/harness_efficiency.py +443 -0
  360. package/harness/bundles/java/pi/scripts/harness_environment.py +1690 -0
  361. package/harness/bundles/java/pi/scripts/harness_events.py +2234 -0
  362. package/harness/bundles/java/pi/scripts/harness_events_sync.py +1425 -0
  363. package/harness/bundles/java/pi/scripts/harness_execution_contracts.py +596 -0
  364. package/harness/bundles/java/pi/scripts/harness_fixback.py +1975 -0
  365. package/harness/bundles/java/pi/scripts/harness_gate.py +3816 -0
  366. package/harness/bundles/java/pi/scripts/harness_headless.py +37 -0
  367. package/harness/bundles/java/pi/scripts/harness_integration.py +2564 -0
  368. package/harness/bundles/java/pi/scripts/harness_knowledge_candidates.py +796 -0
  369. package/harness/bundles/java/pi/scripts/harness_ledger.py +3556 -0
  370. package/harness/bundles/java/pi/scripts/harness_migration_head.py +220 -0
  371. package/harness/bundles/java/pi/scripts/harness_orchestration.py +344 -0
  372. package/harness/bundles/java/pi/scripts/harness_paths.py +676 -0
  373. package/harness/bundles/java/pi/scripts/harness_phase.py +1726 -0
  374. package/harness/bundles/java/pi/scripts/harness_plan_aggregate.py +249 -0
  375. package/harness/bundles/java/pi/scripts/harness_plan_finalize.py +1016 -0
  376. package/harness/bundles/java/pi/scripts/harness_preflight.py +650 -0
  377. package/harness/bundles/java/pi/scripts/harness_process.py +1065 -0
  378. package/harness/bundles/java/pi/scripts/harness_profile.py +1817 -0
  379. package/harness/bundles/java/pi/scripts/harness_report_model.py +442 -0
  380. package/harness/bundles/java/pi/scripts/harness_retry.py +175 -0
  381. package/harness/bundles/java/pi/scripts/harness_review.py +631 -0
  382. package/harness/bundles/java/pi/scripts/harness_runtime.py +2243 -0
  383. package/harness/bundles/java/pi/scripts/harness_service.py +3355 -0
  384. package/harness/bundles/java/pi/scripts/harness_state.py +365 -0
  385. package/harness/bundles/java/pi/scripts/harness_sync.py +407 -0
  386. package/harness/bundles/java/pi/scripts/harness_test_cleanup.py +175 -0
  387. package/harness/bundles/java/pi/scripts/harness_test_guard.py +1899 -0
  388. package/harness/bundles/java/pi/scripts/harness_test_runner.py +1586 -0
  389. package/harness/bundles/java/pi/scripts/harness_verification.py +593 -0
  390. package/harness/bundles/java/pi/scripts/harness_workflow_policy.py +266 -0
  391. package/harness/manifests/general/claude-code.json +19 -19
  392. package/harness/manifests/general/codebuddy.json +19 -19
  393. package/harness/manifests/general/codex.json +19 -19
  394. package/harness/manifests/general/cursor.json +19 -19
  395. package/harness/manifests/general/pi.json +475 -0
  396. package/harness/manifests/java/claude-code.json +21 -21
  397. package/harness/manifests/java/codebuddy.json +21 -21
  398. package/harness/manifests/java/codex.json +21 -21
  399. package/harness/manifests/java/cursor.json +21 -21
  400. package/harness/manifests/java/pi.json +499 -0
  401. package/hunter-workflow-family.json +2 -2
  402. package/package.json +1 -1
@@ -0,0 +1,968 @@
1
+ #!/usr/bin/env python3
2
+ """Harness deploy: build core + overlay into self-contained skills tree (D9/D12).
3
+
4
+ Python 3.10+ stdlib only. UTF-8 without BOM.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import argparse
10
+ import datetime as dt
11
+ import filecmp
12
+ import hashlib
13
+ import json
14
+ import os
15
+ import re
16
+ import shutil
17
+ import subprocess
18
+ import sys
19
+ import time
20
+ import uuid
21
+ from pathlib import Path
22
+ from typing import Any, Iterable
23
+
24
+ if hasattr(sys.stdout, "reconfigure"):
25
+ sys.stdout.reconfigure(encoding="utf-8")
26
+ if hasattr(sys.stderr, "reconfigure"):
27
+ sys.stderr.reconfigure(encoding="utf-8")
28
+
29
+ INCLUDE_RE = re.compile(r"<!--\s*@include\s+shared/([^\s]+)\s*-->")
30
+ # retro §5.17: wiki links [[shared/xxx.md|alias]] reference shared fragments
31
+ # that are inlined by expand_includes. After expansion, replace the wiki link
32
+ # with the alias text (the shared content is already in the document).
33
+ WIKI_LINK_RE = re.compile(r"\[\[shared/([^\]|]+)\|([^\]]*)\]\]")
34
+ # §7.5: overlay anchors support both new section-id and legacy section:"title".
35
+ OVERRIDE_RE = re.compile(r'<!--\s*@override\s+(?:section-id:"([^"]+)"|section:"([^"]+)")\s*-->')
36
+ APPEND_RE = re.compile(r'<!--\s*@append-after\s+(?:section-id:"([^"]+)"|section:"([^"]+)")\s*-->')
37
+ SECTION_ID_RE = re.compile(r"<!--\s*@section-id\s+([\w.-]+)\s*-->")
38
+ HEADING_RE = re.compile(r"^(#{2,6})\s+(.+?)\s*$")
39
+ FRAGMENT_HINT_RE = re.compile(r"^>\s*片段:\[\[shared/[^\]]+\]\]\s*.*$")
40
+
41
+ BUILD_MARKER = ".harness-build.json"
42
+ _REPLACE_RETRY_DELAYS_SECONDS = (0.05, 0.1, 0.2, 0.4, 0.8)
43
+ # Install-side managed manifests record which files a prior install owned, so a
44
+ # later conservative install can delete removed managed files while preserving
45
+ # user-owned files (design §3.8 要点4). Skills manifest lives in the skills
46
+ # target dir; agents manifest lives in .claude/agents.
47
+ MANIFEST_NAME = ".harness-managed.json"
48
+ AGENTS_MANAGED_NAME = ".harness-managed-agents.json"
49
+ INSTALL_METADATA = frozenset({MANIFEST_NAME, AGENTS_MANAGED_NAME})
50
+
51
+
52
+ def _read_managed_set(path: Path) -> set[str]:
53
+ """Return the ``managedFiles`` set from a prior install manifest, or empty."""
54
+ if not path.is_file():
55
+ return set()
56
+ try:
57
+ data = json.loads(path.read_text(encoding="utf-8"))
58
+ except (json.JSONDecodeError, ValueError):
59
+ return set()
60
+ files = data.get("managedFiles") if isinstance(data, dict) else None
61
+ return set(files) if isinstance(files, list) else set()
62
+
63
+ SKIP_DIR_NAMES = {
64
+ "__pycache__",
65
+ ".pytest_cache",
66
+ "redesign",
67
+ "shared",
68
+ "overlays",
69
+ "adapters",
70
+ ".git",
71
+ }
72
+ SKIP_TOP_NAMES = {"harness-merge", "harness-report"}
73
+ HARNESS_AGENTS = ("claude-code", "codex", "cursor", "codebuddy", "pi")
74
+ AGENTS_WITH_CUSTOM_AGENTS = frozenset({"claude-code", "codebuddy"})
75
+
76
+
77
+ def now_iso() -> str:
78
+ return dt.datetime.now().astimezone().isoformat(timespec="seconds")
79
+
80
+
81
+ def replace_with_retry(source: Path, target: Path) -> None:
82
+ """Bounded retry for transient directory locks during atomic swaps.
83
+
84
+ Windows indexers and antivirus scanners may briefly hold a newly generated
85
+ bundle directory. Retrying only ``PermissionError`` keeps real path and
86
+ contract failures fail-fast while avoiding an expensive full bundle rerun.
87
+ """
88
+ for attempt in range(len(_REPLACE_RETRY_DELAYS_SECONDS) + 1):
89
+ try:
90
+ os.replace(source, target)
91
+ return
92
+ except PermissionError:
93
+ if attempt >= len(_REPLACE_RETRY_DELAYS_SECONDS):
94
+ raise
95
+ time.sleep(_REPLACE_RETRY_DELAYS_SECONDS[attempt])
96
+
97
+
98
+ def emit_json(payload: dict[str, Any], *, ok: bool = True) -> int:
99
+ print(json.dumps(payload, ensure_ascii=False, indent=2))
100
+ return 0 if ok else 1
101
+
102
+
103
+ def normalize_section_title(title: str) -> str:
104
+ t = title.strip()
105
+ t = re.sub(r"\s*⚠️.*$", "", t)
106
+ t = re.sub(r"\s*([^)]*)\s*$", "", t)
107
+ return t.strip()
108
+
109
+
110
+ def sha256_file(path: Path) -> str:
111
+ h = hashlib.sha256()
112
+ with path.open("rb") as fh:
113
+ for chunk in iter(lambda: fh.read(65536), b""):
114
+ h.update(chunk)
115
+ return h.hexdigest()
116
+
117
+
118
+ def iter_copy_entries(skills_root: Path, agent: str = "claude-code") -> Iterable[Path]:
119
+ for entry in sorted(skills_root.iterdir()):
120
+ if entry.name in SKIP_TOP_NAMES:
121
+ continue
122
+ if entry.is_dir() and entry.name in SKIP_DIR_NAMES:
123
+ continue
124
+ if entry.is_dir() and entry.name == "agents" and agent not in AGENTS_WITH_CUSTOM_AGENTS:
125
+ continue
126
+ if entry.is_file() and entry.name.startswith("."):
127
+ continue
128
+ yield entry
129
+
130
+
131
+ RUNTIME_IGNORE = shutil.ignore_patterns(
132
+ "tests", "__pycache__", "*.pyc", ".pytest_cache", "_last_run.txt", "_same_proc_*"
133
+ )
134
+ RUNTIME_SKIP_FILES = {"_last_run.txt", "_same_proc_pid.txt", "_same_proc_tmp.txt"}
135
+
136
+
137
+ def copy_tree(skills_root: Path, out_dir: Path, agent: str = "claude-code") -> list[str]:
138
+ copied: list[str] = []
139
+ out_dir.mkdir(parents=True, exist_ok=True)
140
+ for entry in iter_copy_entries(skills_root, agent):
141
+ dest = out_dir / entry.name
142
+ if entry.is_dir():
143
+ if entry.name == "scripts":
144
+ # §7.4: runtime keeps only scripts/*.py; scripts/tests/ excluded.
145
+ # harness_acceptance.py is an acceptance meta-tool (not a runtime
146
+ # skill script) and is excluded from the deployed tree.
147
+ dest.mkdir(parents=True, exist_ok=True)
148
+ for py in sorted(entry.glob("*.py")):
149
+ if py.name == "harness_acceptance.py":
150
+ continue
151
+ shutil.copy2(py, dest / py.name)
152
+ copied.append(f"scripts/{py.name}")
153
+ else:
154
+ shutil.copytree(entry, dest, ignore=RUNTIME_IGNORE)
155
+ copied.append(entry.name + "/")
156
+ else:
157
+ if entry.name in RUNTIME_SKIP_FILES or entry.name.endswith(".pyc"):
158
+ continue
159
+ shutil.copy2(entry, dest)
160
+ copied.append(entry.name)
161
+ return copied
162
+
163
+
164
+ def expand_includes(text: str, shared_dir: Path) -> str:
165
+ def repl(match: re.Match[str]) -> str:
166
+ rel = match.group(1)
167
+ inc_path = shared_dir / rel
168
+ if not inc_path.is_file():
169
+ raise FileNotFoundError(f"shared include not found: {rel}")
170
+ body = inc_path.read_text(encoding="utf-8").rstrip()
171
+ return body
172
+
173
+ expanded = INCLUDE_RE.sub(repl, text)
174
+ # §5.17: replace [[shared/xxx.md|alias]] wiki links with the alias text,
175
+ # since the shared content has been inlined by @include expansion above.
176
+ expanded = WIKI_LINK_RE.sub(lambda m: m.group(2), expanded)
177
+ lines = [ln for ln in expanded.splitlines() if not FRAGMENT_HINT_RE.match(ln)]
178
+ return "\n".join(lines) + ("\n" if text.endswith("\n") else "")
179
+
180
+
181
+ def find_section_span(lines: list[str], section: str) -> tuple[int, int, int]:
182
+ target = normalize_section_title(section)
183
+ start = end = level = -1
184
+ for i, line in enumerate(lines):
185
+ m = HEADING_RE.match(line)
186
+ if not m:
187
+ continue
188
+ title = normalize_section_title(m.group(2))
189
+ if title == target or title.startswith(target):
190
+ start = i
191
+ level = len(m.group(1))
192
+ break
193
+ if start < 0:
194
+ raise KeyError(section)
195
+ end = len(lines)
196
+ for j in range(start + 1, len(lines)):
197
+ m = HEADING_RE.match(lines[j])
198
+ if m and len(m.group(1)) <= level:
199
+ end = j
200
+ break
201
+ return start, end, level
202
+
203
+
204
+ def parse_section_ids(text: str) -> dict[str, tuple[int, int, int]]:
205
+ """Parse ``<!-- @section-id name -->`` markers; map id -> (start, end, level)
206
+ of the heading that follows. Raise ValueError on duplicate ids (§7.5)."""
207
+ lines = text.splitlines()
208
+ ids: dict[str, tuple[int, int, int]] = {}
209
+ pending: str | None = None
210
+ for i, line in enumerate(lines):
211
+ m = SECTION_ID_RE.search(line)
212
+ if m:
213
+ sid = m.group(1)
214
+ if sid in ids:
215
+ raise ValueError(f"duplicate section-id: {sid}")
216
+ pending = sid
217
+ continue
218
+ hm = HEADING_RE.match(line)
219
+ if hm and pending is not None:
220
+ level = len(hm.group(1))
221
+ start = i
222
+ end = len(lines)
223
+ for j in range(i + 1, len(lines)):
224
+ m2 = HEADING_RE.match(lines[j])
225
+ if m2 and len(m2.group(1)) <= level:
226
+ end = j
227
+ break
228
+ ids[pending] = (start, end, level)
229
+ pending = None
230
+ return ids
231
+
232
+
233
+ def apply_overlay_blocks(
234
+ core_text: str, overlay_text: str, section_ids: dict[str, tuple[int, int, int]] | None = None
235
+ ) -> str:
236
+ lines = core_text.splitlines()
237
+ pos = 0
238
+ while pos < len(overlay_text):
239
+ override = OVERRIDE_RE.search(overlay_text, pos)
240
+ append = APPEND_RE.search(overlay_text, pos)
241
+ candidates = [(m.start(), "override", m) for m in [override] if m]
242
+ candidates += [(m.start(), "append", m) for m in [append] if m]
243
+ if not candidates:
244
+ break
245
+ candidates.sort(key=lambda x: x[0])
246
+ _, kind, match = candidates[0]
247
+ section_id = match.group(1) # new style, may be None
248
+ section_title = match.group(2) # legacy style, may be None
249
+ content_start = match.end()
250
+ next_marker = min(
251
+ (
252
+ m.start()
253
+ for m in (
254
+ OVERRIDE_RE.search(overlay_text, content_start),
255
+ APPEND_RE.search(overlay_text, content_start),
256
+ )
257
+ if m
258
+ ),
259
+ default=len(overlay_text),
260
+ )
261
+ block = overlay_text[content_start:next_marker].strip("\n")
262
+ if section_id:
263
+ if not section_ids or section_id not in section_ids:
264
+ raise KeyError(f"section-id:{section_id}")
265
+ start, end, _ = section_ids[section_id]
266
+ else:
267
+ start, end, _ = find_section_span(lines, section_title)
268
+ if kind == "override":
269
+ replacement = block.splitlines()
270
+ lines = lines[:start] + replacement + lines[end:]
271
+ else:
272
+ insertion = block.splitlines()
273
+ lines = lines[:end] + insertion + lines[end:]
274
+ pos = next_marker
275
+ return "\n".join(lines) + ("\n" if core_text.endswith("\n") else "")
276
+
277
+
278
+ def _is_relative_to(child: Path, parent: Path) -> bool:
279
+ try:
280
+ child.relative_to(parent)
281
+ return True
282
+ except ValueError:
283
+ return False
284
+
285
+
286
+ def validate_build_paths(skills_root: Path, out_dir: Path) -> tuple[Path, Path]:
287
+ """Return resolved safe paths; raise ValueError for every forbidden relation.
288
+
289
+ §7.1: refuse out==source, out as ancestor of source, out inside source,
290
+ an existing non-empty out without the build marker, and obvious danger
291
+ dirs (user home).
292
+ """
293
+ skills_root = skills_root.resolve()
294
+ out_dir = out_dir.resolve()
295
+
296
+ if out_dir == skills_root:
297
+ raise ValueError(f"out_dir must not equal skills_root: {out_dir}")
298
+ if _is_relative_to(skills_root, out_dir):
299
+ raise ValueError(f"out_dir must not be an ancestor of skills_root: {out_dir}")
300
+ if _is_relative_to(out_dir, skills_root):
301
+ raise ValueError(f"out_dir must not be inside skills_root: {out_dir}")
302
+ try:
303
+ home = Path.home().resolve()
304
+ except OSError:
305
+ home = None
306
+ if home is not None and out_dir == home:
307
+ raise ValueError(f"out_dir must not be the user home: {out_dir}")
308
+
309
+ if out_dir.exists() and out_dir.is_dir():
310
+ has_marker = (out_dir / BUILD_MARKER).is_file()
311
+ has_user_files = any(p.name != BUILD_MARKER for p in out_dir.iterdir())
312
+ if has_user_files and not has_marker:
313
+ raise ValueError(
314
+ f"out_dir exists with user files and no {BUILD_MARKER} marker: {out_dir}"
315
+ )
316
+ return skills_root, out_dir
317
+
318
+
319
+ def core_content_hash(
320
+ skills_root: Path,
321
+ overlay_dir: Path | None,
322
+ agent_overlay_dir: Path | None = None,
323
+ ) -> str:
324
+ """SHA-256 over the actual core/shared/protocol/script/overlay files that
325
+ participate in the build (path-relative + content). Deterministic across
326
+ time and machine paths (§7.3)."""
327
+ h = hashlib.sha256()
328
+ files: list[Path] = []
329
+ # Hash exactly the runtime source universe copied by copy_tree, including
330
+ # references/templates/checklists rather than only SKILL.md files.
331
+ for entry in iter_copy_entries(skills_root):
332
+ if entry.is_file():
333
+ if entry.name not in RUNTIME_SKIP_FILES and not entry.name.endswith(".pyc"):
334
+ files.append(entry)
335
+ elif entry.name == "scripts":
336
+ files.extend(p for p in entry.glob("*.py") if p.name != "harness_acceptance.py")
337
+ else:
338
+ files.extend(
339
+ p for p in entry.rglob("*")
340
+ if p.is_file() and "__pycache__" not in p.parts and "tests" not in p.parts
341
+ and not p.name.endswith(".pyc") and p.name not in RUNTIME_SKIP_FILES
342
+ )
343
+ if overlay_dir and overlay_dir.is_dir():
344
+ files.extend(p for p in overlay_dir.rglob("*") if p.is_file())
345
+ if agent_overlay_dir and agent_overlay_dir.is_dir():
346
+ files.extend(p for p in agent_overlay_dir.rglob("*") if p.is_file())
347
+ skills_root_resolved = skills_root.resolve()
348
+ files_by_relative_path = {
349
+ f.resolve().relative_to(skills_root_resolved).as_posix(): f
350
+ for f in files
351
+ }
352
+ for rel in sorted(files_by_relative_path):
353
+ f = files_by_relative_path[rel]
354
+ h.update(rel.encode("utf-8"))
355
+ h.update(b"\0")
356
+ # Hash the actual build input with one algorithm regardless of whether the
357
+ # source happens to live inside a Git checkout. Relative POSIX-path sorting
358
+ # keeps the order identical on case-insensitive Windows and Linux.
359
+ h.update(sha256_file(f).encode("ascii"))
360
+ h.update(b"\0")
361
+ return h.hexdigest()[:16]
362
+
363
+
364
+ def synthesis_header(
365
+ skills_root: Path,
366
+ overlay_dir: Path | None,
367
+ agent: str = "claude-code",
368
+ agent_overlay_dir: Path | None = None,
369
+ ) -> str:
370
+ """Deterministic synth header: no absolute path, no timestamp (§7.3)."""
371
+ core_hash = core_content_hash(skills_root, overlay_dir, agent_overlay_dir)
372
+ overlay_name = overlay_dir.name if overlay_dir else "none"
373
+ return (
374
+ f"<!-- generated by harness_deploy.py; core={core_hash}; "
375
+ f"overlay={overlay_name}; agent={agent}; do not edit -->\n"
376
+ )
377
+
378
+
379
+ def inject_header(text: str, header: str) -> str:
380
+ if text.startswith("---"):
381
+ parts = text.split("---", 2)
382
+ if len(parts) >= 3:
383
+ body = parts[2].lstrip("\n")
384
+ return f"---{parts[1]}---\n{header}{body}"
385
+ return header + text
386
+
387
+
388
+ def process_skill_md(
389
+ path: Path,
390
+ shared_dir: Path,
391
+ overlay_path: Path | None,
392
+ header: str,
393
+ agent_overlay_path: Path | None = None,
394
+ ) -> None:
395
+ text = path.read_text(encoding="utf-8")
396
+ text = expand_includes(text, shared_dir)
397
+ section_ids = parse_section_ids(text) # §7.5: raises on duplicate section-id
398
+ if overlay_path and overlay_path.is_file():
399
+ overlay = overlay_path.read_text(encoding="utf-8")
400
+ text = apply_overlay_blocks(text, overlay, section_ids)
401
+ section_ids = parse_section_ids(text)
402
+ if agent_overlay_path and agent_overlay_path.is_file():
403
+ agent_overlay = agent_overlay_path.read_text(encoding="utf-8")
404
+ text = apply_overlay_blocks(text, agent_overlay, section_ids)
405
+ text = inject_header(text, header)
406
+ if INCLUDE_RE.search(text):
407
+ raise RuntimeError(f"unexpanded include placeholder remains in {path}")
408
+ path.write_text(text, encoding="utf-8", newline="\n")
409
+
410
+
411
+ def overlay_for_skill(overlay_dir: Path, skill_name: str) -> Path | None:
412
+ candidate = overlay_dir / f"{skill_name}.overlay.md"
413
+ return candidate if candidate.is_file() else None
414
+
415
+
416
+ def copy_adapter_agents(adapter_agents_dir: Path, staging: Path) -> list[str]:
417
+ """Copy adapters/<agent>/agents/*.md over staging/agents/."""
418
+ if not adapter_agents_dir.is_dir():
419
+ return []
420
+ dest = staging / "agents"
421
+ dest.mkdir(parents=True, exist_ok=True)
422
+ copied: list[str] = []
423
+ for agent_file in sorted(adapter_agents_dir.glob("*.md")):
424
+ shutil.copy2(agent_file, dest / agent_file.name)
425
+ copied.append(f"agents/{agent_file.name}")
426
+ return copied
427
+
428
+
429
+ def copy_overlay_unique_skills(overlay_dir: Path, out_dir: Path) -> list[str]:
430
+ added: list[str] = []
431
+ for entry in sorted(overlay_dir.iterdir()):
432
+ if not entry.is_dir():
433
+ continue
434
+ if not entry.name.startswith("harness-"):
435
+ continue
436
+ if not (entry / "SKILL.md").is_file():
437
+ continue
438
+ if (out_dir / entry.name).exists():
439
+ continue
440
+ shutil.copytree(
441
+ entry,
442
+ out_dir / entry.name,
443
+ ignore=shutil.ignore_patterns("__pycache__", "*.pyc"),
444
+ )
445
+ added.append(entry.name + "/")
446
+ return added
447
+
448
+
449
+ def merge_overlay_skill_extras(overlay_dir: Path, out_dir: Path) -> list[str]:
450
+ """Copy overlays/java/harness-<name>/* into existing out/harness-<name>/ (reference, etc.)."""
451
+ merged: list[str] = []
452
+ unique = {
453
+ p.name
454
+ for p in overlay_dir.iterdir()
455
+ if p.is_dir() and (p / "SKILL.md").is_file()
456
+ }
457
+ for entry in sorted(overlay_dir.iterdir()):
458
+ if not entry.is_dir() or not entry.name.startswith("harness-"):
459
+ continue
460
+ if entry.name in unique:
461
+ continue
462
+ dest_skill = out_dir / entry.name
463
+ if not dest_skill.is_dir():
464
+ continue
465
+ for src in entry.rglob("*"):
466
+ if src.is_dir() or "__pycache__" in src.parts:
467
+ continue
468
+ rel = src.relative_to(entry)
469
+ dest = dest_skill / rel
470
+ dest.parent.mkdir(parents=True, exist_ok=True)
471
+ shutil.copy2(src, dest)
472
+ merged.append(f"{entry.name}/{rel.as_posix()}")
473
+ return merged
474
+
475
+
476
+ def copy_overlay_extras(overlay_dir: Path, out_dir: Path) -> list[str]:
477
+ extras: list[str] = []
478
+ # §7.4: PROJECT-PROFILE-EXAMPLE.md stays a Vault doc (UDP real values);
479
+ # it must NOT be installed to runtime. Only pitfalls-java.md is runtime.
480
+ pitfalls = overlay_dir / "pitfalls-java.md"
481
+ if pitfalls.is_file():
482
+ test_pit = out_dir / "harness-test" / "pitfalls-java.md"
483
+ test_pit.parent.mkdir(parents=True, exist_ok=True)
484
+ shutil.copy2(pitfalls, test_pit)
485
+ extras.append("harness-test/pitfalls-java.md")
486
+ return extras
487
+
488
+
489
+ def cmd_build(
490
+ skills_root: Path,
491
+ out_dir: Path,
492
+ overlay: str | None,
493
+ agent: str | None = None,
494
+ ) -> dict[str, Any]:
495
+ skills_root, out_dir = validate_build_paths(skills_root, out_dir)
496
+ shared_dir = skills_root / "shared"
497
+ if not shared_dir.is_dir():
498
+ raise FileNotFoundError(f"shared/ missing under {skills_root}")
499
+ if agent is None:
500
+ print(
501
+ "WARNING: --agent missing, defaulting to claude-code (deprecated)",
502
+ file=sys.stderr,
503
+ )
504
+ agent = "claude-code"
505
+ if agent not in HARNESS_AGENTS:
506
+ raise ValueError(f"unknown agent: {agent}")
507
+ overlay_dir = skills_root / "overlays" / overlay if overlay else None
508
+ if overlay and (not overlay_dir or not overlay_dir.is_dir()):
509
+ raise FileNotFoundError(f"overlay not found: overlays/{overlay}")
510
+ agent_overlay_dir = skills_root / "adapters" / agent / "skill-overlays"
511
+ if not agent_overlay_dir.is_dir():
512
+ agent_overlay_dir = None
513
+ adapter_agents_dir = skills_root / "adapters" / agent / "agents"
514
+
515
+ # §7.2: build entirely in a staging dir; out_dir is untouched until the
516
+ # atomic swap. Never `shutil.rmtree(out_dir)` at build start.
517
+ staging = out_dir.parent / f".{out_dir.name}.staging-{uuid.uuid4().hex[:8]}"
518
+ if staging.exists():
519
+ shutil.rmtree(staging)
520
+ try:
521
+ copied = copy_tree(skills_root, staging, agent)
522
+ if overlay:
523
+ copied.extend(copy_overlay_unique_skills(overlay_dir, staging))
524
+ copied.extend(copy_overlay_extras(overlay_dir, staging))
525
+ copied.extend(merge_overlay_skill_extras(overlay_dir, staging))
526
+ if agent in AGENTS_WITH_CUSTOM_AGENTS:
527
+ copied.extend(copy_adapter_agents(adapter_agents_dir, staging))
528
+
529
+ header = synthesis_header(skills_root, overlay_dir, agent, agent_overlay_dir)
530
+ processed: list[str] = []
531
+ for skill_md in sorted(staging.glob("harness-*/SKILL.md")):
532
+ ov = overlay_for_skill(overlay_dir, skill_md.parent.name) if overlay_dir else None
533
+ agent_ov = (
534
+ overlay_for_skill(agent_overlay_dir, skill_md.parent.name)
535
+ if agent_overlay_dir
536
+ else None
537
+ )
538
+ try:
539
+ process_skill_md(skill_md, shared_dir, ov, header, agent_ov)
540
+ except KeyError as exc:
541
+ raise KeyError(
542
+ f"overlay anchor not found: {exc.args[0]} ({skill_md})"
543
+ ) from exc
544
+ processed.append(str(skill_md.relative_to(staging)))
545
+
546
+ for skill_md in sorted(staging.glob("harness-*/SKILL.md")):
547
+ if INCLUDE_RE.search(skill_md.read_text(encoding="utf-8")):
548
+ raise RuntimeError(f"include placeholder remains: {skill_md}")
549
+
550
+ # deterministic build marker (no timestamp -> byte-identical builds)
551
+ core_hash = core_content_hash(skills_root, overlay_dir, agent_overlay_dir)
552
+ (staging / BUILD_MARKER).write_text(
553
+ json.dumps(
554
+ {
555
+ "schemaVersion": 1,
556
+ "overlay": overlay or "none",
557
+ "agent": agent,
558
+ "coreHash": core_hash,
559
+ },
560
+ ensure_ascii=False,
561
+ indent=2,
562
+ )
563
+ + "\n",
564
+ encoding="utf-8",
565
+ newline="\n",
566
+ )
567
+ except Exception:
568
+ # build failed: clean staging, leave out_dir completely untouched
569
+ shutil.rmtree(staging, ignore_errors=True)
570
+ raise
571
+
572
+ # atomic swap: move old out aside (if a valid build), rename staging -> out
573
+ old_backup = None
574
+ if out_dir.exists():
575
+ if (out_dir / BUILD_MARKER).is_file():
576
+ old_backup = out_dir.parent / f".{out_dir.name}.old-{uuid.uuid4().hex[:8]}"
577
+ replace_with_retry(out_dir, old_backup)
578
+ else:
579
+ shutil.rmtree(staging, ignore_errors=True)
580
+ raise ValueError(f"refusing to overwrite unmarked out_dir: {out_dir}")
581
+ try:
582
+ replace_with_retry(staging, out_dir)
583
+ except OSError:
584
+ if old_backup is not None and old_backup.exists():
585
+ replace_with_retry(old_backup, out_dir)
586
+ raise
587
+ if old_backup is not None:
588
+ shutil.rmtree(old_backup, ignore_errors=True)
589
+
590
+ return {
591
+ "ok": True,
592
+ "action": "build",
593
+ "skillsRoot": str(skills_root),
594
+ "outDir": str(out_dir),
595
+ "overlay": overlay,
596
+ "agent": agent,
597
+ "copied": copied,
598
+ "processedSkills": processed,
599
+ }
600
+
601
+
602
+ def collect_files(root: Path) -> dict[str, str]:
603
+ files: dict[str, str] = {}
604
+ for path in sorted(root.rglob("*")):
605
+ if path.is_file() and "__pycache__" not in path.parts:
606
+ rel = path.relative_to(root).as_posix()
607
+ files[rel] = sha256_file(path)
608
+ return files
609
+
610
+
611
+ # --- Per-file bundle manifest (retro 2026-07-20 §5.1/5.25) -----------------
612
+ #
613
+ # registry_version + bundle_hash alone cannot prove every installed file
614
+ # belongs to the declared bundle. The bundle therefore ships a per-file
615
+ # manifest (relpath/sha256/size/mode/adapterTransformationId); install
616
+ # verifies staging content against it BEFORE the atomic switch and never
617
+ # updates metadata on partial failure.
618
+
619
+ BUNDLE_MANIFEST_NAME = "bundle-manifest.json"
620
+ _MANIFEST_EXCLUDED = frozenset(
621
+ {BUILD_MARKER, MANIFEST_NAME, AGENTS_MANAGED_NAME, BUNDLE_MANIFEST_NAME}
622
+ )
623
+
624
+
625
+ def build_manifest(root: Path, transformation_id: str = "raw") -> list[dict[str, Any]]:
626
+ """Return per-file manifest entries for every bundle file under ``root``.
627
+
628
+ Install metadata (build marker, managed manifests, the manifest itself)
629
+ is excluded. Entries are sorted by relpath for deterministic output.
630
+ """
631
+ root = root.resolve()
632
+ entries: list[dict[str, Any]] = []
633
+ for path in sorted(root.rglob("*")):
634
+ if not path.is_file() or "__pycache__" in path.parts:
635
+ continue
636
+ rel = path.relative_to(root).as_posix()
637
+ if rel in _MANIFEST_EXCLUDED:
638
+ continue
639
+ stat = path.stat()
640
+ entries.append(
641
+ {
642
+ "relpath": rel,
643
+ "sha256": sha256_file(path),
644
+ "size": stat.st_size,
645
+ "mode": stat.st_mode,
646
+ "adapterTransformationId": transformation_id,
647
+ }
648
+ )
649
+ return entries
650
+
651
+
652
+ def compute_bundle_manifest_hash(entries: list[dict[str, Any]]) -> str:
653
+ """Deterministic aggregate hash over manifest entries (order-independent).
654
+
655
+ Must match packages/contracts/src/bundle-manifest.ts computeBundleManifestHash.
656
+ """
657
+ ordered = sorted(entries, key=lambda entry: entry["relpath"])
658
+ lines = [
659
+ f"{entry['relpath']}:{entry['sha256']}:{entry['size']}:{entry['mode']}:{entry['adapterTransformationId']}"
660
+ for entry in ordered
661
+ ]
662
+ return hashlib.sha256("\n".join(lines).encode("utf-8")).hexdigest()
663
+
664
+
665
+ def aggregate_installed_content_hash(files: dict[str, str]) -> str:
666
+ """Deterministic aggregate hash over actually-installed relpath->sha256."""
667
+ lines = [f"{rel}:{files[rel]}" for rel in sorted(files)]
668
+ return hashlib.sha256("\n".join(lines).encode("utf-8")).hexdigest()
669
+
670
+
671
+ def _verify_tree_against_manifest(
672
+ root: Path, entries: list[dict[str, Any]]
673
+ ) -> list[dict[str, str]]:
674
+ """Return mismatch details for files under ``root`` vs manifest entries."""
675
+ mismatches: list[dict[str, str]] = []
676
+ actual = collect_files(root)
677
+ declared = {entry["relpath"]: entry["sha256"] for entry in entries}
678
+ for rel, expected in sorted(declared.items()):
679
+ if rel not in actual:
680
+ mismatches.append({"relpath": rel, "expected": expected, "actual": "<missing>"})
681
+ elif actual[rel] != expected:
682
+ mismatches.append(
683
+ {"relpath": rel, "expected": expected, "actual": actual[rel]}
684
+ )
685
+ return mismatches
686
+
687
+
688
+ def cmd_verify_installed(
689
+ installed_dir: Path, entries: list[dict[str, Any]], bundle_version: str
690
+ ) -> dict[str, Any]:
691
+ """Verify installed content against the bundle manifest, per file."""
692
+ installed_dir = installed_dir.resolve()
693
+ mismatches = _verify_tree_against_manifest(installed_dir, entries)
694
+ actual = collect_files(installed_dir)
695
+ declared = {entry["relpath"] for entry in entries}
696
+ content = {
697
+ rel: digest
698
+ for rel, digest in actual.items()
699
+ if rel not in _MANIFEST_EXCLUDED and rel in declared
700
+ }
701
+ return {
702
+ "ok": not mismatches,
703
+ "action": "verify-installed",
704
+ "bundleVersion": bundle_version,
705
+ "verificationStatus": "verified" if not mismatches else "degraded",
706
+ "installedContentHash": aggregate_installed_content_hash(content),
707
+ "verifiedAt": now_iso(),
708
+ "mismatchDetails": mismatches,
709
+ }
710
+
711
+
712
+ def cmd_install(build_out: Path, project: Path, target: Path | None) -> dict[str, Any]:
713
+ build_out = build_out.resolve()
714
+ project = project.resolve()
715
+ dest = (target or project / ".claude" / "skills").resolve()
716
+ if not (build_out / BUILD_MARKER).is_file():
717
+ raise ValueError(f"install source is not a marked harness build: {build_out}")
718
+
719
+ # Managed set = real files in the build (exclude build marker). Recorded so a
720
+ # later conservative install can delete removed managed files and preserve
721
+ # user-owned files (design §3.8 要点4). Paths are relative to dest, which
722
+ # mirrors build_out (agents/ included because copy_tree keeps it there).
723
+ managed_new = {p for p in collect_files(build_out) if p != BUILD_MARKER}
724
+ managed_agents_new = {
725
+ p[len("agents/"):]
726
+ for p in managed_new
727
+ if p.startswith("agents/") and "/" not in p[len("agents/"):]
728
+ }
729
+
730
+ backup: str | None = None
731
+ staging = dest.parent / f".{dest.name}.staging-{uuid.uuid4().hex[:8]}"
732
+ shutil.copytree(build_out, staging)
733
+ # Per-file content proof (retro §5.1): if the build carries a
734
+ # bundle-manifest.json, staging must match it exactly before anything is
735
+ # switched. A mismatch aborts the install without touching dest or any
736
+ # metadata — never a partial install under a fresh version label.
737
+ manifest_path = staging / BUNDLE_MANIFEST_NAME
738
+ if manifest_path.is_file():
739
+ manifest_data = json.loads(manifest_path.read_text(encoding="utf-8"))
740
+ manifest_entries = (
741
+ manifest_data.get("files", []) if isinstance(manifest_data, dict) else []
742
+ )
743
+ mismatches = _verify_tree_against_manifest(staging, manifest_entries)
744
+ if mismatches:
745
+ shutil.rmtree(staging, ignore_errors=True)
746
+ raise ValueError(
747
+ "BUNDLE_CONTENT_MISMATCH: staging diverges from bundle-manifest.json: "
748
+ + ", ".join(m["relpath"] for m in mismatches)
749
+ )
750
+ if dest.exists() and any(dest.iterdir()):
751
+ managed_old = _read_managed_set(dest / MANIFEST_NAME)
752
+ # Preserve user-owned files: anything in dest that was NOT managed by a
753
+ # prior install is copied into staging (never overwriting managed files
754
+ # that staging already carries from build_out). Removed managed files
755
+ # (in managed_old but not managed_new) are simply not carried over.
756
+ for rel in collect_files(dest):
757
+ if rel in (BUILD_MARKER, MANIFEST_NAME) or rel in INSTALL_METADATA:
758
+ continue
759
+ if rel in managed_old:
760
+ continue
761
+ dst = staging / rel
762
+ if dst.exists():
763
+ continue
764
+ dst.parent.mkdir(parents=True, exist_ok=True)
765
+ shutil.copy2(dest / rel, dst)
766
+ stamp = dt.datetime.now().strftime("%Y%m%d-%H%M%S")
767
+ backup_path = dest.parent / f"skills-backup-{stamp}"
768
+ os.replace(dest, backup_path)
769
+ backup = str(backup_path)
770
+ try:
771
+ os.replace(staging, dest)
772
+ except OSError:
773
+ shutil.rmtree(staging, ignore_errors=True)
774
+ if backup:
775
+ os.replace(Path(backup), dest)
776
+ raise
777
+
778
+ # Record the managed set so the next conservative install can distinguish
779
+ # managed vs user-owned files.
780
+ (dest / MANIFEST_NAME).write_text(
781
+ json.dumps(
782
+ {"schemaVersion": 1, "managedFiles": sorted(managed_new)},
783
+ ensure_ascii=False,
784
+ indent=2,
785
+ )
786
+ + "\n",
787
+ encoding="utf-8",
788
+ newline="\n",
789
+ )
790
+
791
+ agents_src = build_out / "agents"
792
+ agents_dest = project / ".claude" / "agents"
793
+ if agents_src.is_dir():
794
+ agents_dest.mkdir(parents=True, exist_ok=True)
795
+ agents_old = _read_managed_set(agents_dest / AGENTS_MANAGED_NAME)
796
+ # Delete managed agents removed since the prior install; user agents
797
+ # (never in a managed set) are left untouched.
798
+ for name in list(agents_old):
799
+ if name not in managed_agents_new:
800
+ (agents_dest / name).unlink(missing_ok=True)
801
+ for agent in agents_src.glob("*.md"):
802
+ shutil.copy2(agent, agents_dest / agent.name)
803
+ (agents_dest / AGENTS_MANAGED_NAME).write_text(
804
+ json.dumps(
805
+ {"schemaVersion": 1, "managedFiles": sorted(managed_agents_new)},
806
+ ensure_ascii=False,
807
+ indent=2,
808
+ )
809
+ + "\n",
810
+ encoding="utf-8",
811
+ newline="\n",
812
+ )
813
+
814
+ return {
815
+ "ok": True,
816
+ "action": "install",
817
+ "from": str(build_out),
818
+ "project": str(project),
819
+ "target": str(dest),
820
+ "backup": backup,
821
+ "installedAt": now_iso(),
822
+ }
823
+
824
+
825
+ def cmd_validate_manifest(
826
+ bundle_dir: Path, manifest_entries: list[dict[str, Any]]
827
+ ) -> dict[str, Any]:
828
+ """Compare a bundle directory's actual file set against the manifest's
829
+ declared set. Both missing (declared but absent) and extra (present but
830
+ undeclared) fail validation (design §3.8 要点2)."""
831
+ bundle_dir = bundle_dir.resolve()
832
+ actual = collect_files(bundle_dir)
833
+ declared: dict[str, str] = {}
834
+ for entry in manifest_entries:
835
+ path = entry.get("path")
836
+ sha = entry.get("sha256")
837
+ if isinstance(path, str) and isinstance(sha, str):
838
+ declared[path] = sha
839
+ missing = sorted(p for p in declared if p not in actual)
840
+ extra = sorted(p for p in actual if p not in declared)
841
+ hash_mismatch = sorted(
842
+ p for p in declared if p in actual and actual[p] != declared[p]
843
+ )
844
+ return {
845
+ "ok": not (missing or extra or hash_mismatch),
846
+ "action": "validate-manifest",
847
+ "missing": missing,
848
+ "extra": extra,
849
+ "hashMismatch": hash_mismatch,
850
+ "comparedAt": now_iso(),
851
+ }
852
+
853
+
854
+ def build_parser() -> argparse.ArgumentParser:
855
+ p = argparse.ArgumentParser(description="Harness skills deploy (build/install)")
856
+ sub = p.add_subparsers(dest="command", required=True)
857
+
858
+ b = sub.add_parser("build", help="Synthesize core + overlay into output dir")
859
+ b.add_argument("--skills-root", type=Path, required=True)
860
+ b.add_argument("--out", type=Path, required=True)
861
+ b.add_argument("--overlay")
862
+ b.add_argument("--agent", choices=HARNESS_AGENTS)
863
+ b.add_argument("--json", action="store_true")
864
+
865
+ i = sub.add_parser("install", help="Install build output to project .claude/skills")
866
+ i.add_argument("--from", dest="from_dir", type=Path, required=True)
867
+ i.add_argument("--project", type=Path, required=True)
868
+ i.add_argument("--target", type=Path)
869
+ i.add_argument("--json", action="store_true")
870
+
871
+ v = sub.add_parser(
872
+ "validate-manifest", help="Validate bundle dir vs manifest (missing/extra/hash)"
873
+ )
874
+ v.add_argument("--bundle", type=Path, required=True)
875
+ v.add_argument("--manifest", type=Path, required=True)
876
+ v.add_argument("--json", action="store_true")
877
+
878
+ g = sub.add_parser(
879
+ "generate-manifest",
880
+ help="Generate per-file bundle-manifest.json for a bundle dir",
881
+ )
882
+ g.add_argument("--bundle", type=Path, required=True)
883
+ g.add_argument("--bundle-version", required=True)
884
+ g.add_argument(
885
+ "--transformation",
886
+ choices=["raw", "adapted", "workflow-packaged"],
887
+ default="raw",
888
+ )
889
+ g.add_argument("--out", type=Path)
890
+ g.add_argument("--json", action="store_true")
891
+
892
+ vi = sub.add_parser(
893
+ "verify-installed",
894
+ help="Verify installed tree content against bundle-manifest.json",
895
+ )
896
+ vi.add_argument("--installed", type=Path, required=True)
897
+ vi.add_argument("--manifest", type=Path, required=True)
898
+ vi.add_argument("--json", action="store_true")
899
+ return p
900
+
901
+
902
+ def main(argv: list[str] | None = None) -> int:
903
+ args = build_parser().parse_args(argv)
904
+ try:
905
+ if args.command == "build":
906
+ result = cmd_build(args.skills_root, args.out, args.overlay, args.agent)
907
+ return emit_json(result) if args.json else 0
908
+ if args.command == "install":
909
+ result = cmd_install(args.from_dir, args.project, args.target)
910
+ return emit_json(result) if args.json else 0
911
+ if args.command == "validate-manifest":
912
+ manifest_data = json.loads(args.manifest.read_text(encoding="utf-8"))
913
+ entries = (
914
+ manifest_data.get("files", [])
915
+ if isinstance(manifest_data, dict)
916
+ else []
917
+ )
918
+ result = cmd_validate_manifest(args.bundle, entries)
919
+ if args.json:
920
+ return emit_json(result)
921
+ return 0 if result["ok"] else 1
922
+ if args.command == "generate-manifest":
923
+ entries = build_manifest(args.bundle, transformation_id=args.transformation)
924
+ manifest = {
925
+ "schemaVersion": 1,
926
+ "bundleVersion": args.bundle_version,
927
+ "bundleManifestHash": compute_bundle_manifest_hash(entries),
928
+ "files": entries,
929
+ }
930
+ text = json.dumps(manifest, ensure_ascii=False, indent=2) + "\n"
931
+ if args.out is not None:
932
+ args.out.parent.mkdir(parents=True, exist_ok=True)
933
+ args.out.write_text(text, encoding="utf-8", newline="\n")
934
+ result = {
935
+ "ok": True,
936
+ "action": "generate-manifest",
937
+ "bundleVersion": args.bundle_version,
938
+ "bundleManifestHash": manifest["bundleManifestHash"],
939
+ "fileCount": len(entries),
940
+ "out": str(args.out) if args.out is not None else None,
941
+ }
942
+ if args.json:
943
+ return emit_json(result)
944
+ if args.out is None:
945
+ print(text, end="")
946
+ return 0
947
+ if args.command == "verify-installed":
948
+ manifest_data = json.loads(args.manifest.read_text(encoding="utf-8"))
949
+ if not isinstance(manifest_data, dict):
950
+ raise ValueError("manifest must be a JSON object")
951
+ result = cmd_verify_installed(
952
+ args.installed,
953
+ manifest_data.get("files", []),
954
+ str(manifest_data.get("bundleVersion", "")),
955
+ )
956
+ if args.json:
957
+ return emit_json(result)
958
+ return 0 if result["ok"] else 1
959
+ except (FileNotFoundError, KeyError, RuntimeError, ValueError) as exc:
960
+ if getattr(args, "json", False):
961
+ return emit_json({"ok": False, "error": str(exc)}, ok=False)
962
+ print(f"ERROR: {exc}", file=sys.stderr)
963
+ return 1
964
+ return 1
965
+
966
+
967
+ if __name__ == "__main__":
968
+ raise SystemExit(main())