@hunter-harness/workflow-harness 0.2.4 → 0.2.6

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 (242) 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 +5 -2
  3. package/harness/bundles/general/claude-code/harness-archive/reference.md +1 -1
  4. package/harness/bundles/general/claude-code/harness-archive/templates/render-summary.mjs +78 -155
  5. package/harness/bundles/general/claude-code/harness-codebase-map/SKILL.md +4 -2
  6. package/harness/bundles/general/claude-code/harness-knowledge-ingest/SKILL.md +5 -3
  7. package/harness/bundles/general/claude-code/harness-knowledge-ingest/scripts/harness_knowledge.py +83 -6
  8. package/harness/bundles/general/claude-code/harness-knowledge-query/SKILL.md +4 -2
  9. package/harness/bundles/general/claude-code/harness-plan/SKILL.md +6 -3
  10. package/harness/bundles/general/claude-code/harness-review/SKILL.md +6 -3
  11. package/harness/bundles/general/claude-code/harness-run/SKILL.md +19 -4
  12. package/harness/bundles/general/claude-code/harness-run/checklist.md +7 -3
  13. package/harness/bundles/general/claude-code/harness-run/reference.md +4 -10
  14. package/harness/bundles/general/claude-code/harness-submit/SKILL.md +9 -4
  15. package/harness/bundles/general/claude-code/harness-submit/checklist.md +10 -1
  16. package/harness/bundles/general/claude-code/harness-sync/SKILL.md +5 -3
  17. package/harness/bundles/general/claude-code/harness-test/SKILL.md +18 -3
  18. package/harness/bundles/general/claude-code/harness-test/checklist.md +4 -0
  19. package/harness/bundles/general/claude-code/harness-test/reference.md +5 -5
  20. package/harness/bundles/general/claude-code/protocols/archive-report-protocol.md +2 -4
  21. package/harness/bundles/general/claude-code/protocols/ledger-protocol.md +8 -16
  22. package/harness/bundles/general/claude-code/protocols/report-pipeline-protocol.md +11 -4
  23. package/harness/bundles/general/claude-code/scripts/harness_archive.py +190 -32
  24. package/harness/bundles/general/claude-code/scripts/harness_events.py +134 -40
  25. package/harness/bundles/general/claude-code/scripts/harness_ledger.py +111 -5
  26. package/harness/bundles/general/claude-code/scripts/harness_preflight.py +46 -3
  27. package/harness/bundles/general/claude-code/scripts/harness_profile.py +31 -3
  28. package/harness/bundles/general/claude-code/scripts/harness_state.py +148 -0
  29. package/harness/bundles/general/claude-code/scripts/harness_test_guard.py +502 -0
  30. package/harness/bundles/general/codebuddy/.harness-build.json +1 -1
  31. package/harness/bundles/general/codebuddy/harness-archive/SKILL.md +5 -2
  32. package/harness/bundles/general/codebuddy/harness-archive/reference.md +1 -1
  33. package/harness/bundles/general/codebuddy/harness-archive/templates/render-summary.mjs +78 -155
  34. package/harness/bundles/general/codebuddy/harness-codebase-map/SKILL.md +4 -2
  35. package/harness/bundles/general/codebuddy/harness-knowledge-ingest/SKILL.md +5 -3
  36. package/harness/bundles/general/codebuddy/harness-knowledge-ingest/scripts/harness_knowledge.py +83 -6
  37. package/harness/bundles/general/codebuddy/harness-knowledge-query/SKILL.md +4 -2
  38. package/harness/bundles/general/codebuddy/harness-plan/SKILL.md +6 -3
  39. package/harness/bundles/general/codebuddy/harness-review/SKILL.md +6 -3
  40. package/harness/bundles/general/codebuddy/harness-run/SKILL.md +19 -4
  41. package/harness/bundles/general/codebuddy/harness-run/checklist.md +7 -3
  42. package/harness/bundles/general/codebuddy/harness-run/reference.md +4 -10
  43. package/harness/bundles/general/codebuddy/harness-submit/SKILL.md +9 -4
  44. package/harness/bundles/general/codebuddy/harness-submit/checklist.md +10 -1
  45. package/harness/bundles/general/codebuddy/harness-sync/SKILL.md +5 -3
  46. package/harness/bundles/general/codebuddy/harness-test/SKILL.md +18 -3
  47. package/harness/bundles/general/codebuddy/harness-test/checklist.md +4 -0
  48. package/harness/bundles/general/codebuddy/harness-test/reference.md +5 -5
  49. package/harness/bundles/general/codebuddy/protocols/archive-report-protocol.md +2 -4
  50. package/harness/bundles/general/codebuddy/protocols/ledger-protocol.md +8 -16
  51. package/harness/bundles/general/codebuddy/protocols/report-pipeline-protocol.md +11 -4
  52. package/harness/bundles/general/codebuddy/scripts/harness_archive.py +190 -32
  53. package/harness/bundles/general/codebuddy/scripts/harness_events.py +134 -40
  54. package/harness/bundles/general/codebuddy/scripts/harness_ledger.py +111 -5
  55. package/harness/bundles/general/codebuddy/scripts/harness_preflight.py +46 -3
  56. package/harness/bundles/general/codebuddy/scripts/harness_profile.py +31 -3
  57. package/harness/bundles/general/codebuddy/scripts/harness_state.py +148 -0
  58. package/harness/bundles/general/codebuddy/scripts/harness_test_guard.py +502 -0
  59. package/harness/bundles/general/codex/.harness-build.json +1 -1
  60. package/harness/bundles/general/codex/harness-archive/SKILL.md +5 -2
  61. package/harness/bundles/general/codex/harness-archive/reference.md +1 -1
  62. package/harness/bundles/general/codex/harness-archive/templates/render-summary.mjs +78 -155
  63. package/harness/bundles/general/codex/harness-codebase-map/SKILL.md +4 -2
  64. package/harness/bundles/general/codex/harness-knowledge-ingest/SKILL.md +5 -3
  65. package/harness/bundles/general/codex/harness-knowledge-ingest/scripts/harness_knowledge.py +83 -6
  66. package/harness/bundles/general/codex/harness-knowledge-query/SKILL.md +4 -2
  67. package/harness/bundles/general/codex/harness-plan/SKILL.md +6 -3
  68. package/harness/bundles/general/codex/harness-review/SKILL.md +5 -2
  69. package/harness/bundles/general/codex/harness-run/SKILL.md +19 -4
  70. package/harness/bundles/general/codex/harness-run/checklist.md +7 -3
  71. package/harness/bundles/general/codex/harness-run/reference.md +4 -10
  72. package/harness/bundles/general/codex/harness-submit/SKILL.md +9 -4
  73. package/harness/bundles/general/codex/harness-submit/checklist.md +10 -1
  74. package/harness/bundles/general/codex/harness-sync/SKILL.md +5 -3
  75. package/harness/bundles/general/codex/harness-test/SKILL.md +18 -3
  76. package/harness/bundles/general/codex/harness-test/checklist.md +4 -0
  77. package/harness/bundles/general/codex/harness-test/reference.md +5 -5
  78. package/harness/bundles/general/codex/protocols/archive-report-protocol.md +2 -4
  79. package/harness/bundles/general/codex/protocols/ledger-protocol.md +8 -16
  80. package/harness/bundles/general/codex/protocols/report-pipeline-protocol.md +11 -4
  81. package/harness/bundles/general/codex/scripts/harness_archive.py +190 -32
  82. package/harness/bundles/general/codex/scripts/harness_events.py +134 -40
  83. package/harness/bundles/general/codex/scripts/harness_ledger.py +111 -5
  84. package/harness/bundles/general/codex/scripts/harness_preflight.py +46 -3
  85. package/harness/bundles/general/codex/scripts/harness_profile.py +31 -3
  86. package/harness/bundles/general/codex/scripts/harness_state.py +148 -0
  87. package/harness/bundles/general/codex/scripts/harness_test_guard.py +502 -0
  88. package/harness/bundles/general/cursor/.harness-build.json +1 -1
  89. package/harness/bundles/general/cursor/harness-archive/SKILL.md +5 -2
  90. package/harness/bundles/general/cursor/harness-archive/reference.md +1 -1
  91. package/harness/bundles/general/cursor/harness-archive/templates/render-summary.mjs +78 -155
  92. package/harness/bundles/general/cursor/harness-codebase-map/SKILL.md +4 -2
  93. package/harness/bundles/general/cursor/harness-knowledge-ingest/SKILL.md +5 -3
  94. package/harness/bundles/general/cursor/harness-knowledge-ingest/scripts/harness_knowledge.py +83 -6
  95. package/harness/bundles/general/cursor/harness-knowledge-query/SKILL.md +4 -2
  96. package/harness/bundles/general/cursor/harness-plan/SKILL.md +6 -3
  97. package/harness/bundles/general/cursor/harness-review/SKILL.md +5 -2
  98. package/harness/bundles/general/cursor/harness-run/SKILL.md +19 -4
  99. package/harness/bundles/general/cursor/harness-run/checklist.md +7 -3
  100. package/harness/bundles/general/cursor/harness-run/reference.md +4 -10
  101. package/harness/bundles/general/cursor/harness-submit/SKILL.md +9 -4
  102. package/harness/bundles/general/cursor/harness-submit/checklist.md +10 -1
  103. package/harness/bundles/general/cursor/harness-sync/SKILL.md +5 -3
  104. package/harness/bundles/general/cursor/harness-test/SKILL.md +18 -3
  105. package/harness/bundles/general/cursor/harness-test/checklist.md +4 -0
  106. package/harness/bundles/general/cursor/harness-test/reference.md +5 -5
  107. package/harness/bundles/general/cursor/protocols/archive-report-protocol.md +2 -4
  108. package/harness/bundles/general/cursor/protocols/ledger-protocol.md +8 -16
  109. package/harness/bundles/general/cursor/protocols/report-pipeline-protocol.md +11 -4
  110. package/harness/bundles/general/cursor/scripts/harness_archive.py +190 -32
  111. package/harness/bundles/general/cursor/scripts/harness_events.py +134 -40
  112. package/harness/bundles/general/cursor/scripts/harness_ledger.py +111 -5
  113. package/harness/bundles/general/cursor/scripts/harness_preflight.py +46 -3
  114. package/harness/bundles/general/cursor/scripts/harness_profile.py +31 -3
  115. package/harness/bundles/general/cursor/scripts/harness_state.py +148 -0
  116. package/harness/bundles/general/cursor/scripts/harness_test_guard.py +502 -0
  117. package/harness/bundles/java/claude-code/.harness-build.json +1 -1
  118. package/harness/bundles/java/claude-code/harness-apidoc/SKILL.md +5 -2
  119. package/harness/bundles/java/claude-code/harness-archive/SKILL.md +5 -2
  120. package/harness/bundles/java/claude-code/harness-archive/reference.md +1 -1
  121. package/harness/bundles/java/claude-code/harness-archive/templates/render-summary.mjs +78 -155
  122. package/harness/bundles/java/claude-code/harness-codebase-map/SKILL.md +4 -2
  123. package/harness/bundles/java/claude-code/harness-knowledge-ingest/SKILL.md +5 -3
  124. package/harness/bundles/java/claude-code/harness-knowledge-ingest/scripts/harness_knowledge.py +83 -6
  125. package/harness/bundles/java/claude-code/harness-knowledge-query/SKILL.md +4 -2
  126. package/harness/bundles/java/claude-code/harness-package/SKILL.md +5 -2
  127. package/harness/bundles/java/claude-code/harness-plan/SKILL.md +6 -3
  128. package/harness/bundles/java/claude-code/harness-review/SKILL.md +6 -3
  129. package/harness/bundles/java/claude-code/harness-run/SKILL.md +22 -5
  130. package/harness/bundles/java/claude-code/harness-run/reference.md +1 -1
  131. package/harness/bundles/java/claude-code/harness-submit/SKILL.md +9 -4
  132. package/harness/bundles/java/claude-code/harness-submit/checklist.md +10 -1
  133. package/harness/bundles/java/claude-code/harness-sync/SKILL.md +5 -3
  134. package/harness/bundles/java/claude-code/harness-test/SKILL.md +20 -3
  135. package/harness/bundles/java/claude-code/harness-test/reference.md +1 -1
  136. package/harness/bundles/java/claude-code/protocols/archive-report-protocol.md +2 -4
  137. package/harness/bundles/java/claude-code/protocols/ledger-protocol.md +8 -16
  138. package/harness/bundles/java/claude-code/protocols/report-pipeline-protocol.md +11 -4
  139. package/harness/bundles/java/claude-code/scripts/harness_archive.py +190 -32
  140. package/harness/bundles/java/claude-code/scripts/harness_events.py +134 -40
  141. package/harness/bundles/java/claude-code/scripts/harness_ledger.py +111 -5
  142. package/harness/bundles/java/claude-code/scripts/harness_preflight.py +46 -3
  143. package/harness/bundles/java/claude-code/scripts/harness_profile.py +31 -3
  144. package/harness/bundles/java/claude-code/scripts/harness_state.py +148 -0
  145. package/harness/bundles/java/claude-code/scripts/harness_test_guard.py +502 -0
  146. package/harness/bundles/java/codebuddy/.harness-build.json +1 -1
  147. package/harness/bundles/java/codebuddy/harness-apidoc/SKILL.md +5 -2
  148. package/harness/bundles/java/codebuddy/harness-archive/SKILL.md +5 -2
  149. package/harness/bundles/java/codebuddy/harness-archive/reference.md +1 -1
  150. package/harness/bundles/java/codebuddy/harness-archive/templates/render-summary.mjs +78 -155
  151. package/harness/bundles/java/codebuddy/harness-codebase-map/SKILL.md +4 -2
  152. package/harness/bundles/java/codebuddy/harness-knowledge-ingest/SKILL.md +5 -3
  153. package/harness/bundles/java/codebuddy/harness-knowledge-ingest/scripts/harness_knowledge.py +83 -6
  154. package/harness/bundles/java/codebuddy/harness-knowledge-query/SKILL.md +4 -2
  155. package/harness/bundles/java/codebuddy/harness-package/SKILL.md +5 -2
  156. package/harness/bundles/java/codebuddy/harness-plan/SKILL.md +6 -3
  157. package/harness/bundles/java/codebuddy/harness-review/SKILL.md +6 -3
  158. package/harness/bundles/java/codebuddy/harness-run/SKILL.md +22 -5
  159. package/harness/bundles/java/codebuddy/harness-run/reference.md +1 -1
  160. package/harness/bundles/java/codebuddy/harness-submit/SKILL.md +9 -4
  161. package/harness/bundles/java/codebuddy/harness-submit/checklist.md +10 -1
  162. package/harness/bundles/java/codebuddy/harness-sync/SKILL.md +5 -3
  163. package/harness/bundles/java/codebuddy/harness-test/SKILL.md +20 -3
  164. package/harness/bundles/java/codebuddy/harness-test/reference.md +1 -1
  165. package/harness/bundles/java/codebuddy/protocols/archive-report-protocol.md +2 -4
  166. package/harness/bundles/java/codebuddy/protocols/ledger-protocol.md +8 -16
  167. package/harness/bundles/java/codebuddy/protocols/report-pipeline-protocol.md +11 -4
  168. package/harness/bundles/java/codebuddy/scripts/harness_archive.py +190 -32
  169. package/harness/bundles/java/codebuddy/scripts/harness_events.py +134 -40
  170. package/harness/bundles/java/codebuddy/scripts/harness_ledger.py +111 -5
  171. package/harness/bundles/java/codebuddy/scripts/harness_preflight.py +46 -3
  172. package/harness/bundles/java/codebuddy/scripts/harness_profile.py +31 -3
  173. package/harness/bundles/java/codebuddy/scripts/harness_state.py +148 -0
  174. package/harness/bundles/java/codebuddy/scripts/harness_test_guard.py +502 -0
  175. package/harness/bundles/java/codex/.harness-build.json +1 -1
  176. package/harness/bundles/java/codex/harness-apidoc/SKILL.md +5 -2
  177. package/harness/bundles/java/codex/harness-archive/SKILL.md +5 -2
  178. package/harness/bundles/java/codex/harness-archive/reference.md +1 -1
  179. package/harness/bundles/java/codex/harness-archive/templates/render-summary.mjs +78 -155
  180. package/harness/bundles/java/codex/harness-codebase-map/SKILL.md +4 -2
  181. package/harness/bundles/java/codex/harness-knowledge-ingest/SKILL.md +5 -3
  182. package/harness/bundles/java/codex/harness-knowledge-ingest/scripts/harness_knowledge.py +83 -6
  183. package/harness/bundles/java/codex/harness-knowledge-query/SKILL.md +4 -2
  184. package/harness/bundles/java/codex/harness-package/SKILL.md +5 -2
  185. package/harness/bundles/java/codex/harness-plan/SKILL.md +6 -3
  186. package/harness/bundles/java/codex/harness-review/SKILL.md +5 -2
  187. package/harness/bundles/java/codex/harness-run/SKILL.md +22 -5
  188. package/harness/bundles/java/codex/harness-run/reference.md +1 -1
  189. package/harness/bundles/java/codex/harness-submit/SKILL.md +9 -4
  190. package/harness/bundles/java/codex/harness-submit/checklist.md +10 -1
  191. package/harness/bundles/java/codex/harness-sync/SKILL.md +5 -3
  192. package/harness/bundles/java/codex/harness-test/SKILL.md +20 -3
  193. package/harness/bundles/java/codex/harness-test/reference.md +1 -1
  194. package/harness/bundles/java/codex/protocols/archive-report-protocol.md +2 -4
  195. package/harness/bundles/java/codex/protocols/ledger-protocol.md +8 -16
  196. package/harness/bundles/java/codex/protocols/report-pipeline-protocol.md +11 -4
  197. package/harness/bundles/java/codex/scripts/harness_archive.py +190 -32
  198. package/harness/bundles/java/codex/scripts/harness_events.py +134 -40
  199. package/harness/bundles/java/codex/scripts/harness_ledger.py +111 -5
  200. package/harness/bundles/java/codex/scripts/harness_preflight.py +46 -3
  201. package/harness/bundles/java/codex/scripts/harness_profile.py +31 -3
  202. package/harness/bundles/java/codex/scripts/harness_state.py +148 -0
  203. package/harness/bundles/java/codex/scripts/harness_test_guard.py +502 -0
  204. package/harness/bundles/java/cursor/.harness-build.json +1 -1
  205. package/harness/bundles/java/cursor/harness-apidoc/SKILL.md +5 -2
  206. package/harness/bundles/java/cursor/harness-archive/SKILL.md +5 -2
  207. package/harness/bundles/java/cursor/harness-archive/reference.md +1 -1
  208. package/harness/bundles/java/cursor/harness-archive/templates/render-summary.mjs +78 -155
  209. package/harness/bundles/java/cursor/harness-codebase-map/SKILL.md +4 -2
  210. package/harness/bundles/java/cursor/harness-knowledge-ingest/SKILL.md +5 -3
  211. package/harness/bundles/java/cursor/harness-knowledge-ingest/scripts/harness_knowledge.py +83 -6
  212. package/harness/bundles/java/cursor/harness-knowledge-query/SKILL.md +4 -2
  213. package/harness/bundles/java/cursor/harness-package/SKILL.md +5 -2
  214. package/harness/bundles/java/cursor/harness-plan/SKILL.md +6 -3
  215. package/harness/bundles/java/cursor/harness-review/SKILL.md +5 -2
  216. package/harness/bundles/java/cursor/harness-run/SKILL.md +22 -5
  217. package/harness/bundles/java/cursor/harness-run/reference.md +1 -1
  218. package/harness/bundles/java/cursor/harness-submit/SKILL.md +9 -4
  219. package/harness/bundles/java/cursor/harness-submit/checklist.md +10 -1
  220. package/harness/bundles/java/cursor/harness-sync/SKILL.md +5 -3
  221. package/harness/bundles/java/cursor/harness-test/SKILL.md +20 -3
  222. package/harness/bundles/java/cursor/harness-test/reference.md +1 -1
  223. package/harness/bundles/java/cursor/protocols/archive-report-protocol.md +2 -4
  224. package/harness/bundles/java/cursor/protocols/ledger-protocol.md +8 -16
  225. package/harness/bundles/java/cursor/protocols/report-pipeline-protocol.md +11 -4
  226. package/harness/bundles/java/cursor/scripts/harness_archive.py +190 -32
  227. package/harness/bundles/java/cursor/scripts/harness_events.py +134 -40
  228. package/harness/bundles/java/cursor/scripts/harness_ledger.py +111 -5
  229. package/harness/bundles/java/cursor/scripts/harness_preflight.py +46 -3
  230. package/harness/bundles/java/cursor/scripts/harness_profile.py +31 -3
  231. package/harness/bundles/java/cursor/scripts/harness_state.py +148 -0
  232. package/harness/bundles/java/cursor/scripts/harness_test_guard.py +502 -0
  233. package/harness/manifests/general/claude-code.json +33 -29
  234. package/harness/manifests/general/codebuddy.json +33 -29
  235. package/harness/manifests/general/codex.json +33 -29
  236. package/harness/manifests/general/cursor.json +33 -29
  237. package/harness/manifests/java/claude-code.json +33 -29
  238. package/harness/manifests/java/codebuddy.json +33 -29
  239. package/harness/manifests/java/codex.json +33 -29
  240. package/harness/manifests/java/cursor.json +33 -29
  241. package/hunter-workflow-family.json +2 -2
  242. package/package.json +1 -1
@@ -40,7 +40,9 @@ BROAD_SCOPES = frozenset({"module", "module-am", "full"})
40
40
 
41
41
  # --- Ledger v2 (cluster 2) ---
42
42
  LEDGER_VERSION = "harness-ledger-2"
43
- DIFF_HASH_VERSION = "content-changeset-1"
43
+ DIFF_HASH_VERSION = "content-changeset-2"
44
+ TEST_TRACKING_REL = Path("evidence") / "test-tracking.json"
45
+ TEST_TRACKING_REASONS = frozenset({"tdd-created", "stale-test-repair", "test-updated"})
44
46
  # Coverage lattice: a recorded verification's coverage rank must meet the
45
47
  # verification's required rank. Prevents incremental evidence from satisfying
46
48
  # a module/full gate (UT-015 / API-005).
@@ -168,7 +170,88 @@ def _changed_paths(repo_root: Path, base: str | None) -> tuple[str, list[str]]:
168
170
  return base, sorted(paths)
169
171
 
170
172
 
171
- def compute_diff_hash(repo_root: Path, base: str | None = None) -> tuple[str, dict[str, Any]]:
173
+ def _inside(path: Path, root: Path) -> bool:
174
+ try:
175
+ path.relative_to(root)
176
+ return True
177
+ except ValueError:
178
+ return False
179
+
180
+
181
+ def _tracked_test_contents(
182
+ repo_root: Path,
183
+ change_dir: Path | str | None,
184
+ ) -> tuple[dict[str, bytes], str | None]:
185
+ """Load exact test paths recorded by harness_test_guard.
186
+
187
+ The ledger validates the same security-critical manifest fields again so a
188
+ hand-edited manifest cannot silently widen the fingerprint or reuse stale
189
+ evidence. Missing manifests remain backward-compatible and contribute no
190
+ additional paths.
191
+ """
192
+ if change_dir is None:
193
+ return {}, None
194
+ candidate = Path(change_dir)
195
+ if not candidate.is_absolute():
196
+ candidate = repo_root / candidate
197
+ change_root = candidate.resolve()
198
+ if not _inside(change_root, repo_root):
199
+ 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):
202
+ raise ValueError("TEST_TRACKING_MANIFEST_OUTSIDE_CHANGE")
203
+ if not manifest_path.is_file():
204
+ return {}, None
205
+ try:
206
+ manifest = json.loads(manifest_path.read_text(encoding="utf-8-sig"))
207
+ except (OSError, json.JSONDecodeError) as exc:
208
+ raise ValueError(f"TEST_TRACKING_MANIFEST_INVALID: {exc}") from exc
209
+ if (
210
+ not isinstance(manifest, dict)
211
+ or manifest.get("schemaVersion") != 1
212
+ or manifest.get("mode") != "force-track-touched"
213
+ or manifest.get("projectRoot") != str(repo_root)
214
+ ):
215
+ raise ValueError("TEST_TRACKING_MANIFEST_INVALID")
216
+ entries = manifest.get("files")
217
+ if not isinstance(entries, list) or not entries:
218
+ raise ValueError("TEST_TRACKING_MANIFEST_INVALID: EMPTY_FILES")
219
+
220
+ contents: dict[str, bytes] = {}
221
+ for item in entries:
222
+ if not isinstance(item, dict):
223
+ raise ValueError("TEST_TRACKING_MANIFEST_INVALID")
224
+ rel = item.get("path")
225
+ expected_hash = item.get("sha256")
226
+ if (
227
+ not isinstance(rel, str)
228
+ or not rel
229
+ or not isinstance(expected_hash, str)
230
+ or item.get("reason") not in TEST_TRACKING_REASONS
231
+ or type(item.get("ignored")) is not bool
232
+ or type(item.get("trackedBefore")) is not bool
233
+ ):
234
+ raise ValueError("TEST_TRACKING_MANIFEST_INVALID")
235
+ raw_path = Path(rel)
236
+ resolved = (repo_root / raw_path).resolve()
237
+ if raw_path.is_absolute() or not _inside(resolved, repo_root):
238
+ raise ValueError(f"TEST_TRACKING_PATH_OUTSIDE_PROJECT: {rel}")
239
+ normalized = resolved.relative_to(repo_root).as_posix()
240
+ if normalized != rel or not resolved.is_file():
241
+ raise ValueError(f"TEST_TRACKING_FILE_INVALID: {rel}")
242
+ content = resolved.read_bytes()
243
+ actual_hash = "sha256:" + hashlib.sha256(content).hexdigest()
244
+ if actual_hash != expected_hash:
245
+ raise ValueError(f"TEST_TRACKING_HASH_DRIFT: {rel}")
246
+ contents[rel] = content
247
+ return {rel: contents[rel] for rel in sorted(contents)}, str(manifest_path)
248
+
249
+
250
+ def compute_diff_hash(
251
+ repo_root: Path,
252
+ base: str | None = None,
253
+ change_dir: Path | str | None = None,
254
+ ) -> tuple[str, dict[str, Any]]:
172
255
  """Byte-level, commit-invariant diff hash (cluster 2).
173
256
 
174
257
  Content-based change set: every file whose working-tree content differs
@@ -179,6 +262,8 @@ def compute_diff_hash(repo_root: Path, base: str | None = None) -> tuple[str, di
179
262
  """
180
263
  repo_root = Path(repo_root).resolve()
181
264
  base, paths = _changed_paths(repo_root, base)
265
+ tracked_test_contents, manifest_path = _tracked_test_contents(repo_root, change_dir)
266
+ paths = sorted(set(paths).union(tracked_test_contents))
182
267
  digest = hashlib.sha256()
183
268
  digest.update(DIFF_HASH_VERSION.encode("utf-8"))
184
269
  digest.update(b"\x00")
@@ -187,7 +272,12 @@ def compute_diff_hash(repo_root: Path, base: str | None = None) -> tuple[str, di
187
272
  digest.update(len(path_bytes).to_bytes(4, "big"))
188
273
  digest.update(path_bytes)
189
274
  abs_path = repo_root / rel
190
- if abs_path.is_file():
275
+ if rel in tracked_test_contents:
276
+ content = tracked_test_contents[rel]
277
+ digest.update(b"\x01")
278
+ digest.update(len(content).to_bytes(8, "big"))
279
+ digest.update(content)
280
+ elif abs_path.is_file():
191
281
  content = abs_path.read_bytes()
192
282
  digest.update(b"\x01")
193
283
  digest.update(len(content).to_bytes(8, "big"))
@@ -196,6 +286,14 @@ def compute_diff_hash(repo_root: Path, base: str | None = None) -> tuple[str, di
196
286
  # Deleted since base: record absence with no content.
197
287
  digest.update(b"\x00")
198
288
  digest.update((0).to_bytes(8, "big"))
289
+ for rel, verified_content in tracked_test_contents.items():
290
+ path = repo_root / rel
291
+ try:
292
+ current_content = path.read_bytes()
293
+ except OSError as exc:
294
+ raise ValueError(f"TEST_TRACKING_HASH_DRIFT: {rel}") from exc
295
+ if current_content != verified_content:
296
+ raise ValueError(f"TEST_TRACKING_HASH_DRIFT: {rel}")
199
297
  try:
200
298
  head = _git_bytes(["rev-parse", "HEAD"], repo_root).decode("utf-8", "replace").strip()
201
299
  except RuntimeError:
@@ -205,6 +303,8 @@ def compute_diff_hash(repo_root: Path, base: str | None = None) -> tuple[str, di
205
303
  "fileCount": len(paths),
206
304
  "base": base,
207
305
  "head": head or None,
306
+ "trackedTestFileCount": len(tracked_test_contents),
307
+ "testTrackingManifest": manifest_path,
208
308
  }
209
309
  return f"sha256:{digest.hexdigest()}", meta
210
310
 
@@ -778,9 +878,10 @@ def cmd_diff_hash(args: argparse.Namespace) -> int:
778
878
  repo_raw = getattr(args, "repo", None)
779
879
  repo = Path(repo_raw).expanduser().resolve() if repo_raw else Path.cwd().resolve()
780
880
  base = getattr(args, "base", None)
881
+ change_dir = getattr(args, "change_dir", None)
781
882
  try:
782
- diff_hash, meta = compute_diff_hash(repo, base=base)
783
- except (OSError, RuntimeError) as exc:
883
+ diff_hash, meta = compute_diff_hash(repo, base=base, change_dir=change_dir)
884
+ except (OSError, RuntimeError, ValueError) as exc:
784
885
  return emit_error(str(exc), as_json=as_json)
785
886
  payload = {"ok": True, "action": "diff-hash", "diffHash": diff_hash}
786
887
  payload.update(meta)
@@ -922,6 +1023,11 @@ def build_parser() -> argparse.ArgumentParser:
922
1023
  )
923
1024
  p_diff.add_argument("--repo", default=None, help="repo root (default: cwd)")
924
1025
  p_diff.add_argument("--base", default=None, help="base commit (default: root commit)")
1026
+ p_diff.add_argument(
1027
+ "--change-dir",
1028
+ default=None,
1029
+ help="change directory whose evidence/test-tracking.json contributes ignored tests",
1030
+ )
925
1031
  p_diff.set_defaults(func=cmd_diff_hash)
926
1032
 
927
1033
  return parser
@@ -313,7 +313,23 @@ def _parse_scalar(val: str) -> Any:
313
313
  return val
314
314
 
315
315
 
316
- def cmd_check_agents(skills_root: Path, agent: str) -> dict[str, Any]:
316
+ def _resolve_agents_root(skills_root: Path, agents_root: Path | None) -> tuple[Path | None, str | None]:
317
+ """Resolve agent definitions without confusing a skills root with its sibling."""
318
+ if agents_root is not None:
319
+ return agents_root.resolve(), None
320
+ if skills_root.name == "skills" and skills_root.parent.name in (".claude", ".codebuddy"):
321
+ return (skills_root.parent / "agents").resolve(), None
322
+ if skills_root.name == "skills" and skills_root.parent.name in (".agents", ".cursor"):
323
+ return None, "CUSTOM_AGENTS_UNSUPPORTED"
324
+ return (skills_root / "agents").resolve(), None
325
+
326
+
327
+ def cmd_check_agents(
328
+ skills_root: Path,
329
+ agent: str,
330
+ *,
331
+ agents_root: Path | None = None,
332
+ ) -> dict[str, Any]:
317
333
  skills_root = skills_root.resolve()
318
334
  agent_name = agent.strip()
319
335
  if not agent_name:
@@ -325,9 +341,23 @@ def cmd_check_agents(skills_root: Path, agent: str) -> dict[str, Any]:
325
341
  "reason": "agent name is empty",
326
342
  }
327
343
 
344
+ resolved_agents_root, capability_error = _resolve_agents_root(skills_root, agents_root)
345
+ if capability_error is not None:
346
+ return {
347
+ "ok": True,
348
+ "action": "check-agents",
349
+ "agent": agent_name,
350
+ "usable": False,
351
+ "definitionValid": False,
352
+ "runtimeSupported": False,
353
+ "reasonCode": capability_error,
354
+ "reason": "current adapter runtime does not support custom agent definitions; use main-session fallback",
355
+ }
356
+
357
+ assert resolved_agents_root is not None
328
358
  # Accept bare name or with .md
329
359
  filename = agent_name if agent_name.endswith(".md") else f"{agent_name}.md"
330
- agent_path = skills_root / "agents" / filename
360
+ agent_path = resolved_agents_root / filename
331
361
 
332
362
  if not agent_path.is_file():
333
363
  return {
@@ -335,7 +365,11 @@ def cmd_check_agents(skills_root: Path, agent: str) -> dict[str, Any]:
335
365
  "action": "check-agents",
336
366
  "agent": agent_name,
337
367
  "path": str(agent_path),
368
+ "agentsRoot": str(resolved_agents_root),
338
369
  "usable": False,
370
+ "definitionValid": False,
371
+ "runtimeSupported": True,
372
+ "reasonCode": "AGENT_DEFINITION_NOT_FOUND",
339
373
  "reason": f"agent file not found: {agent_path}",
340
374
  }
341
375
 
@@ -390,7 +424,11 @@ def cmd_check_agents(skills_root: Path, agent: str) -> dict[str, Any]:
390
424
  "action": "check-agents",
391
425
  "agent": agent_name,
392
426
  "path": str(agent_path),
427
+ "agentsRoot": str(resolved_agents_root),
393
428
  "usable": True,
429
+ "definitionValid": True,
430
+ "runtimeSupported": True,
431
+ "reasonCode": "READY",
394
432
  "reason": "agent file exists; frontmatter parsed; tools declared",
395
433
  "name": name_field,
396
434
  "tools": tools,
@@ -431,6 +469,7 @@ def build_parser() -> argparse.ArgumentParser:
431
469
 
432
470
  p_agents = sub.add_parser("check-agents", help="Validate agent definition usability")
433
471
  p_agents.add_argument("--skills-root", required=True, type=Path)
472
+ p_agents.add_argument("--agents-root", type=Path)
434
473
  p_agents.add_argument("--agent", required=True)
435
474
  p_agents.add_argument("--json", action="store_true")
436
475
 
@@ -459,7 +498,11 @@ def main(argv: list[str] | None = None) -> int:
459
498
  )
460
499
  return emit_json(result, ok=bool(result.get("ok", False)))
461
500
  if args.command == "check-agents":
462
- result = cmd_check_agents(args.skills_root, args.agent)
501
+ result = cmd_check_agents(
502
+ args.skills_root,
503
+ args.agent,
504
+ agents_root=args.agents_root,
505
+ )
463
506
  return emit_json(result, ok=True)
464
507
 
465
508
  parser.error(f"unknown command: {args.command}")
@@ -223,8 +223,8 @@ def _java_commands(reactor_modules: list[str], pom_hash: str) -> dict[str, Any]:
223
223
  "basis": dict(basis),
224
224
  },
225
225
  "package": {
226
- "command": "mvn -f pom.xml package '-Dmaven.test.skip=true'",
227
- "argvTemplate": ["mvn", "-f", "pom.xml", "package", "-Dmaven.test.skip=true"],
226
+ "command": "mvn -f pom.xml clean package '-Dmaven.test.skip=true'",
227
+ "argvTemplate": ["mvn", "-f", "pom.xml", "clean", "package", "-Dmaven.test.skip=true"],
228
228
  "scope": "module",
229
229
  "inputs": _cmd_inputs(reactor_modules, pom=True, main=True, test=True),
230
230
  "coverage": "package",
@@ -324,6 +324,13 @@ def empty_profile_skeleton(excluded: tuple[str, ...] | list[str]) -> dict[str, A
324
324
  "knownPreexistingErrors": [],
325
325
  "shellQuirks": [],
326
326
  "fingerprint": {"mvnVersion": "", "nodeVersion": "", "pomHash": ""},
327
+ "testTracking": {
328
+ "source": "detected",
329
+ "mode": "force-track-touched",
330
+ "paths": [],
331
+ "staleTestPolicy": "safe-repair",
332
+ "forbidTemporaryExclusion": True,
333
+ },
327
334
  }
328
335
 
329
336
 
@@ -357,6 +364,9 @@ def _merge_user_overrides(
357
364
  profile["knownPreexistingErrors"] = list(existing["knownPreexistingErrors"])
358
365
  if isinstance(existing.get("shellQuirks"), list):
359
366
  profile["shellQuirks"] = list(existing["shellQuirks"])
367
+ test_tracking = existing.get("testTracking")
368
+ if isinstance(test_tracking, dict) and test_tracking.get("source") == "user":
369
+ profile["testTracking"] = dict(test_tracking)
360
370
  # 兼容字段:用户在 v1 配置的 verificationInputs 不保留(无 provenance),
361
371
  # 由 commands 重新派生。knownPreexistingErrors/shellQuirks 是人工标注,保留。
362
372
 
@@ -392,10 +402,28 @@ def detect(project: Path) -> dict[str, Any]:
392
402
  reactor_modules = find_reactor_modules(project, excluded)
393
403
  if reactor_modules:
394
404
  profile["commands"] = _java_commands(reactor_modules, pom_hash)
405
+ profile["testTracking"]["paths"] = sorted(
406
+ "src/test/**" if module == "." else f"{module}/src/test/**"
407
+ for module in reactor_modules
408
+ )
395
409
  elif project_type == "node":
396
410
  node_cmds = _node_commands(project)
397
411
  if node_cmds:
398
412
  profile["commands"] = node_cmds
413
+ profile["testTracking"]["paths"] = [
414
+ pattern
415
+ for extension in ("js", "jsx", "ts", "tsx", "mjs", "cjs")
416
+ for pattern in (
417
+ f"apps/*/test/**/*.{extension}",
418
+ f"apps/*/tests/**/*.{extension}",
419
+ f"packages/*/test/**/*.{extension}",
420
+ f"packages/*/tests/**/*.{extension}",
421
+ f"test/**/*.{extension}",
422
+ f"tests/**/*.{extension}",
423
+ f"**/*.test.{extension}",
424
+ f"**/*.spec.{extension}",
425
+ )
426
+ ]
399
427
 
400
428
  _merge_user_overrides(profile, existing)
401
429
  _derive_verification_inputs(profile)
@@ -719,7 +747,7 @@ def main(argv: list[str] | None = None) -> int:
719
747
  if args.command == "check":
720
748
  result = check(args.project)
721
749
  sys.stdout.write(json.dumps(result, ensure_ascii=False, indent=2) + "\n")
722
- return 0 if not result.get("stale") else 0 # stale still exits 0 (callers branch on JSON)
750
+ return 0 if result.get("ok") and not result.get("stale") else 1
723
751
  if args.command == "validate":
724
752
  profile = load_profile(args.project.resolve())
725
753
  if profile is None:
@@ -12,9 +12,11 @@ Python 3.10+ stdlib only. UTF-8 without BOM. Windows path friendly.
12
12
  from __future__ import annotations
13
13
 
14
14
  import datetime as dt
15
+ import argparse
15
16
  import hashlib
16
17
  import json
17
18
  import os
19
+ import subprocess
18
20
  import sys
19
21
  from pathlib import Path
20
22
  from typing import Any
@@ -177,3 +179,149 @@ def refresh_segments(
177
179
  refreshed["segments"] = refreshed_segments
178
180
  refreshed["generatedAt"] = now_iso()
179
181
  return refreshed
182
+
183
+
184
+ def _git(project: Path, *args: str) -> str:
185
+ proc = subprocess.run(
186
+ ["git", "-C", str(project), *args],
187
+ capture_output=True,
188
+ text=True,
189
+ encoding="utf-8",
190
+ errors="replace",
191
+ check=False,
192
+ )
193
+ return proc.stdout.strip() if proc.returncode == 0 else ""
194
+
195
+
196
+ def _existing_files(paths: list[Path]) -> list[str]:
197
+ return sorted({str(path.resolve()) for path in paths if path.is_file()})
198
+
199
+
200
+ def discover_segment_files(
201
+ project: Path, change_dir: Path, *, base: str = "", head: str = "HEAD"
202
+ ) -> dict[str, list[str]]:
203
+ """Discover the compact inputs shared by plan/run/test/review/submit."""
204
+ project = project.resolve()
205
+ rules = [project / "AGENTS.md", project / "CLAUDE.md"]
206
+ for pattern in (
207
+ ".claude/rules/**/*",
208
+ ".cursor/rules/**/*",
209
+ ".codebuddy/rules/**/*",
210
+ ".codebuddy/.rules/**/*",
211
+ ):
212
+ rules.extend(project.glob(pattern))
213
+
214
+ profile = [
215
+ project / ".harness/config/build-profile.json",
216
+ project / ".harness/context-index.json",
217
+ project / "project.yaml",
218
+ project / "harness.json",
219
+ ]
220
+ codebase_map = list((project / ".harness/codebase/map").glob("**/*"))
221
+ knowledge = [
222
+ project / ".harness/knowledge/index.json",
223
+ project / ".harness/knowledge/status.json",
224
+ project / ".harness/knowledge/config.json",
225
+ ]
226
+ change_inputs: list[Path] = []
227
+ for folder in ("spec", "plans", "meta"):
228
+ change_inputs.extend((change_dir / folder).glob("**/*"))
229
+ change_inputs = [path for path in change_inputs if path != snapshot_path(change_dir)]
230
+
231
+ changed_names: list[str] = []
232
+ if base:
233
+ raw = _git(project, "diff", "--name-only", "--diff-filter=ACMRT", f"{base}..{head}")
234
+ changed_names = [line for line in raw.splitlines() if line.strip()]
235
+ code = [project / name for name in changed_names]
236
+ return {
237
+ "profile": _existing_files(profile),
238
+ "rules": _existing_files(rules),
239
+ "map": _existing_files(codebase_map),
240
+ "knowledge": _existing_files(knowledge),
241
+ "change": _existing_files(change_inputs),
242
+ "code": _existing_files(code),
243
+ }
244
+
245
+
246
+ def capture_current_state(
247
+ *,
248
+ project: Path,
249
+ change_dir: Path,
250
+ change_name: str,
251
+ worktree_root: Path,
252
+ base: str = "",
253
+ head: str = "HEAD",
254
+ ) -> tuple[dict[str, Any], list[str]]:
255
+ resolved_head = _git(project, "rev-parse", head) or head
256
+ resolved_base = _git(project, "rev-parse", base) if base else ""
257
+ files = discover_segment_files(project, change_dir, base=resolved_base, head=resolved_head)
258
+ previous = load_snapshot(change_dir)
259
+ fresh = capture_snapshot(
260
+ change_dir,
261
+ change_name=change_name,
262
+ project=project,
263
+ worktree_root=worktree_root,
264
+ base=resolved_base,
265
+ head=resolved_head,
266
+ segment_files=files,
267
+ )
268
+ changed: list[str] = []
269
+ if previous:
270
+ old_segments = previous.get("segments") or {}
271
+ for name, segment in fresh["segments"].items():
272
+ old = old_segments.get(name)
273
+ if isinstance(old, dict) and old.get("fingerprint") == segment.get("fingerprint"):
274
+ segment["capturedAt"] = old.get("capturedAt", segment["capturedAt"])
275
+ else:
276
+ changed.append(name)
277
+ else:
278
+ changed = list(fresh["segments"])
279
+ write_snapshot(change_dir, fresh)
280
+ return fresh, changed
281
+
282
+
283
+ def build_parser() -> argparse.ArgumentParser:
284
+ parser = argparse.ArgumentParser(description="Capture reusable Harness state")
285
+ sub = parser.add_subparsers(dest="command", required=True)
286
+ capture = sub.add_parser("capture", help="capture or refresh changed segments")
287
+ capture.add_argument("--project", default=".")
288
+ capture.add_argument("--change-dir", required=True)
289
+ capture.add_argument("--change-name")
290
+ capture.add_argument("--worktree-root")
291
+ capture.add_argument("--base", default="")
292
+ capture.add_argument("--head", default="HEAD")
293
+ capture.add_argument("--json", action="store_true")
294
+ return parser
295
+
296
+
297
+ def main(argv: list[str] | None = None) -> int:
298
+ args = build_parser().parse_args(argv)
299
+ project = Path(args.project).resolve()
300
+ change_dir = Path(args.change_dir).resolve()
301
+ name = args.change_name or change_dir.name
302
+ worktree = Path(args.worktree_root).resolve() if args.worktree_root else project
303
+ snapshot, changed = capture_current_state(
304
+ project=project,
305
+ change_dir=change_dir,
306
+ change_name=name,
307
+ worktree_root=worktree,
308
+ base=args.base,
309
+ head=args.head,
310
+ )
311
+ result = {
312
+ "ok": True,
313
+ "path": str(snapshot_path(change_dir)),
314
+ "changedSegments": changed,
315
+ "reusedSegments": sorted(set(snapshot["segments"]) - set(changed)),
316
+ "git": snapshot["git"],
317
+ }
318
+ if args.json:
319
+ print(json.dumps(result, ensure_ascii=False))
320
+ else:
321
+ print(f"state snapshot: {result['path']}")
322
+ print(f"changed: {', '.join(changed) if changed else 'none'}")
323
+ return 0
324
+
325
+
326
+ if __name__ == "__main__":
327
+ raise SystemExit(main())