@hunter-harness/workflow-harness 0.2.1 → 0.2.2

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 (262) 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 +2 -8
  4. package/harness/bundles/general/claude-code/harness-codebase-map/SKILL.md +2 -1
  5. package/harness/bundles/general/claude-code/harness-knowledge-ingest/SKILL.md +1 -1
  6. package/harness/bundles/general/claude-code/harness-knowledge-ingest/scripts/harness_knowledge.py +474 -122
  7. package/harness/bundles/general/claude-code/harness-knowledge-query/SKILL.md +1 -1
  8. package/harness/bundles/general/claude-code/harness-plan/SKILL.md +2 -1
  9. package/harness/bundles/general/claude-code/harness-review/SKILL.md +5 -1
  10. package/harness/bundles/general/claude-code/harness-run/SKILL.md +1 -1
  11. package/harness/bundles/general/claude-code/harness-run/checklist.md +1 -1
  12. package/harness/bundles/general/claude-code/harness-submit/SKILL.md +2 -2
  13. package/harness/bundles/general/claude-code/harness-submit/checklist.md +30 -6
  14. package/harness/bundles/general/claude-code/harness-sync/SKILL.md +1 -1
  15. package/harness/bundles/general/claude-code/harness-test/SKILL.md +2 -2
  16. package/harness/bundles/general/claude-code/harness-test/checklist.md +1 -1
  17. package/harness/bundles/general/claude-code/harness-test/pitfalls.md +16 -0
  18. package/harness/bundles/general/claude-code/harness-test/scripts/runtime-helpers.mjs +216 -0
  19. package/harness/bundles/general/claude-code/protocols/archive-report-protocol.md +1 -1
  20. package/harness/bundles/general/claude-code/protocols/ledger-protocol.md +55 -2
  21. package/harness/bundles/general/claude-code/protocols/report-pipeline-protocol.md +1 -8
  22. package/harness/bundles/general/claude-code/protocols/sensitive-info-protocol.md +13 -0
  23. package/harness/bundles/general/claude-code/protocols/state-layout-protocol.md +24 -0
  24. package/harness/bundles/general/claude-code/scripts/harness_archive.py +31 -14
  25. package/harness/bundles/general/claude-code/scripts/harness_check_gate.py +103 -0
  26. package/harness/bundles/general/claude-code/scripts/harness_deploy.py +179 -4
  27. package/harness/bundles/general/claude-code/scripts/harness_ledger.py +269 -3
  28. package/harness/bundles/general/claude-code/scripts/harness_preflight.py +72 -382
  29. package/harness/bundles/general/claude-code/scripts/harness_profile.py +742 -0
  30. package/harness/bundles/general/claude-code/scripts/harness_service.py +99 -2
  31. package/harness/bundles/general/claude-code/scripts/harness_state.py +179 -0
  32. package/harness/bundles/general/codebuddy/.harness-build.json +1 -1
  33. package/harness/bundles/general/codebuddy/harness-archive/SKILL.md +1 -1
  34. package/harness/bundles/general/codebuddy/harness-archive/reference.md +2 -8
  35. package/harness/bundles/general/codebuddy/harness-codebase-map/SKILL.md +2 -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 +474 -122
  38. package/harness/bundles/general/codebuddy/harness-knowledge-query/SKILL.md +1 -1
  39. package/harness/bundles/general/codebuddy/harness-plan/SKILL.md +2 -1
  40. package/harness/bundles/general/codebuddy/harness-review/SKILL.md +5 -1
  41. package/harness/bundles/general/codebuddy/harness-run/SKILL.md +1 -1
  42. package/harness/bundles/general/codebuddy/harness-run/checklist.md +1 -1
  43. package/harness/bundles/general/codebuddy/harness-submit/SKILL.md +2 -2
  44. package/harness/bundles/general/codebuddy/harness-submit/checklist.md +30 -6
  45. package/harness/bundles/general/codebuddy/harness-sync/SKILL.md +1 -1
  46. package/harness/bundles/general/codebuddy/harness-test/SKILL.md +2 -2
  47. package/harness/bundles/general/codebuddy/harness-test/checklist.md +1 -1
  48. package/harness/bundles/general/codebuddy/harness-test/pitfalls.md +16 -0
  49. package/harness/bundles/general/codebuddy/harness-test/scripts/runtime-helpers.mjs +216 -0
  50. package/harness/bundles/general/codebuddy/protocols/archive-report-protocol.md +1 -1
  51. package/harness/bundles/general/codebuddy/protocols/ledger-protocol.md +55 -2
  52. package/harness/bundles/general/codebuddy/protocols/report-pipeline-protocol.md +1 -8
  53. package/harness/bundles/general/codebuddy/protocols/sensitive-info-protocol.md +13 -0
  54. package/harness/bundles/general/codebuddy/protocols/state-layout-protocol.md +24 -0
  55. package/harness/bundles/general/codebuddy/scripts/harness_archive.py +31 -14
  56. package/harness/bundles/general/codebuddy/scripts/harness_check_gate.py +103 -0
  57. package/harness/bundles/general/codebuddy/scripts/harness_deploy.py +179 -4
  58. package/harness/bundles/general/codebuddy/scripts/harness_ledger.py +269 -3
  59. package/harness/bundles/general/codebuddy/scripts/harness_preflight.py +72 -382
  60. package/harness/bundles/general/codebuddy/scripts/harness_profile.py +742 -0
  61. package/harness/bundles/general/codebuddy/scripts/harness_service.py +99 -2
  62. package/harness/bundles/general/codebuddy/scripts/harness_state.py +179 -0
  63. package/harness/bundles/general/codex/.harness-build.json +1 -1
  64. package/harness/bundles/general/codex/harness-archive/SKILL.md +1 -1
  65. package/harness/bundles/general/codex/harness-archive/reference.md +2 -8
  66. package/harness/bundles/general/codex/harness-codebase-map/SKILL.md +2 -1
  67. package/harness/bundles/general/codex/harness-knowledge-ingest/SKILL.md +1 -1
  68. package/harness/bundles/general/codex/harness-knowledge-ingest/scripts/harness_knowledge.py +474 -122
  69. package/harness/bundles/general/codex/harness-knowledge-query/SKILL.md +1 -1
  70. package/harness/bundles/general/codex/harness-plan/SKILL.md +2 -1
  71. package/harness/bundles/general/codex/harness-review/SKILL.md +5 -1
  72. package/harness/bundles/general/codex/harness-run/SKILL.md +1 -1
  73. package/harness/bundles/general/codex/harness-run/checklist.md +1 -1
  74. package/harness/bundles/general/codex/harness-submit/SKILL.md +2 -2
  75. package/harness/bundles/general/codex/harness-submit/checklist.md +30 -6
  76. package/harness/bundles/general/codex/harness-sync/SKILL.md +1 -1
  77. package/harness/bundles/general/codex/harness-test/SKILL.md +2 -2
  78. package/harness/bundles/general/codex/harness-test/checklist.md +1 -1
  79. package/harness/bundles/general/codex/harness-test/pitfalls.md +16 -0
  80. package/harness/bundles/general/codex/harness-test/scripts/runtime-helpers.mjs +216 -0
  81. package/harness/bundles/general/codex/protocols/archive-report-protocol.md +1 -1
  82. package/harness/bundles/general/codex/protocols/ledger-protocol.md +55 -2
  83. package/harness/bundles/general/codex/protocols/report-pipeline-protocol.md +1 -8
  84. package/harness/bundles/general/codex/protocols/sensitive-info-protocol.md +13 -0
  85. package/harness/bundles/general/codex/protocols/state-layout-protocol.md +24 -0
  86. package/harness/bundles/general/codex/scripts/harness_archive.py +31 -14
  87. package/harness/bundles/general/codex/scripts/harness_check_gate.py +103 -0
  88. package/harness/bundles/general/codex/scripts/harness_deploy.py +179 -4
  89. package/harness/bundles/general/codex/scripts/harness_ledger.py +269 -3
  90. package/harness/bundles/general/codex/scripts/harness_preflight.py +72 -382
  91. package/harness/bundles/general/codex/scripts/harness_profile.py +742 -0
  92. package/harness/bundles/general/codex/scripts/harness_service.py +99 -2
  93. package/harness/bundles/general/codex/scripts/harness_state.py +179 -0
  94. package/harness/bundles/general/cursor/.harness-build.json +1 -1
  95. package/harness/bundles/general/cursor/harness-archive/SKILL.md +1 -1
  96. package/harness/bundles/general/cursor/harness-archive/reference.md +2 -8
  97. package/harness/bundles/general/cursor/harness-codebase-map/SKILL.md +2 -1
  98. package/harness/bundles/general/cursor/harness-knowledge-ingest/SKILL.md +1 -1
  99. package/harness/bundles/general/cursor/harness-knowledge-ingest/scripts/harness_knowledge.py +474 -122
  100. package/harness/bundles/general/cursor/harness-knowledge-query/SKILL.md +1 -1
  101. package/harness/bundles/general/cursor/harness-plan/SKILL.md +2 -1
  102. package/harness/bundles/general/cursor/harness-review/SKILL.md +5 -1
  103. package/harness/bundles/general/cursor/harness-run/SKILL.md +1 -1
  104. package/harness/bundles/general/cursor/harness-run/checklist.md +1 -1
  105. package/harness/bundles/general/cursor/harness-submit/SKILL.md +2 -2
  106. package/harness/bundles/general/cursor/harness-submit/checklist.md +30 -6
  107. package/harness/bundles/general/cursor/harness-sync/SKILL.md +1 -1
  108. package/harness/bundles/general/cursor/harness-test/SKILL.md +2 -2
  109. package/harness/bundles/general/cursor/harness-test/checklist.md +1 -1
  110. package/harness/bundles/general/cursor/harness-test/pitfalls.md +16 -0
  111. package/harness/bundles/general/cursor/harness-test/scripts/runtime-helpers.mjs +216 -0
  112. package/harness/bundles/general/cursor/protocols/archive-report-protocol.md +1 -1
  113. package/harness/bundles/general/cursor/protocols/ledger-protocol.md +55 -2
  114. package/harness/bundles/general/cursor/protocols/report-pipeline-protocol.md +1 -8
  115. package/harness/bundles/general/cursor/protocols/sensitive-info-protocol.md +13 -0
  116. package/harness/bundles/general/cursor/protocols/state-layout-protocol.md +24 -0
  117. package/harness/bundles/general/cursor/scripts/harness_archive.py +31 -14
  118. package/harness/bundles/general/cursor/scripts/harness_check_gate.py +103 -0
  119. package/harness/bundles/general/cursor/scripts/harness_deploy.py +179 -4
  120. package/harness/bundles/general/cursor/scripts/harness_ledger.py +269 -3
  121. package/harness/bundles/general/cursor/scripts/harness_preflight.py +72 -382
  122. package/harness/bundles/general/cursor/scripts/harness_profile.py +742 -0
  123. package/harness/bundles/general/cursor/scripts/harness_service.py +99 -2
  124. package/harness/bundles/general/cursor/scripts/harness_state.py +179 -0
  125. package/harness/bundles/java/claude-code/.harness-build.json +1 -1
  126. package/harness/bundles/java/claude-code/harness-apidoc/SKILL.md +1 -1
  127. package/harness/bundles/java/claude-code/harness-archive/SKILL.md +1 -1
  128. package/harness/bundles/java/claude-code/harness-archive/reference.md +2 -8
  129. package/harness/bundles/java/claude-code/harness-codebase-map/SKILL.md +2 -1
  130. package/harness/bundles/java/claude-code/harness-knowledge-ingest/SKILL.md +1 -1
  131. package/harness/bundles/java/claude-code/harness-knowledge-ingest/scripts/harness_knowledge.py +474 -122
  132. package/harness/bundles/java/claude-code/harness-knowledge-query/SKILL.md +1 -1
  133. package/harness/bundles/java/claude-code/harness-package/SKILL.md +3 -3
  134. package/harness/bundles/java/claude-code/harness-plan/SKILL.md +2 -1
  135. package/harness/bundles/java/claude-code/harness-review/SKILL.md +5 -1
  136. package/harness/bundles/java/claude-code/harness-run/SKILL.md +4 -4
  137. package/harness/bundles/java/claude-code/harness-run/checklist.md +1 -1
  138. package/harness/bundles/java/claude-code/harness-submit/SKILL.md +3 -3
  139. package/harness/bundles/java/claude-code/harness-submit/checklist.md +30 -6
  140. package/harness/bundles/java/claude-code/harness-sync/SKILL.md +1 -1
  141. package/harness/bundles/java/claude-code/harness-test/SKILL.md +5 -5
  142. package/harness/bundles/java/claude-code/harness-test/pitfalls.md +16 -0
  143. package/harness/bundles/java/claude-code/harness-test/scripts/runtime-helpers.mjs +216 -0
  144. package/harness/bundles/java/claude-code/protocols/archive-report-protocol.md +1 -1
  145. package/harness/bundles/java/claude-code/protocols/ledger-protocol.md +55 -2
  146. package/harness/bundles/java/claude-code/protocols/report-pipeline-protocol.md +1 -8
  147. package/harness/bundles/java/claude-code/protocols/sensitive-info-protocol.md +13 -0
  148. package/harness/bundles/java/claude-code/protocols/state-layout-protocol.md +24 -0
  149. package/harness/bundles/java/claude-code/scripts/harness_archive.py +31 -14
  150. package/harness/bundles/java/claude-code/scripts/harness_check_gate.py +103 -0
  151. package/harness/bundles/java/claude-code/scripts/harness_deploy.py +179 -4
  152. package/harness/bundles/java/claude-code/scripts/harness_ledger.py +269 -3
  153. package/harness/bundles/java/claude-code/scripts/harness_preflight.py +72 -382
  154. package/harness/bundles/java/claude-code/scripts/harness_profile.py +742 -0
  155. package/harness/bundles/java/claude-code/scripts/harness_service.py +99 -2
  156. package/harness/bundles/java/claude-code/scripts/harness_state.py +179 -0
  157. package/harness/bundles/java/codebuddy/.harness-build.json +1 -1
  158. package/harness/bundles/java/codebuddy/harness-apidoc/SKILL.md +1 -1
  159. package/harness/bundles/java/codebuddy/harness-archive/SKILL.md +1 -1
  160. package/harness/bundles/java/codebuddy/harness-archive/reference.md +2 -8
  161. package/harness/bundles/java/codebuddy/harness-codebase-map/SKILL.md +2 -1
  162. package/harness/bundles/java/codebuddy/harness-knowledge-ingest/SKILL.md +1 -1
  163. package/harness/bundles/java/codebuddy/harness-knowledge-ingest/scripts/harness_knowledge.py +474 -122
  164. package/harness/bundles/java/codebuddy/harness-knowledge-query/SKILL.md +1 -1
  165. package/harness/bundles/java/codebuddy/harness-package/SKILL.md +3 -3
  166. package/harness/bundles/java/codebuddy/harness-plan/SKILL.md +2 -1
  167. package/harness/bundles/java/codebuddy/harness-review/SKILL.md +5 -1
  168. package/harness/bundles/java/codebuddy/harness-run/SKILL.md +4 -4
  169. package/harness/bundles/java/codebuddy/harness-run/checklist.md +1 -1
  170. package/harness/bundles/java/codebuddy/harness-submit/SKILL.md +3 -3
  171. package/harness/bundles/java/codebuddy/harness-submit/checklist.md +30 -6
  172. package/harness/bundles/java/codebuddy/harness-sync/SKILL.md +1 -1
  173. package/harness/bundles/java/codebuddy/harness-test/SKILL.md +5 -5
  174. package/harness/bundles/java/codebuddy/harness-test/pitfalls.md +16 -0
  175. package/harness/bundles/java/codebuddy/harness-test/scripts/runtime-helpers.mjs +216 -0
  176. package/harness/bundles/java/codebuddy/protocols/archive-report-protocol.md +1 -1
  177. package/harness/bundles/java/codebuddy/protocols/ledger-protocol.md +55 -2
  178. package/harness/bundles/java/codebuddy/protocols/report-pipeline-protocol.md +1 -8
  179. package/harness/bundles/java/codebuddy/protocols/sensitive-info-protocol.md +13 -0
  180. package/harness/bundles/java/codebuddy/protocols/state-layout-protocol.md +24 -0
  181. package/harness/bundles/java/codebuddy/scripts/harness_archive.py +31 -14
  182. package/harness/bundles/java/codebuddy/scripts/harness_check_gate.py +103 -0
  183. package/harness/bundles/java/codebuddy/scripts/harness_deploy.py +179 -4
  184. package/harness/bundles/java/codebuddy/scripts/harness_ledger.py +269 -3
  185. package/harness/bundles/java/codebuddy/scripts/harness_preflight.py +72 -382
  186. package/harness/bundles/java/codebuddy/scripts/harness_profile.py +742 -0
  187. package/harness/bundles/java/codebuddy/scripts/harness_service.py +99 -2
  188. package/harness/bundles/java/codebuddy/scripts/harness_state.py +179 -0
  189. package/harness/bundles/java/codex/.harness-build.json +1 -1
  190. package/harness/bundles/java/codex/harness-apidoc/SKILL.md +1 -1
  191. package/harness/bundles/java/codex/harness-archive/SKILL.md +1 -1
  192. package/harness/bundles/java/codex/harness-archive/reference.md +2 -8
  193. package/harness/bundles/java/codex/harness-codebase-map/SKILL.md +2 -1
  194. package/harness/bundles/java/codex/harness-knowledge-ingest/SKILL.md +1 -1
  195. package/harness/bundles/java/codex/harness-knowledge-ingest/scripts/harness_knowledge.py +474 -122
  196. package/harness/bundles/java/codex/harness-knowledge-query/SKILL.md +1 -1
  197. package/harness/bundles/java/codex/harness-package/SKILL.md +3 -3
  198. package/harness/bundles/java/codex/harness-plan/SKILL.md +2 -1
  199. package/harness/bundles/java/codex/harness-review/SKILL.md +5 -1
  200. package/harness/bundles/java/codex/harness-run/SKILL.md +4 -4
  201. package/harness/bundles/java/codex/harness-run/checklist.md +1 -1
  202. package/harness/bundles/java/codex/harness-submit/SKILL.md +3 -3
  203. package/harness/bundles/java/codex/harness-submit/checklist.md +30 -6
  204. package/harness/bundles/java/codex/harness-sync/SKILL.md +1 -1
  205. package/harness/bundles/java/codex/harness-test/SKILL.md +5 -5
  206. package/harness/bundles/java/codex/harness-test/pitfalls.md +16 -0
  207. package/harness/bundles/java/codex/harness-test/scripts/runtime-helpers.mjs +216 -0
  208. package/harness/bundles/java/codex/protocols/archive-report-protocol.md +1 -1
  209. package/harness/bundles/java/codex/protocols/ledger-protocol.md +55 -2
  210. package/harness/bundles/java/codex/protocols/report-pipeline-protocol.md +1 -8
  211. package/harness/bundles/java/codex/protocols/sensitive-info-protocol.md +13 -0
  212. package/harness/bundles/java/codex/protocols/state-layout-protocol.md +24 -0
  213. package/harness/bundles/java/codex/scripts/harness_archive.py +31 -14
  214. package/harness/bundles/java/codex/scripts/harness_check_gate.py +103 -0
  215. package/harness/bundles/java/codex/scripts/harness_deploy.py +179 -4
  216. package/harness/bundles/java/codex/scripts/harness_ledger.py +269 -3
  217. package/harness/bundles/java/codex/scripts/harness_preflight.py +72 -382
  218. package/harness/bundles/java/codex/scripts/harness_profile.py +742 -0
  219. package/harness/bundles/java/codex/scripts/harness_service.py +99 -2
  220. package/harness/bundles/java/codex/scripts/harness_state.py +179 -0
  221. package/harness/bundles/java/cursor/.harness-build.json +1 -1
  222. package/harness/bundles/java/cursor/harness-apidoc/SKILL.md +1 -1
  223. package/harness/bundles/java/cursor/harness-archive/SKILL.md +1 -1
  224. package/harness/bundles/java/cursor/harness-archive/reference.md +2 -8
  225. package/harness/bundles/java/cursor/harness-codebase-map/SKILL.md +2 -1
  226. package/harness/bundles/java/cursor/harness-knowledge-ingest/SKILL.md +1 -1
  227. package/harness/bundles/java/cursor/harness-knowledge-ingest/scripts/harness_knowledge.py +474 -122
  228. package/harness/bundles/java/cursor/harness-knowledge-query/SKILL.md +1 -1
  229. package/harness/bundles/java/cursor/harness-package/SKILL.md +3 -3
  230. package/harness/bundles/java/cursor/harness-plan/SKILL.md +2 -1
  231. package/harness/bundles/java/cursor/harness-review/SKILL.md +5 -1
  232. package/harness/bundles/java/cursor/harness-run/SKILL.md +4 -4
  233. package/harness/bundles/java/cursor/harness-run/checklist.md +1 -1
  234. package/harness/bundles/java/cursor/harness-submit/SKILL.md +3 -3
  235. package/harness/bundles/java/cursor/harness-submit/checklist.md +30 -6
  236. package/harness/bundles/java/cursor/harness-sync/SKILL.md +1 -1
  237. package/harness/bundles/java/cursor/harness-test/SKILL.md +5 -5
  238. package/harness/bundles/java/cursor/harness-test/pitfalls.md +16 -0
  239. package/harness/bundles/java/cursor/harness-test/scripts/runtime-helpers.mjs +216 -0
  240. package/harness/bundles/java/cursor/protocols/archive-report-protocol.md +1 -1
  241. package/harness/bundles/java/cursor/protocols/ledger-protocol.md +55 -2
  242. package/harness/bundles/java/cursor/protocols/report-pipeline-protocol.md +1 -8
  243. package/harness/bundles/java/cursor/protocols/sensitive-info-protocol.md +13 -0
  244. package/harness/bundles/java/cursor/protocols/state-layout-protocol.md +24 -0
  245. package/harness/bundles/java/cursor/scripts/harness_archive.py +31 -14
  246. package/harness/bundles/java/cursor/scripts/harness_check_gate.py +103 -0
  247. package/harness/bundles/java/cursor/scripts/harness_deploy.py +179 -4
  248. package/harness/bundles/java/cursor/scripts/harness_ledger.py +269 -3
  249. package/harness/bundles/java/cursor/scripts/harness_preflight.py +72 -382
  250. package/harness/bundles/java/cursor/scripts/harness_profile.py +742 -0
  251. package/harness/bundles/java/cursor/scripts/harness_service.py +99 -2
  252. package/harness/bundles/java/cursor/scripts/harness_state.py +179 -0
  253. package/harness/manifests/general/claude-code.json +44 -28
  254. package/harness/manifests/general/codebuddy.json +44 -28
  255. package/harness/manifests/general/codex.json +44 -28
  256. package/harness/manifests/general/cursor.json +44 -28
  257. package/harness/manifests/java/claude-code.json +45 -29
  258. package/harness/manifests/java/codebuddy.json +45 -29
  259. package/harness/manifests/java/codex.json +45 -29
  260. package/harness/manifests/java/cursor.json +45 -29
  261. package/hunter-workflow-family.json +2 -2
  262. package/package.json +1 -1
@@ -102,11 +102,15 @@ def read_json(path: Path) -> Any:
102
102
 
103
103
  def write_json(path: Path, data: Any) -> None:
104
104
  path.parent.mkdir(parents=True, exist_ok=True)
105
- path.write_text(
106
- json.dumps(data, ensure_ascii=False, indent=2) + "\n",
107
- encoding="utf-8",
108
- newline="\n",
109
- )
105
+ text = json.dumps(data, ensure_ascii=False, indent=2) + "\n"
106
+ # 强制 LF,UTF-8 无 BOM;原子写 temp+os.replace(与 runtime-helpers.mjs 一致)。
107
+ tmp = path.with_name(f".{path.name}.{os.getpid()}.tmp")
108
+ try:
109
+ tmp.write_text(text, encoding="utf-8", newline="\n")
110
+ os.replace(tmp, path)
111
+ except BaseException:
112
+ tmp.unlink(missing_ok=True)
113
+ raise
110
114
 
111
115
 
112
116
  def sha256_file(path: Path) -> str:
@@ -490,16 +494,29 @@ def check_status(change_dir: Path) -> dict[str, Any]:
490
494
  if head_hash.startswith(expected_hash) or expected_hash.startswith(head_hash[:7]):
491
495
  checks["final_hash_match"] = True
492
496
  else:
493
- checks["final_hash_match"] = False
494
- blockers.append(
495
- {
496
- "code": "final-hash-mismatch",
497
- "message": (
498
- f"HEAD={head_hash} != expected={expected_hash} "
499
- f"(source={hash_source})"
500
- ),
501
- }
497
+ # main may have advanced since this change merged (a later change
498
+ # merged on top); the change's final hash is still pushed as long as
499
+ # it is an ancestor of HEAD. Strict == would block archiving whenever
500
+ # the repo moved on, which is the normal case in a multi-change
501
+ # workflow.
502
+ anc_code, _, _ = git_run(
503
+ project, "merge-base", "--is-ancestor", expected_hash, head_hash
502
504
  )
505
+ if anc_code == 0:
506
+ checks["final_hash_match"] = True
507
+ checks["final_hash_ancestor"] = True
508
+ else:
509
+ checks["final_hash_match"] = False
510
+ blockers.append(
511
+ {
512
+ "code": "final-hash-mismatch",
513
+ "message": (
514
+ f"HEAD={head_hash} != expected={expected_hash} "
515
+ f"(source={hash_source}) and expected is not an ancestor "
516
+ f"of HEAD (not pushed?)"
517
+ ),
518
+ }
519
+ )
503
520
  elif head_hash and expected_hash is None:
504
521
  warnings.append(
505
522
  {
@@ -0,0 +1,103 @@
1
+ #!/usr/bin/env python3
2
+ """Pre-push gate: skip `npm run check` when a recent check-ok marker matches
3
+ the current HEAD.
4
+
5
+ Triple check (all must pass to skip, else exit 1 → hook runs npm run check):
6
+ 1. marker exists and its `ts` is within MAX_AGE_S (10 min) of now
7
+ 2. marker `command` == EXPECTED_CMD ("npm run check")
8
+ 3. current `git rev-parse HEAD` == marker `commitHash`
9
+
10
+ The marker is written by harness-submit M5 right after a green `npm run check`.
11
+ Any mismatch (no marker, stale, different command, HEAD moved since the check)
12
+ forces a full re-run — the safe default. This is a local convenience only;
13
+ non-harness pushes have no marker and always run the check.
14
+
15
+ Exit 0 = skip (recent verified), exit 1 = run npm run check.
16
+ """
17
+ from __future__ import annotations
18
+
19
+ import json
20
+ import subprocess
21
+ import sys
22
+ import time
23
+ from pathlib import Path
24
+
25
+ ROOT = Path(__file__).resolve().parents[2] # harness/scripts/../.. = repo root
26
+ MARKER = ROOT / ".harness" / "check-ok.marker"
27
+ MAX_AGE_S = 600 # 10 minutes
28
+ EXPECTED_CMD = "npm run check"
29
+
30
+
31
+ def _current_head() -> str | None:
32
+ try:
33
+ r = subprocess.run(
34
+ ["git", "rev-parse", "HEAD"],
35
+ cwd=str(ROOT),
36
+ capture_output=True,
37
+ text=True,
38
+ timeout=5,
39
+ )
40
+ except (OSError, subprocess.SubprocessError):
41
+ return None
42
+ return r.stdout.strip() if r.returncode == 0 else None
43
+
44
+
45
+ def write_marker() -> int:
46
+ """Write a check-ok marker for the current HEAD (called by harness-submit M5
47
+ after a green npm run check). Pre-push gate reads it to skip a re-run."""
48
+ head = _current_head()
49
+ if head is None:
50
+ return 1
51
+ MARKER.parent.mkdir(parents=True, exist_ok=True)
52
+ payload = {"ts": time.time(), "commitHash": head, "command": EXPECTED_CMD}
53
+ MARKER.write_text(
54
+ json.dumps(payload, ensure_ascii=False) + "\n",
55
+ encoding="utf-8",
56
+ newline="\n",
57
+ )
58
+ print(f"check-ok marker written for HEAD {head[:7]}")
59
+ return 0
60
+
61
+
62
+ def main(argv: list[str] | None = None) -> int:
63
+ import argparse
64
+
65
+ ap = argparse.ArgumentParser(description="pre-push check gate")
66
+ ap.add_argument(
67
+ "--write",
68
+ action="store_true",
69
+ help="write a check-ok marker for current HEAD (harness-submit M5)",
70
+ )
71
+ args = ap.parse_args(argv)
72
+ if args.write:
73
+ return write_marker()
74
+ if not MARKER.is_file():
75
+ return 1
76
+ try:
77
+ data = json.loads(MARKER.read_text(encoding="utf-8-sig"))
78
+ except (OSError, json.JSONDecodeError):
79
+ return 1
80
+ if not isinstance(data, dict):
81
+ return 1
82
+ ts = data.get("ts")
83
+ if not isinstance(ts, (int, float)) or time.time() - ts > MAX_AGE_S:
84
+ return 1
85
+ if data.get("command") != EXPECTED_CMD:
86
+ return 1
87
+ marker_commit = data.get("commitHash")
88
+ if not isinstance(marker_commit, str):
89
+ return 1
90
+ head = _current_head()
91
+ if head is None or head != marker_commit:
92
+ return 1
93
+ # All three checks passed: a green npm run check ran on this exact HEAD
94
+ # within the last 10 minutes. Safe to skip.
95
+ print(
96
+ f"pre-push: skipping npm run check (verified {int(time.time() - ts)}s ago "
97
+ f"at HEAD {head[:7]})"
98
+ )
99
+ return 0
100
+
101
+
102
+ if __name__ == "__main__":
103
+ sys.exit(main())
@@ -14,6 +14,7 @@ import json
14
14
  import os
15
15
  import re
16
16
  import shutil
17
+ import subprocess
17
18
  import sys
18
19
  import uuid
19
20
  from pathlib import Path
@@ -33,6 +34,25 @@ HEADING_RE = re.compile(r"^(#{2,6})\s+(.+?)\s*$")
33
34
  FRAGMENT_HINT_RE = re.compile(r"^>\s*片段:\[\[shared/[^\]]+\]\]\s*.*$")
34
35
 
35
36
  BUILD_MARKER = ".harness-build.json"
37
+ # Install-side managed manifests record which files a prior install owned, so a
38
+ # later conservative install can delete removed managed files while preserving
39
+ # user-owned files (design §3.8 要点4). Skills manifest lives in the skills
40
+ # target dir; agents manifest lives in .claude/agents.
41
+ MANIFEST_NAME = ".harness-managed.json"
42
+ AGENTS_MANAGED_NAME = ".harness-managed-agents.json"
43
+ INSTALL_METADATA = frozenset({MANIFEST_NAME, AGENTS_MANAGED_NAME})
44
+
45
+
46
+ def _read_managed_set(path: Path) -> set[str]:
47
+ """Return the ``managedFiles`` set from a prior install manifest, or empty."""
48
+ if not path.is_file():
49
+ return set()
50
+ try:
51
+ data = json.loads(path.read_text(encoding="utf-8"))
52
+ except (json.JSONDecodeError, ValueError):
53
+ return set()
54
+ files = data.get("managedFiles") if isinstance(data, dict) else None
55
+ return set(files) if isinstance(files, list) else set()
36
56
 
37
57
  SKIP_DIR_NAMES = {
38
58
  "__pycache__",
@@ -270,6 +290,48 @@ def validate_build_paths(skills_root: Path, out_dir: Path) -> tuple[Path, Path]:
270
290
  return skills_root, out_dir
271
291
 
272
292
 
293
+ def _git_repo_root(start: Path) -> Path | None:
294
+ """Return the git toplevel containing ``start``, or None if not a git repo."""
295
+ try:
296
+ r = subprocess.run(
297
+ ["git", "rev-parse", "--show-toplevel"],
298
+ cwd=str(start),
299
+ capture_output=True,
300
+ text=True,
301
+ timeout=5,
302
+ )
303
+ except (OSError, subprocess.SubprocessError):
304
+ return None
305
+ if r.returncode != 0 or not r.stdout.strip():
306
+ return None
307
+ return Path(r.stdout.strip())
308
+
309
+
310
+ def _git_blob_sha(path: Path, repo_root: Path) -> str | None:
311
+ """Return the git blob sha for a tracked file (CRLF-normalized by .gitattributes),
312
+ or None if untracked / git unavailable. Caller falls back to working-copy sha256
313
+ for untracked build inputs (deterministic enough outside git's normalization)."""
314
+ try:
315
+ rel = path.resolve().relative_to(repo_root.resolve())
316
+ except ValueError:
317
+ return None
318
+ try:
319
+ r = subprocess.run(
320
+ ["git", "ls-files", "-s", "--", rel.as_posix()],
321
+ cwd=str(repo_root),
322
+ capture_output=True,
323
+ text=True,
324
+ timeout=5,
325
+ )
326
+ except (OSError, subprocess.SubprocessError):
327
+ return None
328
+ if r.returncode != 0 or not r.stdout.strip():
329
+ return None
330
+ # format: "<mode> <sha> <stage>\t<path>"
331
+ parts = r.stdout.strip().splitlines()[0].split()
332
+ return parts[1] if len(parts) >= 2 else None
333
+
334
+
273
335
  def core_content_hash(
274
336
  skills_root: Path,
275
337
  overlay_dir: Path | None,
@@ -298,11 +360,18 @@ def core_content_hash(
298
360
  files.extend(p for p in overlay_dir.rglob("*") if p.is_file())
299
361
  if agent_overlay_dir and agent_overlay_dir.is_dir():
300
362
  files.extend(p for p in agent_overlay_dir.rglob("*") if p.is_file())
363
+ skills_root_resolved = skills_root.resolve()
364
+ repo_root = _git_repo_root(skills_root_resolved)
301
365
  for f in sorted(set(files)):
302
- rel = f.resolve().relative_to(skills_root.resolve()).as_posix()
366
+ rel = f.resolve().relative_to(skills_root_resolved).as_posix()
303
367
  h.update(rel.encode("utf-8"))
304
368
  h.update(b"\0")
305
- h.update(sha256_file(f).encode("ascii"))
369
+ # Prefer the git blob sha: .gitattributes eol=lf normalization makes the
370
+ # hash independent of working-copy CRLF state (the CRLF working-copy drift
371
+ # that previously caused core hash mismatches across main/worktree). Fall
372
+ # back to working-tree bytes for untracked build inputs (templates, etc.).
373
+ blob = _git_blob_sha(f, repo_root) if repo_root else None
374
+ h.update((blob or sha256_file(f)).encode("ascii"))
306
375
  h.update(b"\0")
307
376
  return h.hexdigest()[:16]
308
377
 
@@ -560,10 +629,37 @@ def cmd_install(build_out: Path, project: Path, target: Path | None) -> dict[str
560
629
  dest = (target or project / ".claude" / "skills").resolve()
561
630
  if not (build_out / BUILD_MARKER).is_file():
562
631
  raise ValueError(f"install source is not a marked harness build: {build_out}")
632
+
633
+ # Managed set = real files in the build (exclude build marker). Recorded so a
634
+ # later conservative install can delete removed managed files and preserve
635
+ # user-owned files (design §3.8 要点4). Paths are relative to dest, which
636
+ # mirrors build_out (agents/ included because copy_tree keeps it there).
637
+ managed_new = {p for p in collect_files(build_out) if p != BUILD_MARKER}
638
+ managed_agents_new = {
639
+ p[len("agents/"):]
640
+ for p in managed_new
641
+ if p.startswith("agents/") and "/" not in p[len("agents/"):]
642
+ }
643
+
563
644
  backup: str | None = None
564
645
  staging = dest.parent / f".{dest.name}.staging-{uuid.uuid4().hex[:8]}"
565
646
  shutil.copytree(build_out, staging)
566
647
  if dest.exists() and any(dest.iterdir()):
648
+ managed_old = _read_managed_set(dest / MANIFEST_NAME)
649
+ # Preserve user-owned files: anything in dest that was NOT managed by a
650
+ # prior install is copied into staging (never overwriting managed files
651
+ # that staging already carries from build_out). Removed managed files
652
+ # (in managed_old but not managed_new) are simply not carried over.
653
+ for rel in collect_files(dest):
654
+ if rel in (BUILD_MARKER, MANIFEST_NAME) or rel in INSTALL_METADATA:
655
+ continue
656
+ if rel in managed_old:
657
+ continue
658
+ dst = staging / rel
659
+ if dst.exists():
660
+ continue
661
+ dst.parent.mkdir(parents=True, exist_ok=True)
662
+ shutil.copy2(dest / rel, dst)
567
663
  stamp = dt.datetime.now().strftime("%Y%m%d-%H%M%S")
568
664
  backup_path = dest.parent / f"skills-backup-{stamp}"
569
665
  os.replace(dest, backup_path)
@@ -576,12 +672,41 @@ def cmd_install(build_out: Path, project: Path, target: Path | None) -> dict[str
576
672
  os.replace(Path(backup), dest)
577
673
  raise
578
674
 
675
+ # Record the managed set so the next conservative install can distinguish
676
+ # managed vs user-owned files.
677
+ (dest / MANIFEST_NAME).write_text(
678
+ json.dumps(
679
+ {"schemaVersion": 1, "managedFiles": sorted(managed_new)},
680
+ ensure_ascii=False,
681
+ indent=2,
682
+ )
683
+ + "\n",
684
+ encoding="utf-8",
685
+ newline="\n",
686
+ )
687
+
579
688
  agents_src = build_out / "agents"
580
689
  agents_dest = project / ".claude" / "agents"
581
690
  if agents_src.is_dir():
582
691
  agents_dest.mkdir(parents=True, exist_ok=True)
692
+ agents_old = _read_managed_set(agents_dest / AGENTS_MANAGED_NAME)
693
+ # Delete managed agents removed since the prior install; user agents
694
+ # (never in a managed set) are left untouched.
695
+ for name in list(agents_old):
696
+ if name not in managed_agents_new:
697
+ (agents_dest / name).unlink(missing_ok=True)
583
698
  for agent in agents_src.glob("*.md"):
584
699
  shutil.copy2(agent, agents_dest / agent.name)
700
+ (agents_dest / AGENTS_MANAGED_NAME).write_text(
701
+ json.dumps(
702
+ {"schemaVersion": 1, "managedFiles": sorted(managed_agents_new)},
703
+ ensure_ascii=False,
704
+ indent=2,
705
+ )
706
+ + "\n",
707
+ encoding="utf-8",
708
+ newline="\n",
709
+ )
585
710
 
586
711
  return {
587
712
  "ok": True,
@@ -613,11 +738,14 @@ def cmd_diff(build_out: Path, project: Path, target: Path | None) -> dict[str, A
613
738
  current = collect_files(installed)
614
739
  outdated = [p for p, h in built.items() if p in current and current[p] != h]
615
740
  missing = [p for p in built if p not in current]
616
- extra = [p for p in current if p not in built]
741
+ # extra includes install-side managed manifests only as metadata, so they
742
+ # are excluded from the extra set; every other untracked file is a real
743
+ # extra and must mark the install stale (design §3.8 要点3).
744
+ extra = [p for p in current if p not in built and p not in INSTALL_METADATA]
617
745
  return {
618
746
  "ok": True,
619
747
  "action": "diff",
620
- "stale": bool(outdated or missing),
748
+ "stale": bool(outdated or missing or extra),
621
749
  "outdated": sorted(outdated),
622
750
  "missing": sorted(missing),
623
751
  "extra": sorted(extra),
@@ -625,6 +753,35 @@ def cmd_diff(build_out: Path, project: Path, target: Path | None) -> dict[str, A
625
753
  }
626
754
 
627
755
 
756
+ def cmd_validate_manifest(
757
+ bundle_dir: Path, manifest_entries: list[dict[str, Any]]
758
+ ) -> dict[str, Any]:
759
+ """Compare a bundle directory's actual file set against the manifest's
760
+ declared set. Both missing (declared but absent) and extra (present but
761
+ undeclared) fail validation (design §3.8 要点2)."""
762
+ bundle_dir = bundle_dir.resolve()
763
+ actual = collect_files(bundle_dir)
764
+ declared: dict[str, str] = {}
765
+ for entry in manifest_entries:
766
+ path = entry.get("path")
767
+ sha = entry.get("sha256")
768
+ if isinstance(path, str) and isinstance(sha, str):
769
+ declared[path] = sha
770
+ missing = sorted(p for p in declared if p not in actual)
771
+ extra = sorted(p for p in actual if p not in declared)
772
+ hash_mismatch = sorted(
773
+ p for p in declared if p in actual and actual[p] != declared[p]
774
+ )
775
+ return {
776
+ "ok": not (missing or extra or hash_mismatch),
777
+ "action": "validate-manifest",
778
+ "missing": missing,
779
+ "extra": extra,
780
+ "hashMismatch": hash_mismatch,
781
+ "comparedAt": now_iso(),
782
+ }
783
+
784
+
628
785
  def build_parser() -> argparse.ArgumentParser:
629
786
  p = argparse.ArgumentParser(description="Harness skills deploy (build/install/diff)")
630
787
  sub = p.add_subparsers(dest="command", required=True)
@@ -647,6 +804,13 @@ def build_parser() -> argparse.ArgumentParser:
647
804
  d.add_argument("--project", type=Path, required=True)
648
805
  d.add_argument("--target", type=Path)
649
806
  d.add_argument("--json", action="store_true")
807
+
808
+ v = sub.add_parser(
809
+ "validate-manifest", help="Validate bundle dir vs manifest (missing/extra/hash)"
810
+ )
811
+ v.add_argument("--bundle", type=Path, required=True)
812
+ v.add_argument("--manifest", type=Path, required=True)
813
+ v.add_argument("--json", action="store_true")
650
814
  return p
651
815
 
652
816
 
@@ -662,6 +826,17 @@ def main(argv: list[str] | None = None) -> int:
662
826
  if args.command == "diff":
663
827
  result = cmd_diff(args.from_dir, args.project, args.target)
664
828
  return emit_json(result) if args.json else 0
829
+ if args.command == "validate-manifest":
830
+ manifest_data = json.loads(args.manifest.read_text(encoding="utf-8"))
831
+ entries = (
832
+ manifest_data.get("files", [])
833
+ if isinstance(manifest_data, dict)
834
+ else []
835
+ )
836
+ result = cmd_validate_manifest(args.bundle, entries)
837
+ if args.json:
838
+ return emit_json(result)
839
+ return 0 if result["ok"] else 1
665
840
  except (FileNotFoundError, KeyError, RuntimeError, ValueError) as exc:
666
841
  if getattr(args, "json", False):
667
842
  return emit_json({"ok": False, "error": str(exc)}, ok=False)