@hunter-harness/workflow-harness 0.2.11 → 0.2.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (250) hide show
  1. package/harness/bundles/general/claude-code/.harness-build.json +1 -1
  2. package/harness/bundles/general/claude-code/contracts/ci-metrics.schema.json +36 -0
  3. package/harness/bundles/general/claude-code/contracts/workflow-policy.json +30 -0
  4. package/harness/bundles/general/claude-code/harness-archive/SKILL.md +1 -1
  5. package/harness/bundles/general/claude-code/harness-codebase-map/SKILL.md +1 -1
  6. package/harness/bundles/general/claude-code/harness-knowledge-ingest/SKILL.md +1 -1
  7. package/harness/bundles/general/claude-code/harness-knowledge-query/SKILL.md +1 -1
  8. package/harness/bundles/general/claude-code/harness-plan/SKILL.md +4 -4
  9. package/harness/bundles/general/claude-code/harness-plan/checklist.md +6 -0
  10. package/harness/bundles/general/claude-code/harness-plan/reference.md +7 -2
  11. package/harness/bundles/general/claude-code/harness-review/SKILL.md +1 -1
  12. package/harness/bundles/general/claude-code/harness-run/SKILL.md +1 -1
  13. package/harness/bundles/general/claude-code/harness-run/checklist.md +3 -2
  14. package/harness/bundles/general/claude-code/harness-run/reference.md +6 -4
  15. package/harness/bundles/general/claude-code/harness-submit/SKILL.md +1 -1
  16. package/harness/bundles/general/claude-code/harness-sync/SKILL.md +2 -2
  17. package/harness/bundles/general/claude-code/harness-sync/reference.md +5 -4
  18. package/harness/bundles/general/claude-code/harness-test/SKILL.md +1 -1
  19. package/harness/bundles/general/claude-code/scripts/harness_archive.py +282 -120
  20. package/harness/bundles/general/claude-code/scripts/harness_events.py +400 -24
  21. package/harness/bundles/general/claude-code/scripts/harness_gate.py +585 -21
  22. package/harness/bundles/general/claude-code/scripts/harness_integration.py +403 -18
  23. package/harness/bundles/general/claude-code/scripts/harness_ledger.py +225 -29
  24. package/harness/bundles/general/claude-code/scripts/harness_phase.py +926 -0
  25. package/harness/bundles/general/claude-code/scripts/harness_plan_finalize.py +361 -0
  26. package/harness/bundles/general/claude-code/scripts/harness_profile.py +50 -0
  27. package/harness/bundles/general/claude-code/scripts/harness_runtime.py +248 -0
  28. package/harness/bundles/general/claude-code/scripts/harness_state.py +15 -2
  29. package/harness/bundles/general/claude-code/scripts/harness_test_guard.py +188 -0
  30. package/harness/bundles/general/claude-code/scripts/harness_workflow_policy.py +19 -0
  31. package/harness/bundles/general/codebuddy/.harness-build.json +1 -1
  32. package/harness/bundles/general/codebuddy/contracts/ci-metrics.schema.json +36 -0
  33. package/harness/bundles/general/codebuddy/contracts/workflow-policy.json +30 -0
  34. package/harness/bundles/general/codebuddy/harness-archive/SKILL.md +1 -1
  35. package/harness/bundles/general/codebuddy/harness-codebase-map/SKILL.md +1 -1
  36. package/harness/bundles/general/codebuddy/harness-knowledge-ingest/SKILL.md +1 -1
  37. package/harness/bundles/general/codebuddy/harness-knowledge-query/SKILL.md +1 -1
  38. package/harness/bundles/general/codebuddy/harness-plan/SKILL.md +4 -4
  39. package/harness/bundles/general/codebuddy/harness-plan/checklist.md +6 -0
  40. package/harness/bundles/general/codebuddy/harness-plan/reference.md +7 -2
  41. package/harness/bundles/general/codebuddy/harness-review/SKILL.md +1 -1
  42. package/harness/bundles/general/codebuddy/harness-run/SKILL.md +1 -1
  43. package/harness/bundles/general/codebuddy/harness-run/checklist.md +3 -2
  44. package/harness/bundles/general/codebuddy/harness-run/reference.md +6 -4
  45. package/harness/bundles/general/codebuddy/harness-submit/SKILL.md +1 -1
  46. package/harness/bundles/general/codebuddy/harness-sync/SKILL.md +2 -2
  47. package/harness/bundles/general/codebuddy/harness-sync/reference.md +5 -4
  48. package/harness/bundles/general/codebuddy/harness-test/SKILL.md +1 -1
  49. package/harness/bundles/general/codebuddy/scripts/harness_archive.py +282 -120
  50. package/harness/bundles/general/codebuddy/scripts/harness_events.py +400 -24
  51. package/harness/bundles/general/codebuddy/scripts/harness_gate.py +585 -21
  52. package/harness/bundles/general/codebuddy/scripts/harness_integration.py +403 -18
  53. package/harness/bundles/general/codebuddy/scripts/harness_ledger.py +225 -29
  54. package/harness/bundles/general/codebuddy/scripts/harness_phase.py +926 -0
  55. package/harness/bundles/general/codebuddy/scripts/harness_plan_finalize.py +361 -0
  56. package/harness/bundles/general/codebuddy/scripts/harness_profile.py +50 -0
  57. package/harness/bundles/general/codebuddy/scripts/harness_runtime.py +248 -0
  58. package/harness/bundles/general/codebuddy/scripts/harness_state.py +15 -2
  59. package/harness/bundles/general/codebuddy/scripts/harness_test_guard.py +188 -0
  60. package/harness/bundles/general/codebuddy/scripts/harness_workflow_policy.py +19 -0
  61. package/harness/bundles/general/codex/.harness-build.json +1 -1
  62. package/harness/bundles/general/codex/contracts/ci-metrics.schema.json +36 -0
  63. package/harness/bundles/general/codex/contracts/workflow-policy.json +30 -0
  64. package/harness/bundles/general/codex/harness-archive/SKILL.md +1 -1
  65. package/harness/bundles/general/codex/harness-codebase-map/SKILL.md +1 -1
  66. package/harness/bundles/general/codex/harness-knowledge-ingest/SKILL.md +1 -1
  67. package/harness/bundles/general/codex/harness-knowledge-query/SKILL.md +1 -1
  68. package/harness/bundles/general/codex/harness-plan/SKILL.md +4 -4
  69. package/harness/bundles/general/codex/harness-plan/checklist.md +6 -0
  70. package/harness/bundles/general/codex/harness-plan/reference.md +7 -2
  71. package/harness/bundles/general/codex/harness-review/SKILL.md +1 -1
  72. package/harness/bundles/general/codex/harness-run/SKILL.md +1 -1
  73. package/harness/bundles/general/codex/harness-run/checklist.md +3 -2
  74. package/harness/bundles/general/codex/harness-run/reference.md +6 -4
  75. package/harness/bundles/general/codex/harness-submit/SKILL.md +1 -1
  76. package/harness/bundles/general/codex/harness-sync/SKILL.md +2 -2
  77. package/harness/bundles/general/codex/harness-sync/reference.md +5 -4
  78. package/harness/bundles/general/codex/harness-test/SKILL.md +1 -1
  79. package/harness/bundles/general/codex/scripts/harness_archive.py +282 -120
  80. package/harness/bundles/general/codex/scripts/harness_events.py +400 -24
  81. package/harness/bundles/general/codex/scripts/harness_gate.py +585 -21
  82. package/harness/bundles/general/codex/scripts/harness_integration.py +403 -18
  83. package/harness/bundles/general/codex/scripts/harness_ledger.py +225 -29
  84. package/harness/bundles/general/codex/scripts/harness_phase.py +926 -0
  85. package/harness/bundles/general/codex/scripts/harness_plan_finalize.py +361 -0
  86. package/harness/bundles/general/codex/scripts/harness_profile.py +50 -0
  87. package/harness/bundles/general/codex/scripts/harness_runtime.py +248 -0
  88. package/harness/bundles/general/codex/scripts/harness_state.py +15 -2
  89. package/harness/bundles/general/codex/scripts/harness_test_guard.py +188 -0
  90. package/harness/bundles/general/codex/scripts/harness_workflow_policy.py +19 -0
  91. package/harness/bundles/general/cursor/.harness-build.json +1 -1
  92. package/harness/bundles/general/cursor/contracts/ci-metrics.schema.json +36 -0
  93. package/harness/bundles/general/cursor/contracts/workflow-policy.json +30 -0
  94. package/harness/bundles/general/cursor/harness-archive/SKILL.md +1 -1
  95. package/harness/bundles/general/cursor/harness-codebase-map/SKILL.md +1 -1
  96. package/harness/bundles/general/cursor/harness-knowledge-ingest/SKILL.md +1 -1
  97. package/harness/bundles/general/cursor/harness-knowledge-query/SKILL.md +1 -1
  98. package/harness/bundles/general/cursor/harness-plan/SKILL.md +4 -4
  99. package/harness/bundles/general/cursor/harness-plan/checklist.md +6 -0
  100. package/harness/bundles/general/cursor/harness-plan/reference.md +7 -2
  101. package/harness/bundles/general/cursor/harness-review/SKILL.md +1 -1
  102. package/harness/bundles/general/cursor/harness-run/SKILL.md +1 -1
  103. package/harness/bundles/general/cursor/harness-run/checklist.md +3 -2
  104. package/harness/bundles/general/cursor/harness-run/reference.md +6 -4
  105. package/harness/bundles/general/cursor/harness-submit/SKILL.md +1 -1
  106. package/harness/bundles/general/cursor/harness-sync/SKILL.md +2 -2
  107. package/harness/bundles/general/cursor/harness-sync/reference.md +5 -4
  108. package/harness/bundles/general/cursor/harness-test/SKILL.md +1 -1
  109. package/harness/bundles/general/cursor/scripts/harness_archive.py +282 -120
  110. package/harness/bundles/general/cursor/scripts/harness_events.py +400 -24
  111. package/harness/bundles/general/cursor/scripts/harness_gate.py +585 -21
  112. package/harness/bundles/general/cursor/scripts/harness_integration.py +403 -18
  113. package/harness/bundles/general/cursor/scripts/harness_ledger.py +225 -29
  114. package/harness/bundles/general/cursor/scripts/harness_phase.py +926 -0
  115. package/harness/bundles/general/cursor/scripts/harness_plan_finalize.py +361 -0
  116. package/harness/bundles/general/cursor/scripts/harness_profile.py +50 -0
  117. package/harness/bundles/general/cursor/scripts/harness_runtime.py +248 -0
  118. package/harness/bundles/general/cursor/scripts/harness_state.py +15 -2
  119. package/harness/bundles/general/cursor/scripts/harness_test_guard.py +188 -0
  120. package/harness/bundles/general/cursor/scripts/harness_workflow_policy.py +19 -0
  121. package/harness/bundles/java/claude-code/.harness-build.json +1 -1
  122. package/harness/bundles/java/claude-code/contracts/ci-metrics.schema.json +36 -0
  123. package/harness/bundles/java/claude-code/contracts/workflow-policy.json +30 -0
  124. package/harness/bundles/java/claude-code/harness-apidoc/SKILL.md +1 -1
  125. package/harness/bundles/java/claude-code/harness-archive/SKILL.md +1 -1
  126. package/harness/bundles/java/claude-code/harness-codebase-map/SKILL.md +1 -1
  127. package/harness/bundles/java/claude-code/harness-knowledge-ingest/SKILL.md +1 -1
  128. package/harness/bundles/java/claude-code/harness-knowledge-query/SKILL.md +1 -1
  129. package/harness/bundles/java/claude-code/harness-package/SKILL.md +1 -1
  130. package/harness/bundles/java/claude-code/harness-plan/SKILL.md +4 -4
  131. package/harness/bundles/java/claude-code/harness-plan/checklist.md +6 -0
  132. package/harness/bundles/java/claude-code/harness-plan/reference.md +7 -2
  133. package/harness/bundles/java/claude-code/harness-review/SKILL.md +1 -1
  134. package/harness/bundles/java/claude-code/harness-run/SKILL.md +1 -1
  135. package/harness/bundles/java/claude-code/harness-submit/SKILL.md +1 -1
  136. package/harness/bundles/java/claude-code/harness-sync/SKILL.md +2 -2
  137. package/harness/bundles/java/claude-code/harness-sync/reference.md +5 -4
  138. package/harness/bundles/java/claude-code/harness-test/SKILL.md +1 -1
  139. package/harness/bundles/java/claude-code/scripts/harness_archive.py +282 -120
  140. package/harness/bundles/java/claude-code/scripts/harness_events.py +400 -24
  141. package/harness/bundles/java/claude-code/scripts/harness_gate.py +585 -21
  142. package/harness/bundles/java/claude-code/scripts/harness_integration.py +403 -18
  143. package/harness/bundles/java/claude-code/scripts/harness_ledger.py +225 -29
  144. package/harness/bundles/java/claude-code/scripts/harness_phase.py +926 -0
  145. package/harness/bundles/java/claude-code/scripts/harness_plan_finalize.py +361 -0
  146. package/harness/bundles/java/claude-code/scripts/harness_profile.py +50 -0
  147. package/harness/bundles/java/claude-code/scripts/harness_runtime.py +248 -0
  148. package/harness/bundles/java/claude-code/scripts/harness_state.py +15 -2
  149. package/harness/bundles/java/claude-code/scripts/harness_test_guard.py +188 -0
  150. package/harness/bundles/java/claude-code/scripts/harness_workflow_policy.py +19 -0
  151. package/harness/bundles/java/codebuddy/.harness-build.json +1 -1
  152. package/harness/bundles/java/codebuddy/contracts/ci-metrics.schema.json +36 -0
  153. package/harness/bundles/java/codebuddy/contracts/workflow-policy.json +30 -0
  154. package/harness/bundles/java/codebuddy/harness-apidoc/SKILL.md +1 -1
  155. package/harness/bundles/java/codebuddy/harness-archive/SKILL.md +1 -1
  156. package/harness/bundles/java/codebuddy/harness-codebase-map/SKILL.md +1 -1
  157. package/harness/bundles/java/codebuddy/harness-knowledge-ingest/SKILL.md +1 -1
  158. package/harness/bundles/java/codebuddy/harness-knowledge-query/SKILL.md +1 -1
  159. package/harness/bundles/java/codebuddy/harness-package/SKILL.md +1 -1
  160. package/harness/bundles/java/codebuddy/harness-plan/SKILL.md +4 -4
  161. package/harness/bundles/java/codebuddy/harness-plan/checklist.md +6 -0
  162. package/harness/bundles/java/codebuddy/harness-plan/reference.md +7 -2
  163. package/harness/bundles/java/codebuddy/harness-review/SKILL.md +1 -1
  164. package/harness/bundles/java/codebuddy/harness-run/SKILL.md +1 -1
  165. package/harness/bundles/java/codebuddy/harness-submit/SKILL.md +1 -1
  166. package/harness/bundles/java/codebuddy/harness-sync/SKILL.md +2 -2
  167. package/harness/bundles/java/codebuddy/harness-sync/reference.md +5 -4
  168. package/harness/bundles/java/codebuddy/harness-test/SKILL.md +1 -1
  169. package/harness/bundles/java/codebuddy/scripts/harness_archive.py +282 -120
  170. package/harness/bundles/java/codebuddy/scripts/harness_events.py +400 -24
  171. package/harness/bundles/java/codebuddy/scripts/harness_gate.py +585 -21
  172. package/harness/bundles/java/codebuddy/scripts/harness_integration.py +403 -18
  173. package/harness/bundles/java/codebuddy/scripts/harness_ledger.py +225 -29
  174. package/harness/bundles/java/codebuddy/scripts/harness_phase.py +926 -0
  175. package/harness/bundles/java/codebuddy/scripts/harness_plan_finalize.py +361 -0
  176. package/harness/bundles/java/codebuddy/scripts/harness_profile.py +50 -0
  177. package/harness/bundles/java/codebuddy/scripts/harness_runtime.py +248 -0
  178. package/harness/bundles/java/codebuddy/scripts/harness_state.py +15 -2
  179. package/harness/bundles/java/codebuddy/scripts/harness_test_guard.py +188 -0
  180. package/harness/bundles/java/codebuddy/scripts/harness_workflow_policy.py +19 -0
  181. package/harness/bundles/java/codex/.harness-build.json +1 -1
  182. package/harness/bundles/java/codex/contracts/ci-metrics.schema.json +36 -0
  183. package/harness/bundles/java/codex/contracts/workflow-policy.json +30 -0
  184. package/harness/bundles/java/codex/harness-apidoc/SKILL.md +1 -1
  185. package/harness/bundles/java/codex/harness-archive/SKILL.md +1 -1
  186. package/harness/bundles/java/codex/harness-codebase-map/SKILL.md +1 -1
  187. package/harness/bundles/java/codex/harness-knowledge-ingest/SKILL.md +1 -1
  188. package/harness/bundles/java/codex/harness-knowledge-query/SKILL.md +1 -1
  189. package/harness/bundles/java/codex/harness-package/SKILL.md +1 -1
  190. package/harness/bundles/java/codex/harness-plan/SKILL.md +4 -4
  191. package/harness/bundles/java/codex/harness-plan/checklist.md +6 -0
  192. package/harness/bundles/java/codex/harness-plan/reference.md +7 -2
  193. package/harness/bundles/java/codex/harness-review/SKILL.md +1 -1
  194. package/harness/bundles/java/codex/harness-run/SKILL.md +1 -1
  195. package/harness/bundles/java/codex/harness-submit/SKILL.md +1 -1
  196. package/harness/bundles/java/codex/harness-sync/SKILL.md +2 -2
  197. package/harness/bundles/java/codex/harness-sync/reference.md +5 -4
  198. package/harness/bundles/java/codex/harness-test/SKILL.md +1 -1
  199. package/harness/bundles/java/codex/scripts/harness_archive.py +282 -120
  200. package/harness/bundles/java/codex/scripts/harness_events.py +400 -24
  201. package/harness/bundles/java/codex/scripts/harness_gate.py +585 -21
  202. package/harness/bundles/java/codex/scripts/harness_integration.py +403 -18
  203. package/harness/bundles/java/codex/scripts/harness_ledger.py +225 -29
  204. package/harness/bundles/java/codex/scripts/harness_phase.py +926 -0
  205. package/harness/bundles/java/codex/scripts/harness_plan_finalize.py +361 -0
  206. package/harness/bundles/java/codex/scripts/harness_profile.py +50 -0
  207. package/harness/bundles/java/codex/scripts/harness_runtime.py +248 -0
  208. package/harness/bundles/java/codex/scripts/harness_state.py +15 -2
  209. package/harness/bundles/java/codex/scripts/harness_test_guard.py +188 -0
  210. package/harness/bundles/java/codex/scripts/harness_workflow_policy.py +19 -0
  211. package/harness/bundles/java/cursor/.harness-build.json +1 -1
  212. package/harness/bundles/java/cursor/contracts/ci-metrics.schema.json +36 -0
  213. package/harness/bundles/java/cursor/contracts/workflow-policy.json +30 -0
  214. package/harness/bundles/java/cursor/harness-apidoc/SKILL.md +1 -1
  215. package/harness/bundles/java/cursor/harness-archive/SKILL.md +1 -1
  216. package/harness/bundles/java/cursor/harness-codebase-map/SKILL.md +1 -1
  217. package/harness/bundles/java/cursor/harness-knowledge-ingest/SKILL.md +1 -1
  218. package/harness/bundles/java/cursor/harness-knowledge-query/SKILL.md +1 -1
  219. package/harness/bundles/java/cursor/harness-package/SKILL.md +1 -1
  220. package/harness/bundles/java/cursor/harness-plan/SKILL.md +4 -4
  221. package/harness/bundles/java/cursor/harness-plan/checklist.md +6 -0
  222. package/harness/bundles/java/cursor/harness-plan/reference.md +7 -2
  223. package/harness/bundles/java/cursor/harness-review/SKILL.md +1 -1
  224. package/harness/bundles/java/cursor/harness-run/SKILL.md +1 -1
  225. package/harness/bundles/java/cursor/harness-submit/SKILL.md +1 -1
  226. package/harness/bundles/java/cursor/harness-sync/SKILL.md +2 -2
  227. package/harness/bundles/java/cursor/harness-sync/reference.md +5 -4
  228. package/harness/bundles/java/cursor/harness-test/SKILL.md +1 -1
  229. package/harness/bundles/java/cursor/scripts/harness_archive.py +282 -120
  230. package/harness/bundles/java/cursor/scripts/harness_events.py +400 -24
  231. package/harness/bundles/java/cursor/scripts/harness_gate.py +585 -21
  232. package/harness/bundles/java/cursor/scripts/harness_integration.py +403 -18
  233. package/harness/bundles/java/cursor/scripts/harness_ledger.py +225 -29
  234. package/harness/bundles/java/cursor/scripts/harness_phase.py +926 -0
  235. package/harness/bundles/java/cursor/scripts/harness_plan_finalize.py +361 -0
  236. package/harness/bundles/java/cursor/scripts/harness_profile.py +50 -0
  237. package/harness/bundles/java/cursor/scripts/harness_runtime.py +248 -0
  238. package/harness/bundles/java/cursor/scripts/harness_state.py +15 -2
  239. package/harness/bundles/java/cursor/scripts/harness_test_guard.py +188 -0
  240. package/harness/bundles/java/cursor/scripts/harness_workflow_policy.py +19 -0
  241. package/harness/manifests/general/claude-code.json +43 -27
  242. package/harness/manifests/general/codebuddy.json +43 -27
  243. package/harness/manifests/general/codex.json +43 -27
  244. package/harness/manifests/general/cursor.json +43 -27
  245. package/harness/manifests/java/claude-code.json +43 -27
  246. package/harness/manifests/java/codebuddy.json +43 -27
  247. package/harness/manifests/java/codex.json +43 -27
  248. package/harness/manifests/java/cursor.json +43 -27
  249. package/hunter-workflow-family.json +2 -2
  250. package/package.json +1 -1
@@ -16,6 +16,7 @@ import datetime as dt
16
16
  import hashlib
17
17
  import json
18
18
  import os
19
+ import re
19
20
  import subprocess
20
21
  import sys
21
22
  import uuid
@@ -35,7 +36,17 @@ if hasattr(sys.stderr, "reconfigure"):
35
36
  sys.stderr.reconfigure(encoding="utf-8")
36
37
 
37
38
 
38
- VERIFICATIONS = frozenset({"compile", "unitTest", "unitTestFull", "apiTest", "install", "package"})
39
+ VERIFICATIONS = frozenset(
40
+ {
41
+ "compile",
42
+ "unitTest",
43
+ "unitTestFull",
44
+ "apiTest",
45
+ "install",
46
+ "package",
47
+ "dbCompatibility",
48
+ }
49
+ )
39
50
  STATUS_MAP = {
40
51
  "ok": "OK",
41
52
  "OK": "OK",
@@ -62,6 +73,7 @@ REQUIRED_COVERAGE = {
62
73
  "apiTest": 1,
63
74
  "install": 2, # module-am or broader
64
75
  "package": 2,
76
+ "dbCompatibility": 1,
65
77
  }
66
78
  # git empty-tree object id (used as base fallback when no commit exists).
67
79
  _EMPTY_TREE = "4b825dc642cb6eb9a060e54bf8d69288fbee4904"
@@ -108,6 +120,51 @@ def parse_files_arg(raw: str | None) -> list[str]:
108
120
  return [p for p in parts if p]
109
121
 
110
122
 
123
+ def parse_files_manifest(raw: str | None) -> list[str]:
124
+ if raw is None or not str(raw).strip():
125
+ return []
126
+ path = Path(str(raw)).expanduser().resolve()
127
+ if not path.is_file():
128
+ raise FileNotFoundError(f"files manifest not found: {path}")
129
+ return [
130
+ line.strip()
131
+ for line in path.read_text(encoding="utf-8-sig").splitlines()
132
+ if line.strip() and not line.lstrip().startswith("#")
133
+ ]
134
+
135
+
136
+ def resolve_input_files(
137
+ files: list[str], project_root: Path | None
138
+ ) -> list[str]:
139
+ if project_root is None:
140
+ return files
141
+ root = project_root.expanduser().resolve()
142
+ resolved: list[str] = []
143
+ for raw in files:
144
+ candidate = Path(raw).expanduser()
145
+ candidate = candidate.resolve() if candidate.is_absolute() else (root / candidate).resolve()
146
+ try:
147
+ candidate.relative_to(root)
148
+ except ValueError as exc:
149
+ raise ValueError(
150
+ f"INPUT_OUTSIDE_PROJECT: {candidate} is outside {root}"
151
+ ) from exc
152
+ resolved.append(str(candidate))
153
+ return resolved
154
+
155
+
156
+ def input_files_from_args(args: argparse.Namespace) -> tuple[list[str], Path | None]:
157
+ project_raw = getattr(args, "project", None)
158
+ project_root = (
159
+ Path(str(project_raw)).expanduser().resolve() if project_raw else None
160
+ )
161
+ files = parse_files_arg(getattr(args, "files", None))
162
+ manifest = parse_files_manifest(getattr(args, "files_from", None))
163
+ if files and manifest:
164
+ raise ValueError("use only one of --files and --files-from")
165
+ return resolve_input_files(files or manifest, project_root), project_root
166
+
167
+
111
168
  def sha256_file(path: Path) -> str:
112
169
  digest = hashlib.sha256()
113
170
  with path.open("rb") as handle:
@@ -561,6 +618,68 @@ def validate_ledger_identity(ledger: dict[str, Any]) -> list[str]:
561
618
  return missing
562
619
 
563
620
 
621
+ def record_integration_hashes(
622
+ ledger_path: Path,
623
+ *,
624
+ repository_id: str,
625
+ merge_final_hash: str,
626
+ ci_expected_head: str,
627
+ remote_head: str,
628
+ ) -> dict[str, Any]:
629
+ """Atomically attach post-push identity to an existing ledger v3."""
630
+ path = Path(ledger_path).resolve()
631
+ if not path.is_file():
632
+ return {"ok": False, "code": "LEDGER_MISSING", "ledgerPath": str(path)}
633
+ try:
634
+ ledger = json.loads(path.read_text(encoding="utf-8-sig"))
635
+ except (OSError, json.JSONDecodeError) as exc:
636
+ return {
637
+ "ok": False, "code": "LEDGER_INVALID", "ledgerPath": str(path),
638
+ "message": str(exc),
639
+ }
640
+ missing = validate_ledger_identity(ledger)
641
+ if missing:
642
+ return {
643
+ "ok": False,
644
+ "code": "LEDGER_IDENTITY_INVALID",
645
+ "ledgerPath": str(path),
646
+ "missing": missing,
647
+ }
648
+ if ledger.get("repositoryId") != repository_id:
649
+ return {
650
+ "ok": False,
651
+ "code": "LEDGER_REPOSITORY_MISMATCH",
652
+ "ledgerPath": str(path),
653
+ }
654
+ values = {
655
+ "mergeFinalHash": merge_final_hash,
656
+ "ciExpectedHead": ci_expected_head,
657
+ "remoteHead": remote_head,
658
+ }
659
+ invalid = [
660
+ field for field, value in values.items()
661
+ if not isinstance(value, str) or re.fullmatch(r"[0-9a-f]{40}", value) is None
662
+ ]
663
+ if invalid:
664
+ return {
665
+ "ok": False,
666
+ "code": "FINAL_HASH_INVALID",
667
+ "ledgerPath": str(path),
668
+ "invalid": invalid,
669
+ }
670
+ if len(set(values.values())) != 1:
671
+ return {
672
+ "ok": False,
673
+ "code": "FINAL_HASH_MISMATCH",
674
+ "ledgerPath": str(path),
675
+ **values,
676
+ }
677
+ ledger.update(values)
678
+ ledger["integrationFinalizedAt"] = now_iso()
679
+ write_ledger(path, ledger)
680
+ return {"ok": True, "code": "INTEGRATION_HASHES_RECORDED", "ledgerPath": str(path), **values}
681
+
682
+
564
683
  def _contract_is_v2(change_dir: Path) -> bool:
565
684
  try:
566
685
  contract = harness_paths.load_change_contract(change_dir)
@@ -581,6 +700,7 @@ def ownership_hash(contract: dict[str, Any]) -> str:
581
700
  _METRICS_SCHEMAS: dict[str, dict[str, tuple[str, ...]]] = {
582
701
  "unitTest": {"required": ("total", "passed", "failed"), "optional": ("errors", "skipped")},
583
702
  "unitTestFull": {"required": ("total", "passed", "failed"), "optional": ("errors", "skipped")},
703
+ "apiTest": {"required": ("total", "passed", "failed"), "optional": ("blocked",)},
584
704
  "apiContract": {"required": ("scenariosTotal", "passed", "failed"), "optional": ("blocked",)},
585
705
  "browserE2E": {"required": ("total", "passed", "failed"), "optional": ("skipped", "retries")},
586
706
  }
@@ -599,9 +719,37 @@ def validate_metrics(verification: str, metrics: Any) -> list[str]:
599
719
  reason = metrics.get("reason")
600
720
  if not isinstance(reason, str) or not reason.strip():
601
721
  problems.append("dbCompatibility.reason required for NOT_APPLICABLE")
722
+ unknown = sorted(set(metrics) - {"applicability", "reason"})
723
+ problems.extend(
724
+ f"dbCompatibility.{field} is not valid for NOT_APPLICABLE"
725
+ for field in unknown
726
+ )
602
727
  else:
603
- if not isinstance(metrics.get("status"), str) or not metrics["status"]:
604
- problems.append("dbCompatibility.status required for APPLICABLE")
728
+ allowed = {
729
+ "applicability", "status", "total", "passed", "failed", "evidenceHash"
730
+ }
731
+ status = metrics.get("status")
732
+ if status not in {"OK", "FAIL"}:
733
+ problems.append("dbCompatibility.status must be OK|FAIL for APPLICABLE")
734
+ for field in ("total", "passed", "failed"):
735
+ value = metrics.get(field)
736
+ if not isinstance(value, int) or isinstance(value, bool) or value < 0:
737
+ problems.append(f"dbCompatibility.{field} must be a non-negative int")
738
+ total = metrics.get("total")
739
+ passed = metrics.get("passed")
740
+ failed = metrics.get("failed")
741
+ if all(isinstance(value, int) and not isinstance(value, bool)
742
+ for value in (total, passed, failed)) and passed + failed != total:
743
+ problems.append("dbCompatibility counts must satisfy passed + failed == total")
744
+ evidence_hash = metrics.get("evidenceHash")
745
+ if not isinstance(evidence_hash, str) or not re.fullmatch(
746
+ r"sha256:[0-9a-f]{64}", evidence_hash
747
+ ):
748
+ problems.append("dbCompatibility.evidenceHash must be sha256:<64 lowercase hex>")
749
+ problems.extend(
750
+ f"dbCompatibility.{field} is not a supported field"
751
+ for field in sorted(set(metrics) - allowed)
752
+ )
605
753
  return problems
606
754
  schema = _METRICS_SCHEMAS.get(verification)
607
755
  if schema is None:
@@ -701,14 +849,19 @@ def compute_ownership_diff(
701
849
  if head:
702
850
  diff_args.append(head)
703
851
  raw = _git_text(repo_root, *diff_args) or ""
852
+ changed_paths = {line.strip() for line in raw.splitlines() if line.strip()}
853
+ if head is None:
854
+ untracked = _git_text(
855
+ repo_root, "ls-files", "--others", "--exclude-standard"
856
+ ) or ""
857
+ changed_paths.update(
858
+ line.strip() for line in untracked.splitlines() if line.strip()
859
+ )
704
860
  owned: list[str] = []
705
861
  static_evidence: list[str] = []
706
862
  foreign: list[str] = []
707
863
  excluded_runtime = 0
708
- for line in raw.splitlines():
709
- rel = line.strip()
710
- if not rel:
711
- continue
864
+ for rel in sorted(changed_paths):
712
865
  verdict = _classify_ownership_path(rel, change_dir.name, ownership)
713
866
  if verdict == "foreign":
714
867
  foreign.append(rel)
@@ -1040,8 +1193,10 @@ def decide_can_reuse(
1040
1193
 
1041
1194
  def cmd_hash(args: argparse.Namespace) -> int:
1042
1195
  as_json = bool(args.json)
1043
- files = parse_files_arg(args.files)
1044
1196
  try:
1197
+ files, project_root = input_files_from_args(args)
1198
+ if not files:
1199
+ return emit_error("hash requires --files or --files-from", as_json=as_json)
1045
1200
  inputs_hash, inputs_files = compute_inputs_hash(files)
1046
1201
  except (OSError, FileNotFoundError) as exc:
1047
1202
  return emit_error(str(exc), as_json=as_json)
@@ -1052,6 +1207,7 @@ def cmd_hash(args: argparse.Namespace) -> int:
1052
1207
  "inputsHash": inputs_hash,
1053
1208
  "inputsFiles": inputs_files,
1054
1209
  "fileCount": len(inputs_files),
1210
+ "resolvedProjectRoot": str(project_root) if project_root else None,
1055
1211
  }
1056
1212
  emit_json(payload, as_json=as_json)
1057
1213
  return 0
@@ -1066,15 +1222,16 @@ def cmd_can_reuse(args: argparse.Namespace) -> int:
1066
1222
  as_json=as_json,
1067
1223
  )
1068
1224
  change_dir = resolve_path(args.change_dir)
1069
- files = parse_files_arg(args.files)
1225
+ try:
1226
+ files, project_root = input_files_from_args(args)
1227
+ except (OSError, ValueError) as exc:
1228
+ return emit_error(f"can-reuse failed: {exc}", as_json=as_json)
1070
1229
  profile_input = getattr(args, "profile_input", None)
1071
1230
  project_raw = getattr(args, "project", None)
1072
1231
  if profile_input:
1073
1232
  if not project_raw:
1074
1233
  return emit_error("--profile-input requires --project", as_json=as_json)
1075
- resolved_files, err = expand_profile_input_files(
1076
- Path(str(project_raw)).expanduser().resolve(), profile_input
1077
- )
1234
+ resolved_files, err = expand_profile_input_files(project_root, profile_input)
1078
1235
  if err:
1079
1236
  # profile 未正确配置:不允许缓存复用,返回 insufficient-evidence(exit 0)。
1080
1237
  payload = {
@@ -1086,7 +1243,7 @@ def cmd_can_reuse(args: argparse.Namespace) -> int:
1086
1243
  }
1087
1244
  emit_json(payload, as_json=as_json)
1088
1245
  return 0
1089
- files = resolved_files
1246
+ files = resolve_input_files(resolved_files, project_root)
1090
1247
  if not files:
1091
1248
  return emit_error(
1092
1249
  "can-reuse requires --files or a non-empty --profile-input file set",
@@ -1114,18 +1271,33 @@ def cmd_record(args: argparse.Namespace) -> int:
1114
1271
  as_json = bool(args.json)
1115
1272
  change_dir = resolve_path(args.change_dir)
1116
1273
  verification = args.verification
1117
- files = parse_files_arg(args.files)
1274
+ if verification not in VERIFICATIONS:
1275
+ return emit_error(
1276
+ f"unsupported verification: {verification}; expected one of {sorted(VERIFICATIONS)}",
1277
+ as_json=as_json,
1278
+ )
1279
+ contract_v2 = _contract_is_v2(change_dir)
1280
+ if not contract_v2 and any(
1281
+ _nonempty_str(getattr(args, field, None))
1282
+ for field in ("base_commit", "diff_hash")
1283
+ ):
1284
+ return emit_error(
1285
+ "IDENTITY_UNSUPPORTED: explicit ledger identity requires a v2 change contract",
1286
+ as_json=as_json,
1287
+ )
1288
+ try:
1289
+ files, project_root = input_files_from_args(args)
1290
+ except (OSError, ValueError) as exc:
1291
+ return emit_error(f"record failed: {exc}", as_json=as_json)
1118
1292
  profile_input = getattr(args, "profile_input", None)
1119
1293
  project_raw = getattr(args, "project", None)
1120
1294
  if profile_input:
1121
1295
  if not project_raw:
1122
1296
  return emit_error("--profile-input requires --project", as_json=as_json)
1123
- resolved_files, err = expand_profile_input_files(
1124
- Path(str(project_raw)).expanduser().resolve(), profile_input
1125
- )
1297
+ resolved_files, err = expand_profile_input_files(project_root, profile_input)
1126
1298
  if err:
1127
1299
  return emit_error(f"record failed: {err}", as_json=as_json)
1128
- files = resolved_files
1300
+ files = resolve_input_files(resolved_files, project_root)
1129
1301
  if not files:
1130
1302
  return emit_error(
1131
1303
  "record requires --files or a non-empty --profile-input file set",
@@ -1164,6 +1336,19 @@ def cmd_record(args: argparse.Namespace) -> int:
1164
1336
  }
1165
1337
  )
1166
1338
  metrics_raw = getattr(args, "metrics_json", None)
1339
+ metrics_file = getattr(args, "metrics_file", None)
1340
+ if metrics_file and metrics_raw is not None and str(metrics_raw).strip():
1341
+ return emit_error(
1342
+ "use only one of --metrics-json and --metrics-file",
1343
+ as_json=as_json,
1344
+ )
1345
+ if metrics_file:
1346
+ try:
1347
+ metrics_raw = Path(str(metrics_file)).expanduser().resolve().read_text(
1348
+ encoding="utf-8-sig"
1349
+ )
1350
+ except OSError as exc:
1351
+ return emit_error(f"invalid --metrics-file: {exc}", as_json=as_json)
1167
1352
  if metrics_raw is not None and str(metrics_raw).strip() != "":
1168
1353
  try:
1169
1354
  metrics_obj = json.loads(str(metrics_raw))
@@ -1171,7 +1356,7 @@ def cmd_record(args: argparse.Namespace) -> int:
1171
1356
  return emit_error("invalid --metrics-json", as_json=as_json)
1172
1357
  if not isinstance(metrics_obj, dict):
1173
1358
  return emit_error("invalid --metrics-json", as_json=as_json)
1174
- if _contract_is_v2(change_dir):
1359
+ if contract_v2:
1175
1360
  # Typed metrics schemas are ledger v3 (contract-gated); legacy
1176
1361
  # changes keep the loose run/failures shape (zero regression).
1177
1362
  problems = validate_metrics(verification, metrics_obj)
@@ -1231,9 +1416,10 @@ def cmd_record(args: argparse.Namespace) -> int:
1231
1416
  ledger["stateDir"] = str(change_dir)
1232
1417
 
1233
1418
  out_path = preferred_write_path(change_dir)
1234
- if _contract_is_v2(change_dir):
1419
+ if contract_v2:
1235
1420
  # Ledger v3: forced top-level identity (RET-15/16, COM-002).
1236
- repo_root_raw = _git_text(change_dir, "rev-parse", "--show-toplevel")
1421
+ repo_probe = project_root or change_dir
1422
+ repo_root_raw = _git_text(repo_probe, "rev-parse", "--show-toplevel")
1237
1423
  if not repo_root_raw:
1238
1424
  return emit_error(
1239
1425
  "record failed: change dir is not inside a git repository",
@@ -1251,8 +1437,6 @@ def cmd_record(args: argparse.Namespace) -> int:
1251
1437
  base_commit = _git_text(repo_root, "rev-parse", "--verify", "HEAD")
1252
1438
  current_head = _git_text(repo_root, "rev-parse", "--verify", "HEAD")
1253
1439
  diff_hash = getattr(args, "diff_hash", None)
1254
- if not _nonempty_str(diff_hash):
1255
- diff_hash = ledger.get("diffHash")
1256
1440
  if not _nonempty_str(diff_hash) and _nonempty_str(base_commit):
1257
1441
  try:
1258
1442
  diff_hash = compute_ownership_diff(
@@ -1289,6 +1473,7 @@ def cmd_record(args: argparse.Namespace) -> int:
1289
1473
  "inputsFiles": inputs_files,
1290
1474
  "ledger_path": str(out_path),
1291
1475
  "diffHash": ledger.get("diffHash"),
1476
+ "resolvedProjectRoot": str(project_root) if project_root else None,
1292
1477
  }
1293
1478
  emit_json(payload, as_json=as_json)
1294
1479
  return 0
@@ -1333,11 +1518,10 @@ def build_parser() -> argparse.ArgumentParser:
1333
1518
  )
1334
1519
 
1335
1520
  p_hash = sub.add_parser("hash", parents=[shared_json], help="compute inputsHash for a file set")
1336
- p_hash.add_argument(
1337
- "--files",
1338
- required=True,
1339
- help="comma-separated source file paths",
1340
- )
1521
+ hash_files = p_hash.add_mutually_exclusive_group()
1522
+ hash_files.add_argument("--files", default=None, help="comma-separated source file paths")
1523
+ hash_files.add_argument("--files-from", default=None, help="UTF-8 newline-delimited source paths")
1524
+ p_hash.add_argument("--project", default=None, help="root for relative input paths")
1341
1525
  p_hash.set_defaults(func=cmd_hash)
1342
1526
 
1343
1527
  p_reuse = sub.add_parser("can-reuse", parents=[shared_json], help="decide whether a verification can be reused")
@@ -1352,6 +1536,11 @@ def build_parser() -> argparse.ArgumentParser:
1352
1536
  default=None,
1353
1537
  help="comma-separated source file paths for current inputsHash",
1354
1538
  )
1539
+ p_reuse.add_argument(
1540
+ "--files-from",
1541
+ default=None,
1542
+ help="UTF-8 newline-delimited source paths",
1543
+ )
1355
1544
  p_reuse.add_argument(
1356
1545
  "--project",
1357
1546
  default=None,
@@ -1397,7 +1586,9 @@ def build_parser() -> argparse.ArgumentParser:
1397
1586
  p_record.add_argument("--command", required=True)
1398
1587
  p_record.add_argument("--exit-code", type=int, required=True)
1399
1588
  p_record.add_argument("--duration-ms", type=int, required=True)
1400
- p_record.add_argument("--files", default=None)
1589
+ p_record_files = p_record.add_mutually_exclusive_group()
1590
+ p_record_files.add_argument("--files", default=None)
1591
+ p_record_files.add_argument("--files-from", default=None)
1401
1592
  p_record.add_argument("--evidence", required=True)
1402
1593
  p_record.add_argument(
1403
1594
  "--project",
@@ -1440,6 +1631,11 @@ def build_parser() -> argparse.ArgumentParser:
1440
1631
  default=None,
1441
1632
  help='structured counts, e.g. \'{"run":155,"failures":0}\' or \'{"total":3,"passed":3}\'',
1442
1633
  )
1634
+ p_record.add_argument(
1635
+ "--metrics-file",
1636
+ default=None,
1637
+ help="UTF-8 JSON file containing typed verification metrics",
1638
+ )
1443
1639
  p_record.add_argument(
1444
1640
  "--base-commit",
1445
1641
  default=None,