@hunter-harness/workflow-harness 0.2.3 → 0.2.5

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 (226) hide show
  1. package/harness/bundles/general/claude-code/.harness-build.json +1 -1
  2. package/harness/bundles/general/claude-code/README.md +12 -11
  3. package/harness/bundles/general/claude-code/harness-archive/SKILL.md +7 -9
  4. package/harness/bundles/general/claude-code/harness-archive/checklist.md +4 -4
  5. package/harness/bundles/general/claude-code/harness-archive/reference.md +3 -5
  6. package/harness/bundles/general/claude-code/harness-codebase-map/SKILL.md +2 -2
  7. package/harness/bundles/general/claude-code/harness-knowledge-ingest/SKILL.md +2 -2
  8. package/harness/bundles/general/claude-code/harness-knowledge-query/SKILL.md +10 -16
  9. package/harness/bundles/general/claude-code/harness-plan/SKILL.md +14 -10
  10. package/harness/bundles/general/claude-code/harness-plan/checklist.md +28 -13
  11. package/harness/bundles/general/claude-code/harness-plan/protocols.md +10 -3
  12. package/harness/bundles/general/claude-code/harness-plan/reference.md +9 -23
  13. package/harness/bundles/general/claude-code/harness-review/SKILL.md +2 -2
  14. package/harness/bundles/general/claude-code/harness-run/SKILL.md +16 -4
  15. package/harness/bundles/general/claude-code/harness-run/checklist.md +7 -3
  16. package/harness/bundles/general/claude-code/harness-run/protocols.md +3 -2
  17. package/harness/bundles/general/claude-code/harness-run/reference.md +3 -9
  18. package/harness/bundles/general/claude-code/harness-submit/SKILL.md +6 -4
  19. package/harness/bundles/general/claude-code/harness-submit/checklist.md +10 -1
  20. package/harness/bundles/general/claude-code/harness-sync/SKILL.md +2 -2
  21. package/harness/bundles/general/claude-code/harness-test/SKILL.md +15 -3
  22. package/harness/bundles/general/claude-code/harness-test/checklist.md +4 -0
  23. package/harness/bundles/general/claude-code/harness-test/reference.md +4 -4
  24. package/harness/bundles/general/claude-code/protocols/archive-report-protocol.md +2 -4
  25. package/harness/bundles/general/claude-code/protocols/ledger-protocol.md +7 -15
  26. package/harness/bundles/general/claude-code/scripts/harness_ledger.py +111 -5
  27. package/harness/bundles/general/claude-code/scripts/harness_profile.py +29 -1
  28. package/harness/bundles/general/claude-code/scripts/harness_test_guard.py +502 -0
  29. package/harness/bundles/general/codebuddy/.harness-build.json +1 -1
  30. package/harness/bundles/general/codebuddy/README.md +12 -11
  31. package/harness/bundles/general/codebuddy/harness-archive/SKILL.md +7 -9
  32. package/harness/bundles/general/codebuddy/harness-archive/checklist.md +4 -4
  33. package/harness/bundles/general/codebuddy/harness-archive/reference.md +3 -5
  34. package/harness/bundles/general/codebuddy/harness-codebase-map/SKILL.md +2 -2
  35. package/harness/bundles/general/codebuddy/harness-knowledge-ingest/SKILL.md +2 -2
  36. package/harness/bundles/general/codebuddy/harness-knowledge-query/SKILL.md +10 -16
  37. package/harness/bundles/general/codebuddy/harness-plan/SKILL.md +13 -9
  38. package/harness/bundles/general/codebuddy/harness-plan/checklist.md +28 -13
  39. package/harness/bundles/general/codebuddy/harness-plan/protocols.md +10 -3
  40. package/harness/bundles/general/codebuddy/harness-plan/reference.md +9 -23
  41. package/harness/bundles/general/codebuddy/harness-review/SKILL.md +2 -2
  42. package/harness/bundles/general/codebuddy/harness-run/SKILL.md +16 -4
  43. package/harness/bundles/general/codebuddy/harness-run/checklist.md +7 -3
  44. package/harness/bundles/general/codebuddy/harness-run/protocols.md +3 -2
  45. package/harness/bundles/general/codebuddy/harness-run/reference.md +3 -9
  46. package/harness/bundles/general/codebuddy/harness-submit/SKILL.md +6 -4
  47. package/harness/bundles/general/codebuddy/harness-submit/checklist.md +10 -1
  48. package/harness/bundles/general/codebuddy/harness-sync/SKILL.md +2 -2
  49. package/harness/bundles/general/codebuddy/harness-test/SKILL.md +15 -3
  50. package/harness/bundles/general/codebuddy/harness-test/checklist.md +4 -0
  51. package/harness/bundles/general/codebuddy/harness-test/reference.md +4 -4
  52. package/harness/bundles/general/codebuddy/protocols/archive-report-protocol.md +2 -4
  53. package/harness/bundles/general/codebuddy/protocols/ledger-protocol.md +7 -15
  54. package/harness/bundles/general/codebuddy/scripts/harness_ledger.py +111 -5
  55. package/harness/bundles/general/codebuddy/scripts/harness_profile.py +29 -1
  56. package/harness/bundles/general/codebuddy/scripts/harness_test_guard.py +502 -0
  57. package/harness/bundles/general/codex/.harness-build.json +1 -1
  58. package/harness/bundles/general/codex/README.md +12 -11
  59. package/harness/bundles/general/codex/harness-archive/SKILL.md +7 -9
  60. package/harness/bundles/general/codex/harness-archive/checklist.md +4 -4
  61. package/harness/bundles/general/codex/harness-archive/reference.md +3 -5
  62. package/harness/bundles/general/codex/harness-codebase-map/SKILL.md +2 -2
  63. package/harness/bundles/general/codex/harness-knowledge-ingest/SKILL.md +2 -2
  64. package/harness/bundles/general/codex/harness-knowledge-query/SKILL.md +10 -16
  65. package/harness/bundles/general/codex/harness-plan/SKILL.md +13 -9
  66. package/harness/bundles/general/codex/harness-plan/checklist.md +28 -13
  67. package/harness/bundles/general/codex/harness-plan/protocols.md +10 -3
  68. package/harness/bundles/general/codex/harness-plan/reference.md +9 -23
  69. package/harness/bundles/general/codex/harness-review/SKILL.md +2 -2
  70. package/harness/bundles/general/codex/harness-run/SKILL.md +16 -4
  71. package/harness/bundles/general/codex/harness-run/checklist.md +7 -3
  72. package/harness/bundles/general/codex/harness-run/protocols.md +3 -2
  73. package/harness/bundles/general/codex/harness-run/reference.md +3 -9
  74. package/harness/bundles/general/codex/harness-submit/SKILL.md +6 -4
  75. package/harness/bundles/general/codex/harness-submit/checklist.md +10 -1
  76. package/harness/bundles/general/codex/harness-sync/SKILL.md +2 -2
  77. package/harness/bundles/general/codex/harness-test/SKILL.md +15 -3
  78. package/harness/bundles/general/codex/harness-test/checklist.md +4 -0
  79. package/harness/bundles/general/codex/harness-test/reference.md +4 -4
  80. package/harness/bundles/general/codex/protocols/archive-report-protocol.md +2 -4
  81. package/harness/bundles/general/codex/protocols/ledger-protocol.md +7 -15
  82. package/harness/bundles/general/codex/scripts/harness_ledger.py +111 -5
  83. package/harness/bundles/general/codex/scripts/harness_profile.py +29 -1
  84. package/harness/bundles/general/codex/scripts/harness_test_guard.py +502 -0
  85. package/harness/bundles/general/cursor/.harness-build.json +1 -1
  86. package/harness/bundles/general/cursor/README.md +12 -11
  87. package/harness/bundles/general/cursor/harness-archive/SKILL.md +7 -9
  88. package/harness/bundles/general/cursor/harness-archive/checklist.md +4 -4
  89. package/harness/bundles/general/cursor/harness-archive/reference.md +3 -5
  90. package/harness/bundles/general/cursor/harness-codebase-map/SKILL.md +2 -2
  91. package/harness/bundles/general/cursor/harness-knowledge-ingest/SKILL.md +2 -2
  92. package/harness/bundles/general/cursor/harness-knowledge-query/SKILL.md +10 -16
  93. package/harness/bundles/general/cursor/harness-plan/SKILL.md +13 -9
  94. package/harness/bundles/general/cursor/harness-plan/checklist.md +28 -13
  95. package/harness/bundles/general/cursor/harness-plan/protocols.md +10 -3
  96. package/harness/bundles/general/cursor/harness-plan/reference.md +9 -23
  97. package/harness/bundles/general/cursor/harness-review/SKILL.md +2 -2
  98. package/harness/bundles/general/cursor/harness-run/SKILL.md +16 -4
  99. package/harness/bundles/general/cursor/harness-run/checklist.md +7 -3
  100. package/harness/bundles/general/cursor/harness-run/protocols.md +3 -2
  101. package/harness/bundles/general/cursor/harness-run/reference.md +3 -9
  102. package/harness/bundles/general/cursor/harness-submit/SKILL.md +6 -4
  103. package/harness/bundles/general/cursor/harness-submit/checklist.md +10 -1
  104. package/harness/bundles/general/cursor/harness-sync/SKILL.md +2 -2
  105. package/harness/bundles/general/cursor/harness-test/SKILL.md +15 -3
  106. package/harness/bundles/general/cursor/harness-test/checklist.md +4 -0
  107. package/harness/bundles/general/cursor/harness-test/reference.md +4 -4
  108. package/harness/bundles/general/cursor/protocols/archive-report-protocol.md +2 -4
  109. package/harness/bundles/general/cursor/protocols/ledger-protocol.md +7 -15
  110. package/harness/bundles/general/cursor/scripts/harness_ledger.py +111 -5
  111. package/harness/bundles/general/cursor/scripts/harness_profile.py +29 -1
  112. package/harness/bundles/general/cursor/scripts/harness_test_guard.py +502 -0
  113. package/harness/bundles/java/claude-code/.harness-build.json +1 -1
  114. package/harness/bundles/java/claude-code/README.md +12 -11
  115. package/harness/bundles/java/claude-code/harness-apidoc/SKILL.md +2 -2
  116. package/harness/bundles/java/claude-code/harness-archive/SKILL.md +7 -9
  117. package/harness/bundles/java/claude-code/harness-archive/checklist.md +4 -4
  118. package/harness/bundles/java/claude-code/harness-archive/reference.md +3 -5
  119. package/harness/bundles/java/claude-code/harness-codebase-map/SKILL.md +2 -2
  120. package/harness/bundles/java/claude-code/harness-knowledge-ingest/SKILL.md +2 -2
  121. package/harness/bundles/java/claude-code/harness-knowledge-query/SKILL.md +10 -16
  122. package/harness/bundles/java/claude-code/harness-package/SKILL.md +2 -2
  123. package/harness/bundles/java/claude-code/harness-plan/SKILL.md +14 -10
  124. package/harness/bundles/java/claude-code/harness-plan/checklist.md +28 -13
  125. package/harness/bundles/java/claude-code/harness-plan/protocols.md +10 -3
  126. package/harness/bundles/java/claude-code/harness-plan/reference.md +9 -23
  127. package/harness/bundles/java/claude-code/harness-review/SKILL.md +2 -2
  128. package/harness/bundles/java/claude-code/harness-run/SKILL.md +19 -5
  129. package/harness/bundles/java/claude-code/harness-run/protocols.md +3 -2
  130. package/harness/bundles/java/claude-code/harness-submit/SKILL.md +6 -4
  131. package/harness/bundles/java/claude-code/harness-submit/checklist.md +10 -1
  132. package/harness/bundles/java/claude-code/harness-sync/SKILL.md +2 -2
  133. package/harness/bundles/java/claude-code/harness-test/SKILL.md +17 -3
  134. package/harness/bundles/java/claude-code/protocols/archive-report-protocol.md +2 -4
  135. package/harness/bundles/java/claude-code/protocols/ledger-protocol.md +7 -15
  136. package/harness/bundles/java/claude-code/scripts/harness_ledger.py +111 -5
  137. package/harness/bundles/java/claude-code/scripts/harness_profile.py +29 -1
  138. package/harness/bundles/java/claude-code/scripts/harness_test_guard.py +502 -0
  139. package/harness/bundles/java/codebuddy/.harness-build.json +1 -1
  140. package/harness/bundles/java/codebuddy/README.md +12 -11
  141. package/harness/bundles/java/codebuddy/harness-apidoc/SKILL.md +2 -2
  142. package/harness/bundles/java/codebuddy/harness-archive/SKILL.md +7 -9
  143. package/harness/bundles/java/codebuddy/harness-archive/checklist.md +4 -4
  144. package/harness/bundles/java/codebuddy/harness-archive/reference.md +3 -5
  145. package/harness/bundles/java/codebuddy/harness-codebase-map/SKILL.md +2 -2
  146. package/harness/bundles/java/codebuddy/harness-knowledge-ingest/SKILL.md +2 -2
  147. package/harness/bundles/java/codebuddy/harness-knowledge-query/SKILL.md +10 -16
  148. package/harness/bundles/java/codebuddy/harness-package/SKILL.md +2 -2
  149. package/harness/bundles/java/codebuddy/harness-plan/SKILL.md +13 -9
  150. package/harness/bundles/java/codebuddy/harness-plan/checklist.md +28 -13
  151. package/harness/bundles/java/codebuddy/harness-plan/protocols.md +10 -3
  152. package/harness/bundles/java/codebuddy/harness-plan/reference.md +9 -23
  153. package/harness/bundles/java/codebuddy/harness-review/SKILL.md +2 -2
  154. package/harness/bundles/java/codebuddy/harness-run/SKILL.md +19 -5
  155. package/harness/bundles/java/codebuddy/harness-run/protocols.md +3 -2
  156. package/harness/bundles/java/codebuddy/harness-submit/SKILL.md +6 -4
  157. package/harness/bundles/java/codebuddy/harness-submit/checklist.md +10 -1
  158. package/harness/bundles/java/codebuddy/harness-sync/SKILL.md +2 -2
  159. package/harness/bundles/java/codebuddy/harness-test/SKILL.md +17 -3
  160. package/harness/bundles/java/codebuddy/protocols/archive-report-protocol.md +2 -4
  161. package/harness/bundles/java/codebuddy/protocols/ledger-protocol.md +7 -15
  162. package/harness/bundles/java/codebuddy/scripts/harness_ledger.py +111 -5
  163. package/harness/bundles/java/codebuddy/scripts/harness_profile.py +29 -1
  164. package/harness/bundles/java/codebuddy/scripts/harness_test_guard.py +502 -0
  165. package/harness/bundles/java/codex/.harness-build.json +1 -1
  166. package/harness/bundles/java/codex/README.md +12 -11
  167. package/harness/bundles/java/codex/harness-apidoc/SKILL.md +2 -2
  168. package/harness/bundles/java/codex/harness-archive/SKILL.md +7 -9
  169. package/harness/bundles/java/codex/harness-archive/checklist.md +4 -4
  170. package/harness/bundles/java/codex/harness-archive/reference.md +3 -5
  171. package/harness/bundles/java/codex/harness-codebase-map/SKILL.md +2 -2
  172. package/harness/bundles/java/codex/harness-knowledge-ingest/SKILL.md +2 -2
  173. package/harness/bundles/java/codex/harness-knowledge-query/SKILL.md +10 -16
  174. package/harness/bundles/java/codex/harness-package/SKILL.md +2 -2
  175. package/harness/bundles/java/codex/harness-plan/SKILL.md +13 -9
  176. package/harness/bundles/java/codex/harness-plan/checklist.md +28 -13
  177. package/harness/bundles/java/codex/harness-plan/protocols.md +10 -3
  178. package/harness/bundles/java/codex/harness-plan/reference.md +9 -23
  179. package/harness/bundles/java/codex/harness-review/SKILL.md +2 -2
  180. package/harness/bundles/java/codex/harness-run/SKILL.md +19 -5
  181. package/harness/bundles/java/codex/harness-run/protocols.md +3 -2
  182. package/harness/bundles/java/codex/harness-submit/SKILL.md +6 -4
  183. package/harness/bundles/java/codex/harness-submit/checklist.md +10 -1
  184. package/harness/bundles/java/codex/harness-sync/SKILL.md +2 -2
  185. package/harness/bundles/java/codex/harness-test/SKILL.md +17 -3
  186. package/harness/bundles/java/codex/protocols/archive-report-protocol.md +2 -4
  187. package/harness/bundles/java/codex/protocols/ledger-protocol.md +7 -15
  188. package/harness/bundles/java/codex/scripts/harness_ledger.py +111 -5
  189. package/harness/bundles/java/codex/scripts/harness_profile.py +29 -1
  190. package/harness/bundles/java/codex/scripts/harness_test_guard.py +502 -0
  191. package/harness/bundles/java/cursor/.harness-build.json +1 -1
  192. package/harness/bundles/java/cursor/README.md +12 -11
  193. package/harness/bundles/java/cursor/harness-apidoc/SKILL.md +2 -2
  194. package/harness/bundles/java/cursor/harness-archive/SKILL.md +7 -9
  195. package/harness/bundles/java/cursor/harness-archive/checklist.md +4 -4
  196. package/harness/bundles/java/cursor/harness-archive/reference.md +3 -5
  197. package/harness/bundles/java/cursor/harness-codebase-map/SKILL.md +2 -2
  198. package/harness/bundles/java/cursor/harness-knowledge-ingest/SKILL.md +2 -2
  199. package/harness/bundles/java/cursor/harness-knowledge-query/SKILL.md +10 -16
  200. package/harness/bundles/java/cursor/harness-package/SKILL.md +2 -2
  201. package/harness/bundles/java/cursor/harness-plan/SKILL.md +13 -9
  202. package/harness/bundles/java/cursor/harness-plan/checklist.md +28 -13
  203. package/harness/bundles/java/cursor/harness-plan/protocols.md +10 -3
  204. package/harness/bundles/java/cursor/harness-plan/reference.md +9 -23
  205. package/harness/bundles/java/cursor/harness-review/SKILL.md +2 -2
  206. package/harness/bundles/java/cursor/harness-run/SKILL.md +19 -5
  207. package/harness/bundles/java/cursor/harness-run/protocols.md +3 -2
  208. package/harness/bundles/java/cursor/harness-submit/SKILL.md +6 -4
  209. package/harness/bundles/java/cursor/harness-submit/checklist.md +10 -1
  210. package/harness/bundles/java/cursor/harness-sync/SKILL.md +2 -2
  211. package/harness/bundles/java/cursor/harness-test/SKILL.md +17 -3
  212. package/harness/bundles/java/cursor/protocols/archive-report-protocol.md +2 -4
  213. package/harness/bundles/java/cursor/protocols/ledger-protocol.md +7 -15
  214. package/harness/bundles/java/cursor/scripts/harness_ledger.py +111 -5
  215. package/harness/bundles/java/cursor/scripts/harness_profile.py +29 -1
  216. package/harness/bundles/java/cursor/scripts/harness_test_guard.py +502 -0
  217. package/harness/manifests/general/claude-code.json +32 -28
  218. package/harness/manifests/general/codebuddy.json +32 -28
  219. package/harness/manifests/general/codex.json +32 -28
  220. package/harness/manifests/general/cursor.json +32 -28
  221. package/harness/manifests/java/claude-code.json +30 -26
  222. package/harness/manifests/java/codebuddy.json +30 -26
  223. package/harness/manifests/java/codex.json +30 -26
  224. package/harness/manifests/java/cursor.json +30 -26
  225. package/hunter-workflow-family.json +2 -2
  226. package/package.json +1 -1
@@ -25,7 +25,7 @@ description: harness-submit 的8 步工作流详细步骤和执行日志记录
25
25
  **读取 verification-ledger + post-test 变更分类**:
26
26
 
27
27
  1. 读取 `.harness/changes/<change-name>/evidence/verification-ledger.json`,记录 `diffHash` / `currentHead` / `unitTest` / `apiTest` 状态
28
- 2. 计算当前 diff diffHash,与 ledger 比对
28
+ 2. 执行 `harness_ledger.py diff-hash --repo . --base <baseCommit> --change-dir ".harness/changes/<change-name>" --json` 计算当前 diffHash;manifest 校验失败即停止,不得绕过后与 ledger 比对
29
29
  3. 若 test 之后有代码变更(当前 diffHash ≠ test 完成时的 diffHash),按 7 类对 post-test diff 分类(见 `../protocols/ledger-protocol.md` 第六章),写入 ledger 的 `postTestClassification`
30
30
  4. 行为性变更(BEHAVIORAL_SERVICE_CHANGE / API_CONTRACT_CHANGE / SQL_OR_MAPPER_CHANGE / SECURITY_OR_PERMISSION_CHANGE)→ **停止 submit**,提示先重跑 `/harness-test` 相关场景
31
31
  5. 非行为性变更(NON_BEHAVIORAL_CLEANUP / COMMENT_ONLY / TEST_ONLY)→ 可继续 submit,但后续验证按复用规则处理
@@ -116,6 +116,13 @@ python <skills-root>/scripts/harness_ledger.py can-reuse `
116
116
 
117
117
  ## 步骤 3:.gitignore 检查 + 暂存业务文件 ⚠️ 强制阻断
118
118
 
119
+ ### Ignored test 精确暂存
120
+
121
+ - [ ] 检查 `.harness/changes/<change-name>/evidence/test-tracking.json` 是否存在
122
+ - [ ] 存在时先执行 `python <skills-root>/scripts/harness_test_guard.py stage --project . --change-dir ".harness/changes/<change-name>" --json`;任一 manifest/path/hash/index 校验失败立即停止
123
+ - [ ] 用 `git diff --cached --name-only` 验证 manifest 中每个精确路径均已暂存,且没有 guard 引入的额外路径
124
+ - [ ] 无 manifest 时禁止使用 `git add -f`;有 manifest 时也只允许 guard 内部的 exact force-add,**禁止全局 force-add**、目录级 force-add 和全局修改 `.gitignore`
125
+
119
126
  提交前**强制检查 `.gitignore`** 是否包含 `.harness/`:
120
127
 
121
128
  ```powershell
@@ -406,6 +413,8 @@ powershell.exe -Command "git -C '<项目路径>' push"
406
413
 
407
414
  ### 步骤 M7:清理 worktree
408
415
 
416
+ 若存在 test-tracking manifest,删除 worktree 前必须逐一确认 manifest 路径已包含在 worktree commit 中,且合并后的主分支可用 `git ls-files --error-unmatch -- <exact-path>` 找到;任一路径缺失即停止清理并报告,防止 ignored test 随 worktree 被永久删除。
417
+
409
418
  ```powershell
410
419
  powershell.exe -Command "git -C '<项目路径>' worktree remove '<worktree-path>' --force"
411
420
  powershell.exe -Command "git -C '<项目路径>' branch -D worktree/<change-name>"
@@ -19,7 +19,7 @@ disallowed-tools:
19
19
  - Bash(awk *)
20
20
  - Bash(curl *)
21
21
  ---
22
- <!-- generated by harness_deploy.py; core=959b22fdc1e7af33; overlay=java; agent=claude-code; do not edit -->
22
+ <!-- generated by harness_deploy.py; core=a90894841bbf814e; overlay=java; agent=claude-code; do not edit -->
23
23
  # harness-sync — 元数据同步
24
24
 
25
25
  ## Purpose
@@ -71,7 +71,7 @@ disallowed-tools:
71
71
 
72
72
  ## 执行日志
73
73
 
74
- `events.ndjson` 为唯一事实源(schema_version 2,`note` 承载人类可读摘要);`logs/execution-log.md` 由 `harness_events.py` 渲染,**禁止手工 Edit**。结构 → [[../protocols/report-pipeline-protocol.md|report-pipeline-protocol]]
74
+ `events.ndjson` 为唯一事实源(schema_version 2,`note` 承载人类可读摘要);`logs/execution-log.md` 由 `harness_events.py` 渲染,**禁止用 Write/Edit 直接维护**。直接修改的内容会在 `phase.end` 或 finalize 时被完整重建覆盖,属于数据丢失;需要保留的详情必须进入事件 `note`。结构 → [[../protocols/report-pipeline-protocol.md|report-pipeline-protocol]]
75
75
 
76
76
  ```powershell
77
77
  python <skills-root>/scripts/harness_events.py append --change-dir ".harness/changes/<change-name>" --phase <phase> --type phase.start --note "<触发指令>"
@@ -22,7 +22,7 @@ disallowed-tools:
22
22
  - Bash(node *)
23
23
  - Bash(codegraph *)
24
24
  ---
25
- <!-- generated by harness_deploy.py; core=959b22fdc1e7af33; overlay=java; agent=claude-code; do not edit -->
25
+ <!-- generated by harness_deploy.py; core=a90894841bbf814e; overlay=java; agent=claude-code; do not edit -->
26
26
  # harness-test — 测试执行
27
27
 
28
28
  ## Purpose
@@ -101,7 +101,9 @@ disallowed-tools:
101
101
  - **编译门禁**:测试前按 `build-profile.json` 的 `commands.compile`(v2,profile key resolve);输出须含 `BUILD SUCCESS` 或 exit 0 证据
102
102
  - **条件 install**:worktree 首建或上游模块变更时按 profile 执行 `commands.install`(非每次强制 `-am`)
103
103
  - **单元测试**:可复用 ledger unitTest 则跳过;否则 `commands.unitTest`(典型 `mvn test -pl <module>`);执行模块全量 `commands.unitTestFull` 成功 → 记 `unitTestFull`(scope=module,供 submit 复用)
104
+ - **陈旧测试安全修复**:测试编译明确命中已移除/改名 API 且当前代码与批准计划唯一确定新契约时,只修测试并定向重跑,记录 `stale-test-repair`;有歧义则 `BLOCKED_PREEXISTING`,禁止 `.bak`/删除/禁用/exclude
104
105
  - **服务启动**:`build-profile.json` 的 `serviceStartTemplate`(典型 `spring-boot:run` + profile);**禁止 hardcode 端口/模块路径**
106
+ - **Maven 生命周期去重**:单元测试已独立通过后,服务启动命令可配置 `-Dmaven.test.skip=true` 避免重复测试编译;它仅优化启动,不是测试通过证据
105
107
  - **服务指纹输入**(Task 3 §5.1):`serviceStart.inputFiles` 必须列出 module 源 glob(如 `["<module>/pom.xml", "<module>/src/main/**/*.java", "<module>/src/main/resources/**"]`);`harness_service.py ensure` 取 CLI `--files` ∪ `inputFiles` 计算 `moduleInputsHash`。**空输入被拒绝**,不得生成可复用空指纹。源码/command/profile/overlayPath 任一变化即 restart
106
108
  - **runtime overlay**:`-Dspring.config.additional-location=file:<ascii-abs-path>/application-harness-test.yml`;禁止默认 Edit tracked `application*.yml`
107
109
  - **known-good-test-profile**:profile 名、baseURL、healthUrl 写在 build-profile,非 skill 硬编码
@@ -131,12 +133,24 @@ disallowed-tools:
131
133
 
132
134
  ### 四、单元测试复用 + 写入 ledger
133
135
 
134
- Phase 1 前先读 `.harness/changes/<change-name>/evidence/verification-ledger.json`:run 的 unitTest 满足(diffHash 一致 + module/profile 一致 + scope 一致或更严格 + run 后无行为性修改 + run 跑了全量测试)则复用,否则按 **profile key resolve** 执行测试命令(`python <skills-root>/scripts/harness_profile.py resolve --project . --key unitTest|unitTestFull --json`,**不复制示例 `-pl` 命令**)。Phase 1/2 完成后必须写回 ledger:执行增量测试类 → 记 `unitTest`(scope=incremental);执行 profile 模块全量命令 → 记 `unitTestFull`(scope=module,可供 submit 复用);接口测试 → 记 `apiTest`。详见 `checklist.md`「单元测试复用」、`../protocols/ledger-protocol.md`。
136
+ Phase 1 前先读 `.harness/changes/<change-name>/evidence/verification-ledger.json`,并用 `harness_ledger.py diff-hash --repo . --base <baseCommit> --change-dir ".harness/changes/<change-name>" --json` 重算真实指纹:run 的 unitTest 满足(diffHash 一致 + module/profile 一致 + scope 一致或更严格 + run 后无行为性修改 + run 跑了全量测试)则复用,否则按 **profile key resolve** 执行测试命令(`python <skills-root>/scripts/harness_profile.py resolve --project . --key unitTest|unitTestFull --json`,**不复制示例 `-pl` 命令**)。Phase 1/2 完成后必须写回 ledger:执行增量测试类 → 记 `unitTest`(scope=incremental);执行 profile 模块全量命令 → 记 `unitTestFull`(scope=module,可供 submit 复用);接口测试 → 记 `apiTest`。详见 `checklist.md`「单元测试复用」、`../protocols/ledger-protocol.md`。
135
137
 
136
138
  ### 五、命令与请求超时治理
137
139
 
138
140
  所有命令必须有「预期时长 + 超时上限」,超过预期必须输出一次状态行,**不得静默等待**。`durationMs > 10000` → 🟡 SLOW,`> 30000` → ❌ TIMEOUT_RISK。详见 `reference.md`「命令与请求超时治理」。
139
141
 
142
+ ### 五-A、陈旧测试安全修复
143
+
144
+ 若测试编译或执行明确指向已移除/改名 API 的陈旧测试,且当前生产代码、已批准计划或可验证历史能唯一确定新契约,可仅修改测试并立即重跑该测试与目标测试,然后记录:
145
+
146
+ ```text
147
+ python <skills-root>/scripts/harness_test_guard.py record --project . --change-dir ".harness/changes/<change-name>" --files "<精确测试文件路径,逗号分隔>" --reason stale-test-repair --json
148
+ ```
149
+
150
+ 普通新增/更新测试使用 `tdd-created` / `test-updated`。存在业务歧义或修复会触及生产代码时,记录 `BLOCKED_PREEXISTING` 并停止复用该验证,不得猜测或绕过。
151
+
152
+ **禁止临时排除测试**:禁止 `.bak`/改名、移出测试目录、删除、禁用注解、构建 exclude、`skipTests`/`maven.test.skip` 充当测试通过证据;服务启动可在单元测试已独立通过后使用 `-Dmaven.test.skip=true` 避免重复编译测试,但不得据此声明测试通过。所有本轮新增、更新或安全修复且被忽略的测试必须写入 test-tracking manifest;仅执行、未修改的只读 ignored test 不获得 force-track 授权。
153
+
140
154
  ### 六、服务启动 + 生命周期管理
141
155
 
142
156
  启动等待状态机:0–30s 每 2s 探测、30–120s 每 5s 探测、>120s 读日志判定;遇启动失败特征立即停。**Service Gate**:`harness_service.py ensure` 返回 `action=needs-user-decision`(用户自启服务占端口)时 **才** AskUserQuestion;AI 托管服务或端口空闲则自动继续,不询问。服务指纹(`moduleInputsHash`,来自 CLI `--files` ∪ `serviceStart.inputFiles`)+ `startCommandHash` + `profile` + `overlayPath` + 进程身份任一变化即 restart;**空输入被拒绝**,不生成可复用空指纹。测试结束默认清理 AI 启动的服务。详见 `reference.md`「服务决策门」。
@@ -181,7 +195,7 @@ Phase 1 前先读 `.harness/changes/<change-name>/evidence/verification-ledger.j
181
195
 
182
196
  ## 执行日志
183
197
 
184
- `events.ndjson` 为唯一事实源(schema_version 2,`note` 承载人类可读摘要);`logs/execution-log.md` 由 `harness_events.py` 渲染,**禁止手工 Edit**。结构 → [[../protocols/report-pipeline-protocol.md|report-pipeline-protocol]]
198
+ `events.ndjson` 为唯一事实源(schema_version 2,`note` 承载人类可读摘要);`logs/execution-log.md` 由 `harness_events.py` 渲染,**禁止用 Write/Edit 直接维护**。直接修改的内容会在 `phase.end` 或 finalize 时被完整重建覆盖,属于数据丢失;需要保留的详情必须进入事件 `note`。结构 → [[../protocols/report-pipeline-protocol.md|report-pipeline-protocol]]
185
199
 
186
200
  ```powershell
187
201
  python <skills-root>/scripts/harness_events.py append --change-dir ".harness/changes/<change-name>" --phase <phase> --type phase.start --note "<触发指令>"
@@ -168,8 +168,6 @@ CONDITIONAL_OK
168
168
 
169
169
  ## 未提交测试证据
170
170
 
171
- 如果 run/test 使用了被 `.gitignore` 忽略或未提交的测试文件作为验证证据,必须:
171
+ run/test 本轮新增、更新或安全修复且被 `.gitignore` 忽略的测试,必须先由 `test-tracking.json` + `harness_test_guard.py stage` 精确提交;这类测试不得继续作为“未提交证据”收尾,也不得计入可复用 P0 验证后随 worktree 删除。
172
172
 
173
- 1. 归档到 `backups/uncommitted-tests/`;
174
- 2. 在 `summary-data.json.uncommittedTestEvidence[]` 中记录文件名、验证了什么、是否随 commit 提交;
175
- 3. final-summary 明确展示(`uncommittedTestEvidence` 非空时渲染"🧪 未提交测试证据"卡片),避免维护者误以为仓库包含该回归测试;无此类证据时不渲染该卡。
173
+ `uncommittedTestEvidence` 只兼容历史归档或明确只读的外部证据:归档到 `backups/uncommitted-tests/`,记录文件名、验证范围与未提交原因,并在 final-summary 展示风险。若它是本轮可修改的回归测试,则 archive 必须阻断并要求先走精确跟踪流程。
@@ -199,33 +199,25 @@ profile key 缺失 / glob 无匹配 / 结果为空 → `insufficient-evidence`
199
199
 
200
200
  ## 五、真实 diffHash
201
201
 
202
- 必须基于真实 diff 计算 SHA-256,且**必须是 commit-invariant 的**——覆盖本变更集相对 `baseCommit` 的全部内容变更(已提交 + 未提交 + 未跟踪),使 checkpoint commit 前后指纹一致,保证 run→test 复用链不断。
202
+ 必须基于真实内容变更集计算 SHA-256,且**必须是 commit-invariant 的**——覆盖本变更集相对 `baseCommit` tracked 变化、标准 untracked 文件,以及 `test-tracking.json` 明确记录的 ignored tests,使 checkpoint commit 前后指纹一致,保证 run→test→submit 复用链不断。
203
203
 
204
204
  `<baseCommit>` 从 ledger 读取(harness-plan 阶段写入);缺失时用 `git merge-base HEAD <默认分支>` 兜底。
205
205
 
206
206
  **canonical 命令(cluster 2 起脚本化,推荐)**:
207
207
 
208
208
  ```text
209
- harness_ledger.py diff-hash --repo <projectRoot> --base <baseCommit> --json
209
+ harness_ledger.py diff-hash --repo <projectRoot> --base <baseCommit> --change-dir ".harness/changes/<change-name>" --json
210
210
  ```
211
211
 
212
- `diff-hash` 用 subprocess 直接捕获 Git bytes,基于**内容变更集**计算 SHA-256:working tree 相对 base 的所有变化文件 + 未跟踪文件,按 repo-relative path 排序、长度 framing(`path-len | path | exists | content-len | content`),独立于 shell/控制台编码/BOM/系统换行,且 **commit-invariant**(checkpoint commit 不改 working tree → 指纹不变,UT-011)。算法版本 `content-changeset-1`,输出 `diffHash`/`algorithmVersion`/`fileCount`/`base`/`head`。
212
+ `diff-hash` 用 subprocess 直接捕获 Git bytes,按 repo-relative path 排序并进行长度 framing(`path-len | path | exists | content-len | content`),独立于 shell/控制台编码/BOM/系统换行。算法版本 `content-changeset-2`,输出 `diffHash`/`algorithmVersion`/`fileCount`/`base`/`head`/`trackedTestFileCount`/`testTrackingManifest`。
213
213
 
214
- > v1 PowerShell 拼接命令(下方)已废弃:依赖文本编码,且未跟踪文件提交后表示从 framed-content 切换为 diff-format 会破坏 commit-invariance。仅作历史参考。
215
-
216
- ```powershell
217
- powershell.exe -NoProfile -Command "$base = '<baseCommit>'; $patch = '.harness/changes/<change>/runtime/current-diff.patch'; & { git diff $base HEAD --binary; git diff --binary; git ls-files --others --exclude-standard | ForEach-Object { Get-Content -Raw -LiteralPath $_ } } | Out-File -Encoding utf8 $patch; (Get-FileHash $patch -Algorithm SHA256).Hash"
218
- ```
219
-
220
- - 命令为模板:`<baseCommit>`/`<change>` 需替换;经 `powershell.exe -Command "..."` 从 PowerShell/Bash 调用时外层会展开 `$` 变量,建议直接用 PowerShell 工具执行去外壳脚本,或外层用单引号/`--%` 防展开
221
- - 三部分合并:`git diff $base HEAD`(已提交变更)+ `git diff`(未提交 tracked)+ 未跟踪新文件内容(`git ls-files --others --exclude-standard`);`& { }` 必须带 `&` 才执行 scriptblock(bare `{ } | Out-File` 只写脚本块字符串,不执行)
222
- - commit 前:`$base..HEAD` 为空 + 未提交 24 文件 + 未跟踪 9 文件 = 全量;commit 后:`$base..HEAD` 24 文件 + 未提交空 + 未跟踪空 = 同内容 → 指纹一致 ✅
214
+ - `--change-dir` 可选以兼容普通调用;run/test/submit 对正式 change 必须传入。manifest 不存在时贡献 0 个额外路径。
215
+ - manifest 存在时必须严格验证 schema/mode/projectRoot、精确相对路径、文件存在性与 SHA-256;结构非法、路径越界或 hash 漂移均非零退出,旧 ledger 不得复用。
216
+ - ignored test 在 checkpoint 前由 manifest 加入内容集;经 guard force-stage/commit 后由 Git diff 加入同一路径,集合去重,因此 hash 保持不变。
223
217
  - diffHash 只用于"内容是否变化"判断,不等于 commit hash
224
218
  - 账本中 `currentHead`(验证时 HEAD)与 `diffHash` 分开记录;reuse 规则 #2 允许 HEAD 前移(如 harness-run Step 5 checkpoint commit),因 diffHash 已 commit-invariant,HEAD 前移不改变指纹
225
219
  - **禁止**使用类似 `3files-84plus-5minus` 的描述性字符串作为复用依据
226
- - **禁止**仅用 `git diff`(未提交)计算 diffHash——checkpoint commit 后工作树干净会致指纹变空,断裂 run→test 复用链
227
- - **禁止**任何单部分简化:含 `git diff <base> HEAD`(仅已提交部分)、`git diff`(仅未提交)、`node -e`/`crypto.createHash` 自算 SHA-256。无论 commit 前后、无论工作树是否 clean,必须用上述三部分合并命令。commit 后 clean 致单部分偶然等价不得作为省略依据——时序或工作树状态变化即复现复用链断裂
228
- - **commit 前时序说明**:harness-run 步骤 2c 在 Step 5 checkpoint commit **之前**写 ledger,此时 `git diff baseCommit..HEAD` 部分为空(HEAD==baseCommit),只有"未提交 + 未跟踪"是全量。**即使第一部分为空也必须保留三部分合并命令**——commit 后第一部分被填充、未提交/未跟踪变空,两者内容相同 → diffHash 一致。省略第一部分只用未提交 diff,commit 后未提交变空 → diffHash 变化 → 复用链断裂(真实教训:run 产出非规范 `8a94c874`,test 重算 `b4c580fc` 不一致被迫重跑全量单元测试)。harness-run 步骤 2c 已 inline 完整三部分命令,执行者直接跑 verbatim,不得自行省略
220
+ - **禁止**删除/忽略 test-tracking manifest 后继续复用,也禁止用手写 Git/Node/PowerShell hash 替代 canonical 命令。
229
221
 
230
222
  ## 六、Post-test 变更分类
231
223
 
@@ -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
@@ -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: