@hunter-harness/workflow-harness 0.2.9 → 0.2.10

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 (282) 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 +12 -2
  4. package/harness/bundles/general/claude-code/harness-archive/templates/summary-data-template.json +6 -2
  5. package/harness/bundles/general/claude-code/harness-codebase-map/SKILL.md +1 -1
  6. package/harness/bundles/general/claude-code/harness-knowledge-ingest/SKILL.md +3 -1
  7. package/harness/bundles/general/claude-code/harness-knowledge-ingest/reference.md +7 -0
  8. package/harness/bundles/general/claude-code/harness-knowledge-ingest/scripts/harness_knowledge.py +117 -0
  9. package/harness/bundles/general/claude-code/harness-knowledge-query/SKILL.md +1 -1
  10. package/harness/bundles/general/claude-code/harness-plan/SKILL.md +1 -1
  11. package/harness/bundles/general/claude-code/harness-review/SKILL.md +3 -3
  12. package/harness/bundles/general/claude-code/harness-run/SKILL.md +1 -1
  13. package/harness/bundles/general/claude-code/harness-run/reference.md +2 -0
  14. package/harness/bundles/general/claude-code/harness-submit/SKILL.md +15 -10
  15. package/harness/bundles/general/claude-code/harness-submit/checklist.md +40 -76
  16. package/harness/bundles/general/claude-code/harness-sync/SKILL.md +17 -2
  17. package/harness/bundles/general/claude-code/harness-sync/reference.md +2 -2
  18. package/harness/bundles/general/claude-code/harness-test/SKILL.md +1 -1
  19. package/harness/bundles/general/claude-code/harness-test/reference.md +2 -0
  20. package/harness/bundles/general/claude-code/protocols/archive-report-protocol.md +24 -4
  21. package/harness/bundles/general/claude-code/protocols/ledger-protocol.md +36 -0
  22. package/harness/bundles/general/claude-code/protocols/state-layout-protocol.md +15 -0
  23. package/harness/bundles/general/claude-code/scripts/harness_archive.py +803 -274
  24. package/harness/bundles/general/claude-code/scripts/harness_change.py +18 -0
  25. package/harness/bundles/general/claude-code/scripts/harness_deploy.py +1 -44
  26. package/harness/bundles/general/claude-code/scripts/harness_events.py +104 -6
  27. package/harness/bundles/general/claude-code/scripts/harness_gate.py +25 -4
  28. package/harness/bundles/general/claude-code/scripts/harness_integration.py +878 -0
  29. package/harness/bundles/general/claude-code/scripts/harness_ledger.py +440 -10
  30. package/harness/bundles/general/claude-code/scripts/harness_paths.py +315 -0
  31. package/harness/bundles/general/claude-code/scripts/harness_profile.py +45 -0
  32. package/harness/bundles/general/claude-code/scripts/harness_review.py +326 -0
  33. package/harness/bundles/general/claude-code/scripts/harness_sync.py +407 -0
  34. package/harness/bundles/general/claude-code/scripts/harness_test_guard.py +244 -49
  35. package/harness/bundles/general/codebuddy/.harness-build.json +1 -1
  36. package/harness/bundles/general/codebuddy/harness-archive/SKILL.md +1 -1
  37. package/harness/bundles/general/codebuddy/harness-archive/reference.md +12 -2
  38. package/harness/bundles/general/codebuddy/harness-archive/templates/summary-data-template.json +6 -2
  39. package/harness/bundles/general/codebuddy/harness-codebase-map/SKILL.md +1 -1
  40. package/harness/bundles/general/codebuddy/harness-knowledge-ingest/SKILL.md +3 -1
  41. package/harness/bundles/general/codebuddy/harness-knowledge-ingest/reference.md +7 -0
  42. package/harness/bundles/general/codebuddy/harness-knowledge-ingest/scripts/harness_knowledge.py +117 -0
  43. package/harness/bundles/general/codebuddy/harness-knowledge-query/SKILL.md +1 -1
  44. package/harness/bundles/general/codebuddy/harness-plan/SKILL.md +1 -1
  45. package/harness/bundles/general/codebuddy/harness-review/SKILL.md +3 -3
  46. package/harness/bundles/general/codebuddy/harness-run/SKILL.md +1 -1
  47. package/harness/bundles/general/codebuddy/harness-run/reference.md +2 -0
  48. package/harness/bundles/general/codebuddy/harness-submit/SKILL.md +15 -10
  49. package/harness/bundles/general/codebuddy/harness-submit/checklist.md +40 -76
  50. package/harness/bundles/general/codebuddy/harness-sync/SKILL.md +17 -2
  51. package/harness/bundles/general/codebuddy/harness-sync/reference.md +2 -2
  52. package/harness/bundles/general/codebuddy/harness-test/SKILL.md +1 -1
  53. package/harness/bundles/general/codebuddy/harness-test/reference.md +2 -0
  54. package/harness/bundles/general/codebuddy/protocols/archive-report-protocol.md +24 -4
  55. package/harness/bundles/general/codebuddy/protocols/ledger-protocol.md +36 -0
  56. package/harness/bundles/general/codebuddy/protocols/state-layout-protocol.md +15 -0
  57. package/harness/bundles/general/codebuddy/scripts/harness_archive.py +803 -274
  58. package/harness/bundles/general/codebuddy/scripts/harness_change.py +18 -0
  59. package/harness/bundles/general/codebuddy/scripts/harness_deploy.py +1 -44
  60. package/harness/bundles/general/codebuddy/scripts/harness_events.py +104 -6
  61. package/harness/bundles/general/codebuddy/scripts/harness_gate.py +25 -4
  62. package/harness/bundles/general/codebuddy/scripts/harness_integration.py +878 -0
  63. package/harness/bundles/general/codebuddy/scripts/harness_ledger.py +440 -10
  64. package/harness/bundles/general/codebuddy/scripts/harness_paths.py +315 -0
  65. package/harness/bundles/general/codebuddy/scripts/harness_profile.py +45 -0
  66. package/harness/bundles/general/codebuddy/scripts/harness_review.py +326 -0
  67. package/harness/bundles/general/codebuddy/scripts/harness_sync.py +407 -0
  68. package/harness/bundles/general/codebuddy/scripts/harness_test_guard.py +244 -49
  69. package/harness/bundles/general/codex/.harness-build.json +1 -1
  70. package/harness/bundles/general/codex/harness-archive/SKILL.md +1 -1
  71. package/harness/bundles/general/codex/harness-archive/reference.md +12 -2
  72. package/harness/bundles/general/codex/harness-archive/templates/summary-data-template.json +6 -2
  73. package/harness/bundles/general/codex/harness-codebase-map/SKILL.md +1 -1
  74. package/harness/bundles/general/codex/harness-knowledge-ingest/SKILL.md +3 -1
  75. package/harness/bundles/general/codex/harness-knowledge-ingest/reference.md +7 -0
  76. package/harness/bundles/general/codex/harness-knowledge-ingest/scripts/harness_knowledge.py +117 -0
  77. package/harness/bundles/general/codex/harness-knowledge-query/SKILL.md +1 -1
  78. package/harness/bundles/general/codex/harness-plan/SKILL.md +1 -1
  79. package/harness/bundles/general/codex/harness-review/SKILL.md +1 -1
  80. package/harness/bundles/general/codex/harness-run/SKILL.md +1 -1
  81. package/harness/bundles/general/codex/harness-run/reference.md +2 -0
  82. package/harness/bundles/general/codex/harness-submit/SKILL.md +15 -10
  83. package/harness/bundles/general/codex/harness-submit/checklist.md +40 -76
  84. package/harness/bundles/general/codex/harness-sync/SKILL.md +17 -2
  85. package/harness/bundles/general/codex/harness-sync/reference.md +2 -2
  86. package/harness/bundles/general/codex/harness-test/SKILL.md +1 -1
  87. package/harness/bundles/general/codex/harness-test/reference.md +2 -0
  88. package/harness/bundles/general/codex/protocols/archive-report-protocol.md +24 -4
  89. package/harness/bundles/general/codex/protocols/ledger-protocol.md +36 -0
  90. package/harness/bundles/general/codex/protocols/state-layout-protocol.md +15 -0
  91. package/harness/bundles/general/codex/scripts/harness_archive.py +803 -274
  92. package/harness/bundles/general/codex/scripts/harness_change.py +18 -0
  93. package/harness/bundles/general/codex/scripts/harness_deploy.py +1 -44
  94. package/harness/bundles/general/codex/scripts/harness_events.py +104 -6
  95. package/harness/bundles/general/codex/scripts/harness_gate.py +25 -4
  96. package/harness/bundles/general/codex/scripts/harness_integration.py +878 -0
  97. package/harness/bundles/general/codex/scripts/harness_ledger.py +440 -10
  98. package/harness/bundles/general/codex/scripts/harness_paths.py +315 -0
  99. package/harness/bundles/general/codex/scripts/harness_profile.py +45 -0
  100. package/harness/bundles/general/codex/scripts/harness_review.py +326 -0
  101. package/harness/bundles/general/codex/scripts/harness_sync.py +407 -0
  102. package/harness/bundles/general/codex/scripts/harness_test_guard.py +244 -49
  103. package/harness/bundles/general/cursor/.harness-build.json +1 -1
  104. package/harness/bundles/general/cursor/harness-archive/SKILL.md +1 -1
  105. package/harness/bundles/general/cursor/harness-archive/reference.md +12 -2
  106. package/harness/bundles/general/cursor/harness-archive/templates/summary-data-template.json +6 -2
  107. package/harness/bundles/general/cursor/harness-codebase-map/SKILL.md +1 -1
  108. package/harness/bundles/general/cursor/harness-knowledge-ingest/SKILL.md +3 -1
  109. package/harness/bundles/general/cursor/harness-knowledge-ingest/reference.md +7 -0
  110. package/harness/bundles/general/cursor/harness-knowledge-ingest/scripts/harness_knowledge.py +117 -0
  111. package/harness/bundles/general/cursor/harness-knowledge-query/SKILL.md +1 -1
  112. package/harness/bundles/general/cursor/harness-plan/SKILL.md +1 -1
  113. package/harness/bundles/general/cursor/harness-review/SKILL.md +1 -1
  114. package/harness/bundles/general/cursor/harness-run/SKILL.md +1 -1
  115. package/harness/bundles/general/cursor/harness-run/reference.md +2 -0
  116. package/harness/bundles/general/cursor/harness-submit/SKILL.md +15 -10
  117. package/harness/bundles/general/cursor/harness-submit/checklist.md +40 -76
  118. package/harness/bundles/general/cursor/harness-sync/SKILL.md +17 -2
  119. package/harness/bundles/general/cursor/harness-sync/reference.md +2 -2
  120. package/harness/bundles/general/cursor/harness-test/SKILL.md +1 -1
  121. package/harness/bundles/general/cursor/harness-test/reference.md +2 -0
  122. package/harness/bundles/general/cursor/protocols/archive-report-protocol.md +24 -4
  123. package/harness/bundles/general/cursor/protocols/ledger-protocol.md +36 -0
  124. package/harness/bundles/general/cursor/protocols/state-layout-protocol.md +15 -0
  125. package/harness/bundles/general/cursor/scripts/harness_archive.py +803 -274
  126. package/harness/bundles/general/cursor/scripts/harness_change.py +18 -0
  127. package/harness/bundles/general/cursor/scripts/harness_deploy.py +1 -44
  128. package/harness/bundles/general/cursor/scripts/harness_events.py +104 -6
  129. package/harness/bundles/general/cursor/scripts/harness_gate.py +25 -4
  130. package/harness/bundles/general/cursor/scripts/harness_integration.py +878 -0
  131. package/harness/bundles/general/cursor/scripts/harness_ledger.py +440 -10
  132. package/harness/bundles/general/cursor/scripts/harness_paths.py +315 -0
  133. package/harness/bundles/general/cursor/scripts/harness_profile.py +45 -0
  134. package/harness/bundles/general/cursor/scripts/harness_review.py +326 -0
  135. package/harness/bundles/general/cursor/scripts/harness_sync.py +407 -0
  136. package/harness/bundles/general/cursor/scripts/harness_test_guard.py +244 -49
  137. package/harness/bundles/java/claude-code/.harness-build.json +1 -1
  138. package/harness/bundles/java/claude-code/harness-apidoc/SKILL.md +1 -1
  139. package/harness/bundles/java/claude-code/harness-archive/SKILL.md +1 -1
  140. package/harness/bundles/java/claude-code/harness-archive/reference.md +12 -2
  141. package/harness/bundles/java/claude-code/harness-archive/templates/summary-data-template.json +6 -2
  142. package/harness/bundles/java/claude-code/harness-codebase-map/SKILL.md +1 -1
  143. package/harness/bundles/java/claude-code/harness-knowledge-ingest/SKILL.md +3 -1
  144. package/harness/bundles/java/claude-code/harness-knowledge-ingest/reference.md +7 -0
  145. package/harness/bundles/java/claude-code/harness-knowledge-ingest/scripts/harness_knowledge.py +117 -0
  146. package/harness/bundles/java/claude-code/harness-knowledge-query/SKILL.md +1 -1
  147. package/harness/bundles/java/claude-code/harness-package/SKILL.md +1 -1
  148. package/harness/bundles/java/claude-code/harness-plan/SKILL.md +1 -1
  149. package/harness/bundles/java/claude-code/harness-review/SKILL.md +3 -3
  150. package/harness/bundles/java/claude-code/harness-run/SKILL.md +1 -1
  151. package/harness/bundles/java/claude-code/harness-submit/SKILL.md +15 -10
  152. package/harness/bundles/java/claude-code/harness-submit/checklist.md +40 -76
  153. package/harness/bundles/java/claude-code/harness-sync/SKILL.md +17 -2
  154. package/harness/bundles/java/claude-code/harness-sync/reference.md +2 -2
  155. package/harness/bundles/java/claude-code/harness-test/SKILL.md +1 -1
  156. package/harness/bundles/java/claude-code/protocols/archive-report-protocol.md +24 -4
  157. package/harness/bundles/java/claude-code/protocols/ledger-protocol.md +36 -0
  158. package/harness/bundles/java/claude-code/protocols/state-layout-protocol.md +15 -0
  159. package/harness/bundles/java/claude-code/scripts/harness_archive.py +803 -274
  160. package/harness/bundles/java/claude-code/scripts/harness_change.py +18 -0
  161. package/harness/bundles/java/claude-code/scripts/harness_deploy.py +1 -44
  162. package/harness/bundles/java/claude-code/scripts/harness_events.py +104 -6
  163. package/harness/bundles/java/claude-code/scripts/harness_gate.py +25 -4
  164. package/harness/bundles/java/claude-code/scripts/harness_integration.py +878 -0
  165. package/harness/bundles/java/claude-code/scripts/harness_ledger.py +440 -10
  166. package/harness/bundles/java/claude-code/scripts/harness_paths.py +315 -0
  167. package/harness/bundles/java/claude-code/scripts/harness_profile.py +45 -0
  168. package/harness/bundles/java/claude-code/scripts/harness_review.py +326 -0
  169. package/harness/bundles/java/claude-code/scripts/harness_sync.py +407 -0
  170. package/harness/bundles/java/claude-code/scripts/harness_test_guard.py +244 -49
  171. package/harness/bundles/java/codebuddy/.harness-build.json +1 -1
  172. package/harness/bundles/java/codebuddy/harness-apidoc/SKILL.md +1 -1
  173. package/harness/bundles/java/codebuddy/harness-archive/SKILL.md +1 -1
  174. package/harness/bundles/java/codebuddy/harness-archive/reference.md +12 -2
  175. package/harness/bundles/java/codebuddy/harness-archive/templates/summary-data-template.json +6 -2
  176. package/harness/bundles/java/codebuddy/harness-codebase-map/SKILL.md +1 -1
  177. package/harness/bundles/java/codebuddy/harness-knowledge-ingest/SKILL.md +3 -1
  178. package/harness/bundles/java/codebuddy/harness-knowledge-ingest/reference.md +7 -0
  179. package/harness/bundles/java/codebuddy/harness-knowledge-ingest/scripts/harness_knowledge.py +117 -0
  180. package/harness/bundles/java/codebuddy/harness-knowledge-query/SKILL.md +1 -1
  181. package/harness/bundles/java/codebuddy/harness-package/SKILL.md +1 -1
  182. package/harness/bundles/java/codebuddy/harness-plan/SKILL.md +1 -1
  183. package/harness/bundles/java/codebuddy/harness-review/SKILL.md +3 -3
  184. package/harness/bundles/java/codebuddy/harness-run/SKILL.md +1 -1
  185. package/harness/bundles/java/codebuddy/harness-submit/SKILL.md +15 -10
  186. package/harness/bundles/java/codebuddy/harness-submit/checklist.md +40 -76
  187. package/harness/bundles/java/codebuddy/harness-sync/SKILL.md +17 -2
  188. package/harness/bundles/java/codebuddy/harness-sync/reference.md +2 -2
  189. package/harness/bundles/java/codebuddy/harness-test/SKILL.md +1 -1
  190. package/harness/bundles/java/codebuddy/protocols/archive-report-protocol.md +24 -4
  191. package/harness/bundles/java/codebuddy/protocols/ledger-protocol.md +36 -0
  192. package/harness/bundles/java/codebuddy/protocols/state-layout-protocol.md +15 -0
  193. package/harness/bundles/java/codebuddy/scripts/harness_archive.py +803 -274
  194. package/harness/bundles/java/codebuddy/scripts/harness_change.py +18 -0
  195. package/harness/bundles/java/codebuddy/scripts/harness_deploy.py +1 -44
  196. package/harness/bundles/java/codebuddy/scripts/harness_events.py +104 -6
  197. package/harness/bundles/java/codebuddy/scripts/harness_gate.py +25 -4
  198. package/harness/bundles/java/codebuddy/scripts/harness_integration.py +878 -0
  199. package/harness/bundles/java/codebuddy/scripts/harness_ledger.py +440 -10
  200. package/harness/bundles/java/codebuddy/scripts/harness_paths.py +315 -0
  201. package/harness/bundles/java/codebuddy/scripts/harness_profile.py +45 -0
  202. package/harness/bundles/java/codebuddy/scripts/harness_review.py +326 -0
  203. package/harness/bundles/java/codebuddy/scripts/harness_sync.py +407 -0
  204. package/harness/bundles/java/codebuddy/scripts/harness_test_guard.py +244 -49
  205. package/harness/bundles/java/codex/.harness-build.json +1 -1
  206. package/harness/bundles/java/codex/harness-apidoc/SKILL.md +1 -1
  207. package/harness/bundles/java/codex/harness-archive/SKILL.md +1 -1
  208. package/harness/bundles/java/codex/harness-archive/reference.md +12 -2
  209. package/harness/bundles/java/codex/harness-archive/templates/summary-data-template.json +6 -2
  210. package/harness/bundles/java/codex/harness-codebase-map/SKILL.md +1 -1
  211. package/harness/bundles/java/codex/harness-knowledge-ingest/SKILL.md +3 -1
  212. package/harness/bundles/java/codex/harness-knowledge-ingest/reference.md +7 -0
  213. package/harness/bundles/java/codex/harness-knowledge-ingest/scripts/harness_knowledge.py +117 -0
  214. package/harness/bundles/java/codex/harness-knowledge-query/SKILL.md +1 -1
  215. package/harness/bundles/java/codex/harness-package/SKILL.md +1 -1
  216. package/harness/bundles/java/codex/harness-plan/SKILL.md +1 -1
  217. package/harness/bundles/java/codex/harness-review/SKILL.md +1 -1
  218. package/harness/bundles/java/codex/harness-run/SKILL.md +1 -1
  219. package/harness/bundles/java/codex/harness-submit/SKILL.md +15 -10
  220. package/harness/bundles/java/codex/harness-submit/checklist.md +40 -76
  221. package/harness/bundles/java/codex/harness-sync/SKILL.md +17 -2
  222. package/harness/bundles/java/codex/harness-sync/reference.md +2 -2
  223. package/harness/bundles/java/codex/harness-test/SKILL.md +1 -1
  224. package/harness/bundles/java/codex/protocols/archive-report-protocol.md +24 -4
  225. package/harness/bundles/java/codex/protocols/ledger-protocol.md +36 -0
  226. package/harness/bundles/java/codex/protocols/state-layout-protocol.md +15 -0
  227. package/harness/bundles/java/codex/scripts/harness_archive.py +803 -274
  228. package/harness/bundles/java/codex/scripts/harness_change.py +18 -0
  229. package/harness/bundles/java/codex/scripts/harness_deploy.py +1 -44
  230. package/harness/bundles/java/codex/scripts/harness_events.py +104 -6
  231. package/harness/bundles/java/codex/scripts/harness_gate.py +25 -4
  232. package/harness/bundles/java/codex/scripts/harness_integration.py +878 -0
  233. package/harness/bundles/java/codex/scripts/harness_ledger.py +440 -10
  234. package/harness/bundles/java/codex/scripts/harness_paths.py +315 -0
  235. package/harness/bundles/java/codex/scripts/harness_profile.py +45 -0
  236. package/harness/bundles/java/codex/scripts/harness_review.py +326 -0
  237. package/harness/bundles/java/codex/scripts/harness_sync.py +407 -0
  238. package/harness/bundles/java/codex/scripts/harness_test_guard.py +244 -49
  239. package/harness/bundles/java/cursor/.harness-build.json +1 -1
  240. package/harness/bundles/java/cursor/harness-apidoc/SKILL.md +1 -1
  241. package/harness/bundles/java/cursor/harness-archive/SKILL.md +1 -1
  242. package/harness/bundles/java/cursor/harness-archive/reference.md +12 -2
  243. package/harness/bundles/java/cursor/harness-archive/templates/summary-data-template.json +6 -2
  244. package/harness/bundles/java/cursor/harness-codebase-map/SKILL.md +1 -1
  245. package/harness/bundles/java/cursor/harness-knowledge-ingest/SKILL.md +3 -1
  246. package/harness/bundles/java/cursor/harness-knowledge-ingest/reference.md +7 -0
  247. package/harness/bundles/java/cursor/harness-knowledge-ingest/scripts/harness_knowledge.py +117 -0
  248. package/harness/bundles/java/cursor/harness-knowledge-query/SKILL.md +1 -1
  249. package/harness/bundles/java/cursor/harness-package/SKILL.md +1 -1
  250. package/harness/bundles/java/cursor/harness-plan/SKILL.md +1 -1
  251. package/harness/bundles/java/cursor/harness-review/SKILL.md +1 -1
  252. package/harness/bundles/java/cursor/harness-run/SKILL.md +1 -1
  253. package/harness/bundles/java/cursor/harness-submit/SKILL.md +15 -10
  254. package/harness/bundles/java/cursor/harness-submit/checklist.md +40 -76
  255. package/harness/bundles/java/cursor/harness-sync/SKILL.md +17 -2
  256. package/harness/bundles/java/cursor/harness-sync/reference.md +2 -2
  257. package/harness/bundles/java/cursor/harness-test/SKILL.md +1 -1
  258. package/harness/bundles/java/cursor/protocols/archive-report-protocol.md +24 -4
  259. package/harness/bundles/java/cursor/protocols/ledger-protocol.md +36 -0
  260. package/harness/bundles/java/cursor/protocols/state-layout-protocol.md +15 -0
  261. package/harness/bundles/java/cursor/scripts/harness_archive.py +803 -274
  262. package/harness/bundles/java/cursor/scripts/harness_change.py +18 -0
  263. package/harness/bundles/java/cursor/scripts/harness_deploy.py +1 -44
  264. package/harness/bundles/java/cursor/scripts/harness_events.py +104 -6
  265. package/harness/bundles/java/cursor/scripts/harness_gate.py +25 -4
  266. package/harness/bundles/java/cursor/scripts/harness_integration.py +878 -0
  267. package/harness/bundles/java/cursor/scripts/harness_ledger.py +440 -10
  268. package/harness/bundles/java/cursor/scripts/harness_paths.py +315 -0
  269. package/harness/bundles/java/cursor/scripts/harness_profile.py +45 -0
  270. package/harness/bundles/java/cursor/scripts/harness_review.py +326 -0
  271. package/harness/bundles/java/cursor/scripts/harness_sync.py +407 -0
  272. package/harness/bundles/java/cursor/scripts/harness_test_guard.py +244 -49
  273. package/harness/manifests/general/claude-code.json +47 -31
  274. package/harness/manifests/general/codebuddy.json +47 -31
  275. package/harness/manifests/general/codex.json +47 -31
  276. package/harness/manifests/general/cursor.json +47 -31
  277. package/harness/manifests/java/claude-code.json +47 -31
  278. package/harness/manifests/java/codebuddy.json +47 -31
  279. package/harness/manifests/java/codex.json +47 -31
  280. package/harness/manifests/java/cursor.json +47 -31
  281. package/hunter-workflow-family.json +2 -2
  282. package/package.json +1 -1
@@ -15,11 +15,19 @@ import argparse
15
15
  import datetime as dt
16
16
  import hashlib
17
17
  import json
18
+ import os
18
19
  import subprocess
19
20
  import sys
21
+ import uuid
20
22
  from pathlib import Path
21
23
  from typing import Any
22
24
 
25
+ SCRIPTS_DIR = Path(__file__).resolve().parent
26
+ if str(SCRIPTS_DIR) not in sys.path:
27
+ sys.path.insert(0, str(SCRIPTS_DIR))
28
+
29
+ import harness_paths # noqa: E402
30
+
23
31
 
24
32
  if hasattr(sys.stdout, "reconfigure"):
25
33
  sys.stdout.reconfigure(encoding="utf-8")
@@ -143,6 +151,22 @@ def _git_bytes(args: list[str], cwd: Path) -> bytes:
143
151
  return proc.stdout
144
152
 
145
153
 
154
+ def _git_text(cwd: Path, *args: str) -> str | None:
155
+ """Run git, return stripped stdout; None on non-zero exit."""
156
+ proc = subprocess.run(
157
+ ["git", *args],
158
+ cwd=str(cwd),
159
+ capture_output=True,
160
+ text=True,
161
+ encoding="utf-8",
162
+ errors="replace",
163
+ check=False,
164
+ )
165
+ if proc.returncode != 0:
166
+ return None
167
+ return proc.stdout.strip()
168
+
169
+
146
170
  def _root_commit(repo_root: Path) -> str | None:
147
171
  out = _git_bytes(["rev-list", "--max-parents=0", "HEAD"], repo_root)
148
172
  lines = [ln for ln in out.decode("utf-8", "replace").splitlines() if ln.strip()]
@@ -195,10 +219,12 @@ def _tracked_test_contents(
195
219
  if not candidate.is_absolute():
196
220
  candidate = repo_root / candidate
197
221
  change_root = candidate.resolve()
198
- if not _inside(change_root, repo_root):
222
+ main_root = harness_paths.resolve_main_project_root(repo_root)
223
+ if not (_inside(change_root, repo_root) or _inside(change_root, main_root)):
199
224
  raise ValueError("TEST_TRACKING_CHANGE_DIR_OUTSIDE_PROJECT")
200
- manifest_path = (change_root / TEST_TRACKING_REL).resolve()
201
- if not _inside(manifest_path, change_root):
225
+ state_root = _state_dir(change_root)
226
+ manifest_path = (state_root / TEST_TRACKING_REL).resolve()
227
+ if not _inside(manifest_path, state_root):
202
228
  raise ValueError("TEST_TRACKING_MANIFEST_OUTSIDE_CHANGE")
203
229
  if not manifest_path.is_file():
204
230
  return {}, None
@@ -206,6 +232,8 @@ def _tracked_test_contents(
206
232
  manifest = json.loads(manifest_path.read_text(encoding="utf-8-sig"))
207
233
  except (OSError, json.JSONDecodeError) as exc:
208
234
  raise ValueError(f"TEST_TRACKING_MANIFEST_INVALID: {exc}") from exc
235
+ if isinstance(manifest, dict) and manifest.get("schemaVersion") == 2:
236
+ return _tracked_test_contents_v2(repo_root, manifest, manifest_path)
209
237
  if (
210
238
  not isinstance(manifest, dict)
211
239
  or manifest.get("schemaVersion") != 1
@@ -247,6 +275,87 @@ def _tracked_test_contents(
247
275
  return {rel: contents[rel] for rel in sorted(contents)}, str(manifest_path)
248
276
 
249
277
 
278
+ def _tracked_test_contents_v2(
279
+ repo_root: Path,
280
+ manifest: dict[str, Any],
281
+ manifest_path: Path,
282
+ ) -> tuple[dict[str, bytes], str | None]:
283
+ """Validate a schema-2 manifest: repositoryId equality + logical hashes."""
284
+ if manifest.get("mode") != "force-track-touched":
285
+ raise ValueError("TEST_TRACKING_MANIFEST_INVALID")
286
+ repository_id = manifest.get("repositoryId")
287
+ if not isinstance(repository_id, str) or not repository_id.startswith("sha256:"):
288
+ raise ValueError("TEST_TRACKING_MANIFEST_INVALID")
289
+ if repository_id != harness_paths.repository_identity(repo_root):
290
+ raise ValueError("TEST_TRACKING_REPOSITORY_MISMATCH")
291
+ entries = manifest.get("files")
292
+ if not isinstance(entries, list) or not entries:
293
+ raise ValueError("TEST_TRACKING_MANIFEST_INVALID: EMPTY_FILES")
294
+
295
+ contents: dict[str, bytes] = {}
296
+ for item in entries:
297
+ if not isinstance(item, dict):
298
+ raise ValueError("TEST_TRACKING_MANIFEST_INVALID")
299
+ rel = item.get("path")
300
+ expected = item.get("logicalHash") or item.get("binaryHash")
301
+ if (
302
+ not isinstance(rel, str)
303
+ or not rel
304
+ or not isinstance(expected, str)
305
+ or not (expected.startswith("gitblob:") or expected.startswith("sha256:"))
306
+ or item.get("reason") not in TEST_TRACKING_REASONS
307
+ or type(item.get("ignored")) is not bool
308
+ or not isinstance(item.get("introducedBy"), str)
309
+ or not isinstance(item.get("touchedBy"), list)
310
+ or item.get("commitScope") not in ("current-change", "foreign-change")
311
+ ):
312
+ raise ValueError("TEST_TRACKING_MANIFEST_INVALID")
313
+ raw_path = Path(rel)
314
+ resolved = (repo_root / raw_path).resolve()
315
+ if raw_path.is_absolute() or not _inside(resolved, repo_root):
316
+ raise ValueError(f"TEST_TRACKING_PATH_OUTSIDE_PROJECT: {rel}")
317
+ normalized = resolved.relative_to(repo_root).as_posix()
318
+ if normalized != rel or not resolved.is_file():
319
+ raise ValueError(f"TEST_TRACKING_FILE_INVALID: {rel}")
320
+ content = resolved.read_bytes()
321
+ actual = _logical_file_hash(repo_root, rel, content)
322
+ if actual != expected:
323
+ raise ValueError(f"TEST_TRACKING_HASH_DRIFT: {rel}")
324
+ contents[rel] = content
325
+ return {rel: contents[rel] for rel in sorted(contents)}, str(manifest_path)
326
+
327
+
328
+ def _logical_file_hash(repo_root: Path, rel: str, content: bytes) -> str:
329
+ """Mirror of harness_test_guard.logical_file_hash for validation."""
330
+ attr = subprocess.run(
331
+ ["git", "check-attr", "text", "--", rel],
332
+ cwd=str(repo_root),
333
+ capture_output=True,
334
+ text=True,
335
+ encoding="utf-8",
336
+ errors="replace",
337
+ check=False,
338
+ )
339
+ attr_out = attr.stdout.strip() if attr.returncode == 0 else ""
340
+ byte_hash = "sha256:" + hashlib.sha256(content).hexdigest()
341
+ if attr_out.endswith(": unset") or b"\x00" in content:
342
+ return byte_hash
343
+ try:
344
+ content.decode("utf-8")
345
+ except UnicodeDecodeError:
346
+ return byte_hash
347
+ proc = subprocess.run(
348
+ ["git", "hash-object", "--path", rel, "--stdin"],
349
+ input=content,
350
+ capture_output=True,
351
+ cwd=str(repo_root),
352
+ check=False,
353
+ )
354
+ if proc.returncode != 0:
355
+ return byte_hash
356
+ return "gitblob:" + proc.stdout.decode("ascii").strip()
357
+
358
+
250
359
  def compute_diff_hash(
251
360
  repo_root: Path,
252
361
  base: str | None = None,
@@ -367,11 +476,20 @@ def expand_profile_input_files(
367
476
  return sorted(seen), None
368
477
 
369
478
 
479
+ def _state_dir(change_dir: Path) -> Path:
480
+ return Path(harness_paths.resolve_state_dir_for_contract(change_dir))
481
+
482
+
370
483
  def ledger_candidates(change_dir: Path) -> list[Path]:
371
- return [
372
- change_dir / "evidence" / "verification-ledger.json",
373
- change_dir / "verification-ledger.json",
374
- ]
484
+ state = _state_dir(change_dir)
485
+ contract = Path(change_dir)
486
+ candidates = [state / "evidence" / "verification-ledger.json"]
487
+ if state != contract:
488
+ candidates.append(contract / "evidence" / "verification-ledger.json")
489
+ candidates.append(state / "verification-ledger.json")
490
+ if state != contract:
491
+ candidates.append(contract / "verification-ledger.json")
492
+ return candidates
375
493
 
376
494
 
377
495
  def find_ledger_path(change_dir: Path) -> Path | None:
@@ -382,8 +500,9 @@ def find_ledger_path(change_dir: Path) -> Path | None:
382
500
 
383
501
 
384
502
  def preferred_write_path(change_dir: Path) -> Path:
385
- # New writes always go to evidence/ (protocol preferred path).
386
- return change_dir / "evidence" / "verification-ledger.json"
503
+ # New writes always go to evidence/ (protocol preferred path); split-v1
504
+ # changes route to the dynamic state root.
505
+ return _state_dir(change_dir) / "evidence" / "verification-ledger.json"
387
506
 
388
507
 
389
508
  def load_ledger(change_dir: Path) -> tuple[dict[str, Any] | None, Path | None]:
@@ -402,9 +521,237 @@ def load_ledger(change_dir: Path) -> tuple[dict[str, Any] | None, Path | None]:
402
521
 
403
522
 
404
523
  def write_ledger(path: Path, data: dict[str, Any]) -> None:
524
+ """Atomic ledger write: temp -> fsync -> replace (ledger v3)."""
405
525
  path.parent.mkdir(parents=True, exist_ok=True)
406
526
  payload = json.dumps(data, ensure_ascii=False, indent=2) + "\n"
407
- path.write_text(payload, encoding="utf-8", newline="\n")
527
+ tmp = path.with_name(f".{path.name}.{os.getpid()}.{uuid.uuid4().hex}.tmp")
528
+ try:
529
+ with open(tmp, "w", encoding="utf-8", newline="\n") as handle:
530
+ handle.write(payload)
531
+ handle.flush()
532
+ os.fsync(handle.fileno())
533
+ os.replace(tmp, path)
534
+ except BaseException:
535
+ tmp.unlink(missing_ok=True)
536
+ raise
537
+
538
+
539
+ LEDGER_SCHEMA_VERSION = 3
540
+ LEDGER_IDENTITY_FIELDS = (
541
+ "repositoryId",
542
+ "changeName",
543
+ "baseCommit",
544
+ "currentHead",
545
+ "diffHash",
546
+ "ownershipHash",
547
+ )
548
+
549
+
550
+ def validate_ledger_identity(ledger: dict[str, Any]) -> list[str]:
551
+ """Missing/invalid top-level identity fields (ledger v3)."""
552
+ missing: list[str] = []
553
+ if not isinstance(ledger, dict):
554
+ return ["ledger"]
555
+ if ledger.get("schemaVersion") != LEDGER_SCHEMA_VERSION:
556
+ missing.append("schemaVersion")
557
+ for field in LEDGER_IDENTITY_FIELDS:
558
+ value = ledger.get(field)
559
+ if not isinstance(value, str) or not value.strip():
560
+ missing.append(field)
561
+ return missing
562
+
563
+
564
+ def _contract_is_v2(change_dir: Path) -> bool:
565
+ try:
566
+ contract = harness_paths.load_change_contract(change_dir)
567
+ except (OSError, ValueError):
568
+ return False
569
+ if harness_paths.contract_layout_kind(contract) == "split-v1":
570
+ return True
571
+ version = contract.get("schemaVersion")
572
+ return isinstance(version, int) and version >= 2
573
+
574
+
575
+ def ownership_hash(contract: dict[str, Any]) -> str:
576
+ ownership = contract.get("ownership") or {}
577
+ canonical = json.dumps(ownership, ensure_ascii=False, sort_keys=True)
578
+ return "sha256:" + hashlib.sha256(canonical.encode("utf-8")).hexdigest()
579
+
580
+
581
+ _METRICS_SCHEMAS: dict[str, dict[str, tuple[str, ...]]] = {
582
+ "unitTest": {"required": ("total", "passed", "failed"), "optional": ("errors", "skipped")},
583
+ "unitTestFull": {"required": ("total", "passed", "failed"), "optional": ("errors", "skipped")},
584
+ "apiContract": {"required": ("scenariosTotal", "passed", "failed"), "optional": ("blocked",)},
585
+ "browserE2E": {"required": ("total", "passed", "failed"), "optional": ("skipped", "retries")},
586
+ }
587
+
588
+
589
+ def validate_metrics(verification: str, metrics: Any) -> list[str]:
590
+ """Typed metrics schema check; unknown verification types pass through."""
591
+ problems: list[str] = []
592
+ if not isinstance(metrics, dict):
593
+ return ["metrics must be an object"]
594
+ if verification == "dbCompatibility":
595
+ applicability = metrics.get("applicability")
596
+ if applicability not in ("APPLICABLE", "NOT_APPLICABLE"):
597
+ return ["dbCompatibility.applicability must be APPLICABLE|NOT_APPLICABLE"]
598
+ if applicability == "NOT_APPLICABLE":
599
+ reason = metrics.get("reason")
600
+ if not isinstance(reason, str) or not reason.strip():
601
+ problems.append("dbCompatibility.reason required for NOT_APPLICABLE")
602
+ else:
603
+ if not isinstance(metrics.get("status"), str) or not metrics["status"]:
604
+ problems.append("dbCompatibility.status required for APPLICABLE")
605
+ return problems
606
+ schema = _METRICS_SCHEMAS.get(verification)
607
+ if schema is None:
608
+ return problems
609
+ allowed = set(schema["required"]) | set(schema["optional"])
610
+ for field in schema["required"]:
611
+ value = metrics.get(field)
612
+ if not isinstance(value, int) or isinstance(value, bool) or value < 0:
613
+ problems.append(f"metrics.{field} must be a non-negative int")
614
+ for field, value in metrics.items():
615
+ if field not in allowed:
616
+ problems.append(f"metrics.{field} is not a {verification} field")
617
+ elif not isinstance(value, int) or isinstance(value, bool) or value < 0:
618
+ problems.append(f"metrics.{field} must be a non-negative int")
619
+ return problems
620
+
621
+
622
+ def build_applicability_entry(value: str, reason: str | None = None) -> dict[str, Any]:
623
+ if value not in ("APPLICABLE", "NOT_APPLICABLE"):
624
+ raise ValueError("applicability must be APPLICABLE|NOT_APPLICABLE")
625
+ if value == "NOT_APPLICABLE" and not (isinstance(reason, str) and reason.strip()):
626
+ raise ValueError("NOT_APPLICABLE requires a scope reason")
627
+ entry: dict[str, Any] = {"applicability": value}
628
+ if reason and reason.strip():
629
+ entry["reason"] = reason.strip()
630
+ return entry
631
+
632
+
633
+ def applicability_counts_as_success(entry: dict[str, Any]) -> bool:
634
+ """Applicability never contributes to success counters (RET-24)."""
635
+ return False
636
+
637
+
638
+ def applicability_counts_as_failure(entry: dict[str, Any]) -> bool:
639
+ """NOT_APPLICABLE is not a failure; status decides for APPLICABLE."""
640
+ return False
641
+
642
+
643
+ _DYNAMIC_OWN_DIRS = ("events.ndjson", "logs", "evidence", "reports", "runtime", "backups")
644
+
645
+
646
+ def _matches_ownership_path(rel: str, declared: Any) -> bool:
647
+ scope = str(declared).replace("\\", "/").strip("/")
648
+ if not scope:
649
+ return False
650
+ normalized = rel.replace("\\", "/").strip("/")
651
+ return normalized == scope or normalized.startswith(scope + "/")
652
+
653
+
654
+ def _classify_ownership_path(
655
+ rel: str, change_name: str, ownership: dict[str, Any]
656
+ ) -> str:
657
+ """owned | staticEvidence | excludedRuntime | foreign."""
658
+ normalized = rel.replace("\\", "/")
659
+ if normalized.startswith(".harness/state/changes/"):
660
+ owner = normalized.split("/")[3] if len(normalized.split("/")) > 3 else ""
661
+ return "excludedRuntime" if owner == change_name else "foreign"
662
+ if normalized.startswith(".harness/state/"):
663
+ return "excludedRuntime"
664
+ if normalized.startswith(".harness/changes/"):
665
+ parts = normalized.split("/")
666
+ owner = parts[2] if len(parts) > 2 else ""
667
+ if owner and owner != change_name:
668
+ return "foreign"
669
+ remainder = "/".join(parts[3:]) if len(parts) > 3 else ""
670
+ head = remainder.split("/")[0] if remainder else ""
671
+ if remainder in _DYNAMIC_OWN_DIRS or head in _DYNAMIC_OWN_DIRS:
672
+ return "excludedRuntime"
673
+ for excluded in ownership.get("excludedPaths") or []:
674
+ if _matches_ownership_path(normalized, excluded):
675
+ return "excludedRuntime"
676
+ for static_path in ownership.get("staticEvidencePaths") or []:
677
+ if _matches_ownership_path(normalized, static_path):
678
+ return "staticEvidence"
679
+ for product_path in ownership.get("productPaths") or []:
680
+ if _matches_ownership_path(normalized, product_path):
681
+ return "owned"
682
+ return "foreign"
683
+
684
+
685
+ def compute_ownership_diff(
686
+ repo_root: Path, *, base: str, change_dir: Path, head: str | None = None
687
+ ) -> dict[str, Any]:
688
+ """diffHash over the change's ownership scope only (RET-18).
689
+
690
+ Excludes .harness/state/** and dynamic evidence; reports foreign change
691
+ paths separately instead of folding them into the hash.
692
+ """
693
+ repo_root = Path(repo_root).resolve()
694
+ change_dir = Path(change_dir).resolve()
695
+ try:
696
+ contract = harness_paths.load_change_contract(change_dir)
697
+ except (OSError, ValueError):
698
+ contract = {}
699
+ ownership = contract.get("ownership") or {}
700
+ diff_args = ["diff", "--name-only", base]
701
+ if head:
702
+ diff_args.append(head)
703
+ raw = _git_text(repo_root, *diff_args) or ""
704
+ owned: list[str] = []
705
+ static_evidence: list[str] = []
706
+ foreign: list[str] = []
707
+ excluded_runtime = 0
708
+ for line in raw.splitlines():
709
+ rel = line.strip()
710
+ if not rel:
711
+ continue
712
+ verdict = _classify_ownership_path(rel, change_dir.name, ownership)
713
+ if verdict == "foreign":
714
+ foreign.append(rel)
715
+ elif verdict == "staticEvidence":
716
+ static_evidence.append(rel)
717
+ elif verdict == "excludedRuntime":
718
+ excluded_runtime += 1
719
+ else:
720
+ owned.append(rel)
721
+ owned.sort()
722
+ static_evidence.sort()
723
+ foreign.sort()
724
+
725
+ hasher = hashlib.sha256()
726
+ for rel in owned:
727
+ hasher.update(rel.encode("utf-8"))
728
+ hasher.update(b"\x00")
729
+ if head:
730
+ try:
731
+ content = _git_bytes(["show", f"{head}:{rel}"], repo_root)
732
+ except RuntimeError:
733
+ content = None
734
+ hasher.update(
735
+ hashlib.sha256(content).hexdigest().encode("ascii")
736
+ if content is not None
737
+ else b"<deleted>"
738
+ )
739
+ else:
740
+ content_path = (repo_root / rel).resolve()
741
+ if content_path.is_file() and _inside(content_path, repo_root):
742
+ hasher.update(hashlib.sha256(content_path.read_bytes()).hexdigest().encode("ascii"))
743
+ else:
744
+ hasher.update(b"<deleted>")
745
+ hasher.update(b"\x00")
746
+ return {
747
+ "diffHash": "sha256:" + hasher.hexdigest(),
748
+ "files": owned,
749
+ "staticEvidenceFiles": static_evidence,
750
+ "foreignPaths": foreign,
751
+ "excludedRuntimeCount": excluded_runtime,
752
+ "ownedFileCount": len(owned),
753
+ "ownershipHash": ownership_hash(contract),
754
+ }
408
755
 
409
756
 
410
757
  def normalize_status(raw: str) -> str:
@@ -824,10 +1171,27 @@ def cmd_record(args: argparse.Namespace) -> int:
824
1171
  return emit_error("invalid --metrics-json", as_json=as_json)
825
1172
  if not isinstance(metrics_obj, dict):
826
1173
  return emit_error("invalid --metrics-json", as_json=as_json)
1174
+ if _contract_is_v2(change_dir):
1175
+ # Typed metrics schemas are ledger v3 (contract-gated); legacy
1176
+ # changes keep the loose run/failures shape (zero regression).
1177
+ problems = validate_metrics(verification, metrics_obj)
1178
+ if problems:
1179
+ return emit_error(
1180
+ "invalid metrics: " + "; ".join(problems), as_json=as_json
1181
+ )
827
1182
  entry["metrics"] = metrics_obj
828
1183
  elif "metrics" in entry:
829
1184
  # Fresh record without metrics must not keep stale metrics from prev.
830
1185
  entry.pop("metrics", None)
1186
+ applicability_raw = getattr(args, "applicability", None)
1187
+ if applicability_raw:
1188
+ try:
1189
+ entry["applicability"] = build_applicability_entry(
1190
+ str(applicability_raw).strip(),
1191
+ reason=getattr(args, "applicability_reason", None),
1192
+ )
1193
+ except ValueError as exc:
1194
+ return emit_error(str(exc), as_json=as_json)
831
1195
  if args.scope is not None and str(args.scope).strip():
832
1196
  entry["scope"] = str(args.scope).strip()
833
1197
  # No default scope: recording an incremental run as broad "module" scope
@@ -867,6 +1231,51 @@ def cmd_record(args: argparse.Namespace) -> int:
867
1231
  ledger["stateDir"] = str(change_dir)
868
1232
 
869
1233
  out_path = preferred_write_path(change_dir)
1234
+ if _contract_is_v2(change_dir):
1235
+ # Ledger v3: forced top-level identity (RET-15/16, COM-002).
1236
+ repo_root_raw = _git_text(change_dir, "rev-parse", "--show-toplevel")
1237
+ if not repo_root_raw:
1238
+ return emit_error(
1239
+ "record failed: change dir is not inside a git repository",
1240
+ as_json=as_json,
1241
+ )
1242
+ repo_root = Path(repo_root_raw).resolve()
1243
+ try:
1244
+ contract = harness_paths.load_change_contract(change_dir)
1245
+ except (OSError, ValueError) as exc:
1246
+ return emit_error(f"record failed: {exc}", as_json=as_json)
1247
+ base_commit = getattr(args, "base_commit", None)
1248
+ if not _nonempty_str(base_commit):
1249
+ base_commit = ledger.get("baseCommit")
1250
+ if not _nonempty_str(base_commit):
1251
+ base_commit = _git_text(repo_root, "rev-parse", "--verify", "HEAD")
1252
+ current_head = _git_text(repo_root, "rev-parse", "--verify", "HEAD")
1253
+ diff_hash = getattr(args, "diff_hash", None)
1254
+ if not _nonempty_str(diff_hash):
1255
+ diff_hash = ledger.get("diffHash")
1256
+ if not _nonempty_str(diff_hash) and _nonempty_str(base_commit):
1257
+ try:
1258
+ diff_hash = compute_ownership_diff(
1259
+ repo_root, base=str(base_commit).strip(), change_dir=change_dir
1260
+ )["diffHash"]
1261
+ except (OSError, ValueError, RuntimeError):
1262
+ diff_hash = None
1263
+ ledger["schemaVersion"] = LEDGER_SCHEMA_VERSION
1264
+ ledger["repositoryId"] = harness_paths.repository_identity(repo_root)
1265
+ ledger["changeName"] = change_dir.name
1266
+ if _nonempty_str(base_commit):
1267
+ ledger["baseCommit"] = str(base_commit).strip()
1268
+ if _nonempty_str(current_head):
1269
+ ledger["currentHead"] = str(current_head).strip()
1270
+ if _nonempty_str(diff_hash):
1271
+ ledger["diffHash"] = str(diff_hash).strip()
1272
+ ledger["ownershipHash"] = ownership_hash(contract)
1273
+ missing = validate_ledger_identity(ledger)
1274
+ if missing:
1275
+ return emit_error(
1276
+ "ledger identity incomplete: " + ", ".join(missing),
1277
+ as_json=as_json,
1278
+ )
870
1279
  write_ledger(out_path, ledger)
871
1280
  except (OSError, ValueError, FileNotFoundError, json.JSONDecodeError) as exc:
872
1281
  return emit_error(f"record failed: {exc}", as_json=as_json)
@@ -1031,6 +1440,27 @@ def build_parser() -> argparse.ArgumentParser:
1031
1440
  default=None,
1032
1441
  help='structured counts, e.g. \'{"run":155,"failures":0}\' or \'{"total":3,"passed":3}\'',
1033
1442
  )
1443
+ p_record.add_argument(
1444
+ "--base-commit",
1445
+ default=None,
1446
+ help="ledger v3: base commit for identity (default: existing ledger value, else HEAD)",
1447
+ )
1448
+ p_record.add_argument(
1449
+ "--diff-hash",
1450
+ default=None,
1451
+ help="ledger v3: precomputed ownership diff hash (default: existing, else computed)",
1452
+ )
1453
+ p_record.add_argument(
1454
+ "--applicability",
1455
+ choices=("APPLICABLE", "NOT_APPLICABLE"),
1456
+ default=None,
1457
+ help="ledger v3: applicability of this verification to the change",
1458
+ )
1459
+ p_record.add_argument(
1460
+ "--applicability-reason",
1461
+ default=None,
1462
+ help="ledger v3: scope reason (required when --applicability NOT_APPLICABLE)",
1463
+ )
1034
1464
  p_record.set_defaults(func=cmd_record)
1035
1465
 
1036
1466
  p_diff = sub.add_parser(