@hunter-harness/workflow-harness 0.2.16 → 0.2.17

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 (234) hide show
  1. package/harness/bundles/general/claude-code/.harness-build.json +1 -1
  2. package/harness/bundles/general/claude-code/CONTEXT.md +14 -1
  3. package/harness/bundles/general/claude-code/README.md +1 -1
  4. package/harness/bundles/general/claude-code/harness-archive/SKILL.md +7 -4
  5. package/harness/bundles/general/claude-code/harness-archive/reference.md +1 -1
  6. package/harness/bundles/general/claude-code/harness-codebase-map/SKILL.md +4 -2
  7. package/harness/bundles/general/claude-code/harness-knowledge-ingest/SKILL.md +4 -2
  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 +8 -4
  10. package/harness/bundles/general/claude-code/harness-plan/checklist.md +5 -5
  11. package/harness/bundles/general/claude-code/harness-review/SKILL.md +5 -2
  12. package/harness/bundles/general/claude-code/harness-run/SKILL.md +7 -3
  13. package/harness/bundles/general/claude-code/harness-run/checklist.md +4 -4
  14. package/harness/bundles/general/claude-code/harness-submit/SKILL.md +6 -3
  15. package/harness/bundles/general/claude-code/harness-submit/checklist.md +5 -5
  16. package/harness/bundles/general/claude-code/harness-submit/reference.md +3 -3
  17. package/harness/bundles/general/claude-code/harness-sync/SKILL.md +4 -2
  18. package/harness/bundles/general/claude-code/harness-sync/reference.md +1 -1
  19. package/harness/bundles/general/claude-code/harness-test/SKILL.md +7 -4
  20. package/harness/bundles/general/claude-code/harness-test/checklist.md +8 -5
  21. package/harness/bundles/general/claude-code/harness-test/pitfalls.md +1 -1
  22. package/harness/bundles/general/claude-code/harness-test/reference.md +28 -13
  23. package/harness/bundles/general/claude-code/protocols/submit-protocol.md +1 -1
  24. package/harness/bundles/general/claude-code/scripts/harness_deploy.py +7 -0
  25. package/harness/bundles/general/claude-code/scripts/harness_integration.py +26 -2
  26. package/harness/bundles/general/claude-code/scripts/harness_plan_finalize.py +159 -7
  27. package/harness/bundles/general/claude-code/scripts/harness_test_cleanup.py +175 -0
  28. package/harness/bundles/general/codebuddy/.harness-build.json +1 -1
  29. package/harness/bundles/general/codebuddy/CONTEXT.md +14 -1
  30. package/harness/bundles/general/codebuddy/README.md +1 -1
  31. package/harness/bundles/general/codebuddy/harness-archive/SKILL.md +7 -4
  32. package/harness/bundles/general/codebuddy/harness-archive/reference.md +1 -1
  33. package/harness/bundles/general/codebuddy/harness-codebase-map/SKILL.md +4 -2
  34. package/harness/bundles/general/codebuddy/harness-knowledge-ingest/SKILL.md +4 -2
  35. package/harness/bundles/general/codebuddy/harness-knowledge-query/SKILL.md +4 -2
  36. package/harness/bundles/general/codebuddy/harness-plan/SKILL.md +8 -4
  37. package/harness/bundles/general/codebuddy/harness-plan/checklist.md +5 -5
  38. package/harness/bundles/general/codebuddy/harness-review/SKILL.md +5 -2
  39. package/harness/bundles/general/codebuddy/harness-run/SKILL.md +7 -3
  40. package/harness/bundles/general/codebuddy/harness-run/checklist.md +4 -4
  41. package/harness/bundles/general/codebuddy/harness-submit/SKILL.md +6 -3
  42. package/harness/bundles/general/codebuddy/harness-submit/checklist.md +5 -5
  43. package/harness/bundles/general/codebuddy/harness-submit/reference.md +3 -3
  44. package/harness/bundles/general/codebuddy/harness-sync/SKILL.md +4 -2
  45. package/harness/bundles/general/codebuddy/harness-sync/reference.md +1 -1
  46. package/harness/bundles/general/codebuddy/harness-test/SKILL.md +7 -4
  47. package/harness/bundles/general/codebuddy/harness-test/checklist.md +8 -5
  48. package/harness/bundles/general/codebuddy/harness-test/pitfalls.md +1 -1
  49. package/harness/bundles/general/codebuddy/harness-test/reference.md +28 -13
  50. package/harness/bundles/general/codebuddy/protocols/submit-protocol.md +1 -1
  51. package/harness/bundles/general/codebuddy/scripts/harness_deploy.py +7 -0
  52. package/harness/bundles/general/codebuddy/scripts/harness_integration.py +26 -2
  53. package/harness/bundles/general/codebuddy/scripts/harness_plan_finalize.py +159 -7
  54. package/harness/bundles/general/codebuddy/scripts/harness_test_cleanup.py +175 -0
  55. package/harness/bundles/general/codex/.harness-build.json +1 -1
  56. package/harness/bundles/general/codex/CONTEXT.md +14 -1
  57. package/harness/bundles/general/codex/README.md +1 -1
  58. package/harness/bundles/general/codex/harness-archive/SKILL.md +7 -4
  59. package/harness/bundles/general/codex/harness-archive/reference.md +1 -1
  60. package/harness/bundles/general/codex/harness-codebase-map/SKILL.md +4 -2
  61. package/harness/bundles/general/codex/harness-knowledge-ingest/SKILL.md +4 -2
  62. package/harness/bundles/general/codex/harness-knowledge-query/SKILL.md +4 -2
  63. package/harness/bundles/general/codex/harness-plan/SKILL.md +8 -4
  64. package/harness/bundles/general/codex/harness-plan/checklist.md +5 -5
  65. package/harness/bundles/general/codex/harness-review/SKILL.md +5 -2
  66. package/harness/bundles/general/codex/harness-run/SKILL.md +7 -3
  67. package/harness/bundles/general/codex/harness-run/checklist.md +4 -4
  68. package/harness/bundles/general/codex/harness-submit/SKILL.md +6 -3
  69. package/harness/bundles/general/codex/harness-submit/checklist.md +5 -5
  70. package/harness/bundles/general/codex/harness-submit/reference.md +3 -3
  71. package/harness/bundles/general/codex/harness-sync/SKILL.md +4 -2
  72. package/harness/bundles/general/codex/harness-sync/reference.md +1 -1
  73. package/harness/bundles/general/codex/harness-test/SKILL.md +7 -4
  74. package/harness/bundles/general/codex/harness-test/checklist.md +8 -5
  75. package/harness/bundles/general/codex/harness-test/pitfalls.md +1 -1
  76. package/harness/bundles/general/codex/harness-test/reference.md +28 -13
  77. package/harness/bundles/general/codex/protocols/submit-protocol.md +1 -1
  78. package/harness/bundles/general/codex/scripts/harness_deploy.py +7 -0
  79. package/harness/bundles/general/codex/scripts/harness_integration.py +26 -2
  80. package/harness/bundles/general/codex/scripts/harness_plan_finalize.py +159 -7
  81. package/harness/bundles/general/codex/scripts/harness_test_cleanup.py +175 -0
  82. package/harness/bundles/general/cursor/.harness-build.json +1 -1
  83. package/harness/bundles/general/cursor/CONTEXT.md +14 -1
  84. package/harness/bundles/general/cursor/README.md +1 -1
  85. package/harness/bundles/general/cursor/harness-archive/SKILL.md +7 -4
  86. package/harness/bundles/general/cursor/harness-archive/reference.md +1 -1
  87. package/harness/bundles/general/cursor/harness-codebase-map/SKILL.md +4 -2
  88. package/harness/bundles/general/cursor/harness-knowledge-ingest/SKILL.md +4 -2
  89. package/harness/bundles/general/cursor/harness-knowledge-query/SKILL.md +4 -2
  90. package/harness/bundles/general/cursor/harness-plan/SKILL.md +8 -4
  91. package/harness/bundles/general/cursor/harness-plan/checklist.md +5 -5
  92. package/harness/bundles/general/cursor/harness-review/SKILL.md +5 -2
  93. package/harness/bundles/general/cursor/harness-run/SKILL.md +7 -3
  94. package/harness/bundles/general/cursor/harness-run/checklist.md +4 -4
  95. package/harness/bundles/general/cursor/harness-submit/SKILL.md +6 -3
  96. package/harness/bundles/general/cursor/harness-submit/checklist.md +5 -5
  97. package/harness/bundles/general/cursor/harness-submit/reference.md +3 -3
  98. package/harness/bundles/general/cursor/harness-sync/SKILL.md +4 -2
  99. package/harness/bundles/general/cursor/harness-sync/reference.md +1 -1
  100. package/harness/bundles/general/cursor/harness-test/SKILL.md +7 -4
  101. package/harness/bundles/general/cursor/harness-test/checklist.md +8 -5
  102. package/harness/bundles/general/cursor/harness-test/pitfalls.md +1 -1
  103. package/harness/bundles/general/cursor/harness-test/reference.md +28 -13
  104. package/harness/bundles/general/cursor/protocols/submit-protocol.md +1 -1
  105. package/harness/bundles/general/cursor/scripts/harness_deploy.py +7 -0
  106. package/harness/bundles/general/cursor/scripts/harness_integration.py +26 -2
  107. package/harness/bundles/general/cursor/scripts/harness_plan_finalize.py +159 -7
  108. package/harness/bundles/general/cursor/scripts/harness_test_cleanup.py +175 -0
  109. package/harness/bundles/java/claude-code/.harness-build.json +1 -1
  110. package/harness/bundles/java/claude-code/CONTEXT.md +14 -1
  111. package/harness/bundles/java/claude-code/README.md +1 -1
  112. package/harness/bundles/java/claude-code/harness-apidoc/SKILL.md +5 -2
  113. package/harness/bundles/java/claude-code/harness-archive/SKILL.md +7 -4
  114. package/harness/bundles/java/claude-code/harness-archive/reference.md +1 -1
  115. package/harness/bundles/java/claude-code/harness-codebase-map/SKILL.md +4 -2
  116. package/harness/bundles/java/claude-code/harness-knowledge-ingest/SKILL.md +4 -2
  117. package/harness/bundles/java/claude-code/harness-knowledge-query/SKILL.md +4 -2
  118. package/harness/bundles/java/claude-code/harness-package/SKILL.md +5 -2
  119. package/harness/bundles/java/claude-code/harness-package/checklist.md +4 -4
  120. package/harness/bundles/java/claude-code/harness-plan/SKILL.md +8 -4
  121. package/harness/bundles/java/claude-code/harness-plan/checklist.md +5 -5
  122. package/harness/bundles/java/claude-code/harness-review/SKILL.md +5 -2
  123. package/harness/bundles/java/claude-code/harness-run/SKILL.md +7 -3
  124. package/harness/bundles/java/claude-code/harness-run/checklist.md +4 -4
  125. package/harness/bundles/java/claude-code/harness-submit/SKILL.md +6 -3
  126. package/harness/bundles/java/claude-code/harness-submit/checklist.md +5 -5
  127. package/harness/bundles/java/claude-code/harness-submit/reference.md +3 -3
  128. package/harness/bundles/java/claude-code/harness-sync/SKILL.md +4 -2
  129. package/harness/bundles/java/claude-code/harness-sync/reference.md +1 -1
  130. package/harness/bundles/java/claude-code/harness-test/SKILL.md +8 -5
  131. package/harness/bundles/java/claude-code/harness-test/pitfalls.md +1 -1
  132. package/harness/bundles/java/claude-code/harness-test/reference.md +1 -1
  133. package/harness/bundles/java/claude-code/protocols/submit-protocol.md +1 -1
  134. package/harness/bundles/java/claude-code/scripts/harness_deploy.py +7 -0
  135. package/harness/bundles/java/claude-code/scripts/harness_integration.py +26 -2
  136. package/harness/bundles/java/claude-code/scripts/harness_plan_finalize.py +159 -7
  137. package/harness/bundles/java/claude-code/scripts/harness_test_cleanup.py +175 -0
  138. package/harness/bundles/java/codebuddy/.harness-build.json +1 -1
  139. package/harness/bundles/java/codebuddy/CONTEXT.md +14 -1
  140. package/harness/bundles/java/codebuddy/README.md +1 -1
  141. package/harness/bundles/java/codebuddy/harness-apidoc/SKILL.md +5 -2
  142. package/harness/bundles/java/codebuddy/harness-archive/SKILL.md +7 -4
  143. package/harness/bundles/java/codebuddy/harness-archive/reference.md +1 -1
  144. package/harness/bundles/java/codebuddy/harness-codebase-map/SKILL.md +4 -2
  145. package/harness/bundles/java/codebuddy/harness-knowledge-ingest/SKILL.md +4 -2
  146. package/harness/bundles/java/codebuddy/harness-knowledge-query/SKILL.md +4 -2
  147. package/harness/bundles/java/codebuddy/harness-package/SKILL.md +5 -2
  148. package/harness/bundles/java/codebuddy/harness-package/checklist.md +4 -4
  149. package/harness/bundles/java/codebuddy/harness-plan/SKILL.md +8 -4
  150. package/harness/bundles/java/codebuddy/harness-plan/checklist.md +5 -5
  151. package/harness/bundles/java/codebuddy/harness-review/SKILL.md +5 -2
  152. package/harness/bundles/java/codebuddy/harness-run/SKILL.md +7 -3
  153. package/harness/bundles/java/codebuddy/harness-run/checklist.md +4 -4
  154. package/harness/bundles/java/codebuddy/harness-submit/SKILL.md +6 -3
  155. package/harness/bundles/java/codebuddy/harness-submit/checklist.md +5 -5
  156. package/harness/bundles/java/codebuddy/harness-submit/reference.md +3 -3
  157. package/harness/bundles/java/codebuddy/harness-sync/SKILL.md +4 -2
  158. package/harness/bundles/java/codebuddy/harness-sync/reference.md +1 -1
  159. package/harness/bundles/java/codebuddy/harness-test/SKILL.md +8 -5
  160. package/harness/bundles/java/codebuddy/harness-test/pitfalls.md +1 -1
  161. package/harness/bundles/java/codebuddy/harness-test/reference.md +1 -1
  162. package/harness/bundles/java/codebuddy/protocols/submit-protocol.md +1 -1
  163. package/harness/bundles/java/codebuddy/scripts/harness_deploy.py +7 -0
  164. package/harness/bundles/java/codebuddy/scripts/harness_integration.py +26 -2
  165. package/harness/bundles/java/codebuddy/scripts/harness_plan_finalize.py +159 -7
  166. package/harness/bundles/java/codebuddy/scripts/harness_test_cleanup.py +175 -0
  167. package/harness/bundles/java/codex/.harness-build.json +1 -1
  168. package/harness/bundles/java/codex/CONTEXT.md +14 -1
  169. package/harness/bundles/java/codex/README.md +1 -1
  170. package/harness/bundles/java/codex/harness-apidoc/SKILL.md +5 -2
  171. package/harness/bundles/java/codex/harness-archive/SKILL.md +7 -4
  172. package/harness/bundles/java/codex/harness-archive/reference.md +1 -1
  173. package/harness/bundles/java/codex/harness-codebase-map/SKILL.md +4 -2
  174. package/harness/bundles/java/codex/harness-knowledge-ingest/SKILL.md +4 -2
  175. package/harness/bundles/java/codex/harness-knowledge-query/SKILL.md +4 -2
  176. package/harness/bundles/java/codex/harness-package/SKILL.md +5 -2
  177. package/harness/bundles/java/codex/harness-package/checklist.md +4 -4
  178. package/harness/bundles/java/codex/harness-plan/SKILL.md +8 -4
  179. package/harness/bundles/java/codex/harness-plan/checklist.md +5 -5
  180. package/harness/bundles/java/codex/harness-review/SKILL.md +5 -2
  181. package/harness/bundles/java/codex/harness-run/SKILL.md +7 -3
  182. package/harness/bundles/java/codex/harness-run/checklist.md +4 -4
  183. package/harness/bundles/java/codex/harness-submit/SKILL.md +6 -3
  184. package/harness/bundles/java/codex/harness-submit/checklist.md +5 -5
  185. package/harness/bundles/java/codex/harness-submit/reference.md +3 -3
  186. package/harness/bundles/java/codex/harness-sync/SKILL.md +4 -2
  187. package/harness/bundles/java/codex/harness-sync/reference.md +1 -1
  188. package/harness/bundles/java/codex/harness-test/SKILL.md +8 -5
  189. package/harness/bundles/java/codex/harness-test/pitfalls.md +1 -1
  190. package/harness/bundles/java/codex/harness-test/reference.md +1 -1
  191. package/harness/bundles/java/codex/protocols/submit-protocol.md +1 -1
  192. package/harness/bundles/java/codex/scripts/harness_deploy.py +7 -0
  193. package/harness/bundles/java/codex/scripts/harness_integration.py +26 -2
  194. package/harness/bundles/java/codex/scripts/harness_plan_finalize.py +159 -7
  195. package/harness/bundles/java/codex/scripts/harness_test_cleanup.py +175 -0
  196. package/harness/bundles/java/cursor/.harness-build.json +1 -1
  197. package/harness/bundles/java/cursor/CONTEXT.md +14 -1
  198. package/harness/bundles/java/cursor/README.md +1 -1
  199. package/harness/bundles/java/cursor/harness-apidoc/SKILL.md +5 -2
  200. package/harness/bundles/java/cursor/harness-archive/SKILL.md +7 -4
  201. package/harness/bundles/java/cursor/harness-archive/reference.md +1 -1
  202. package/harness/bundles/java/cursor/harness-codebase-map/SKILL.md +4 -2
  203. package/harness/bundles/java/cursor/harness-knowledge-ingest/SKILL.md +4 -2
  204. package/harness/bundles/java/cursor/harness-knowledge-query/SKILL.md +4 -2
  205. package/harness/bundles/java/cursor/harness-package/SKILL.md +5 -2
  206. package/harness/bundles/java/cursor/harness-package/checklist.md +4 -4
  207. package/harness/bundles/java/cursor/harness-plan/SKILL.md +8 -4
  208. package/harness/bundles/java/cursor/harness-plan/checklist.md +5 -5
  209. package/harness/bundles/java/cursor/harness-review/SKILL.md +5 -2
  210. package/harness/bundles/java/cursor/harness-run/SKILL.md +7 -3
  211. package/harness/bundles/java/cursor/harness-run/checklist.md +4 -4
  212. package/harness/bundles/java/cursor/harness-submit/SKILL.md +6 -3
  213. package/harness/bundles/java/cursor/harness-submit/checklist.md +5 -5
  214. package/harness/bundles/java/cursor/harness-submit/reference.md +3 -3
  215. package/harness/bundles/java/cursor/harness-sync/SKILL.md +4 -2
  216. package/harness/bundles/java/cursor/harness-sync/reference.md +1 -1
  217. package/harness/bundles/java/cursor/harness-test/SKILL.md +8 -5
  218. package/harness/bundles/java/cursor/harness-test/pitfalls.md +1 -1
  219. package/harness/bundles/java/cursor/harness-test/reference.md +1 -1
  220. package/harness/bundles/java/cursor/protocols/submit-protocol.md +1 -1
  221. package/harness/bundles/java/cursor/scripts/harness_deploy.py +7 -0
  222. package/harness/bundles/java/cursor/scripts/harness_integration.py +26 -2
  223. package/harness/bundles/java/cursor/scripts/harness_plan_finalize.py +159 -7
  224. package/harness/bundles/java/cursor/scripts/harness_test_cleanup.py +175 -0
  225. package/harness/manifests/general/claude-code.json +31 -27
  226. package/harness/manifests/general/codebuddy.json +31 -27
  227. package/harness/manifests/general/codex.json +31 -27
  228. package/harness/manifests/general/cursor.json +31 -27
  229. package/harness/manifests/java/claude-code.json +33 -29
  230. package/harness/manifests/java/codebuddy.json +33 -29
  231. package/harness/manifests/java/codex.json +33 -29
  232. package/harness/manifests/java/cursor.json +33 -29
  233. package/hunter-workflow-family.json +2 -2
  234. package/package.json +1 -1
@@ -2,7 +2,7 @@
2
2
  name: harness-test
3
3
  description: 测试执行:读取场景表,执行单元测试+API接口测试+数据兼容验证,输出测试报告。当用户说'跑测试/验证/跑用例/接口测试/单元测试'时使用
4
4
  ---
5
- <!-- generated by harness_deploy.py; core=10114ac730b539b5; overlay=none; agent=cursor; do not edit -->
5
+ <!-- generated by harness_deploy.py; core=d2eb65d744fddce3; overlay=none; agent=cursor; do not edit -->
6
6
  # harness-test — 测试执行
7
7
 
8
8
  ## Purpose
@@ -39,6 +39,7 @@ description: 测试执行:读取场景表,执行单元测试+API接口测试
39
39
  9. **每个 skill 边界只刷新一次状态快照** — 先运行 `python <skills-root>/scripts/harness_state.py capture --project . --change-dir ".harness/changes/<change-name>" --base <baseCommit> --json`;`changedSegments=[]` 时复用已有 profile/rules/map/knowledge/change/code 指纹,不再重复跑全量 `harness-sync`、知识重建或代码库扫描。只有对应 segment 变化或缺失时才定向刷新;快照不能替代源码读取和验证门禁。
40
40
 
41
41
  状态目录分层:新路径优先,旧路径兼容 → [[../protocols/state-layout-protocol.md|state-layout-protocol]]
42
+ > 片段:read-protocol
42
43
 
43
44
  ## Workflow
44
45
 
@@ -87,6 +88,7 @@ description: 测试执行:读取场景表,执行单元测试+API接口测试
87
88
  - 命令结果不得靠猜测;普通 Bash 被拒 → 立即改用等价 PowerShell 重试一次
88
89
  - 仅 PowerShell 成功且有明确证据(构建/git/测试输出、文件存在、exit 0)时可标 ✅OK;否则 ❌FAIL 或 🟡WARN
89
90
  - 禁止把 hook 拒绝、静态验证、无输出、用户跳过说成成功 → 详见 [[../protocols/powershell-protocol.md|powershell-protocol]]、[[../protocols/evidence-based-reporting-protocol.md|evidence-based-reporting-protocol]]
91
+ > 片段:p0-trust
90
92
 
91
93
  ## 关键规则(硬门禁速查)
92
94
 
@@ -126,11 +128,11 @@ python <skills-root>/scripts/harness_test_guard.py record --project . --change-d
126
128
 
127
129
  ### 六、服务启动 + 生命周期管理
128
130
 
129
- 启动等待状态机: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`「服务决策门」。
131
+ 启动等待状态机:0–30s 每 2s 探测、30–120s 每 5s 探测、>120s 读日志判定;遇启动失败特征立即停。**Service Gate**:`harness_service.py ensure` 返回 `action=needs-user-decision`(用户自启服务占端口)时 **才** blocking user confirmation;AI 托管服务或端口空闲则自动继续,不询问。服务指纹(`moduleInputsHash`,来自 CLI `--files` ∪ `serviceStart.inputFiles`)+ `startCommandHash` + `profile` + `overlayPath` + 进程身份任一变化即 restart;**空输入被拒绝**,不生成可复用空指纹。测试结束默认清理 AI 启动的服务。详见 `reference.md`「服务决策门」。
130
132
 
131
133
  ### 七、运行时配置叠加(不动 tracked 配置)
132
134
 
133
- 禁止默认 Edit tracked 应用配置文件。默认运行时配置叠加(ASCII 绝对路径);改 tracked 配置 → 默认拒绝,记 `decision` 事件(不 AskUserQuestion,报告 🟡 WARN)。详见 `reference.md`。
135
+ 禁止默认 Edit tracked 应用配置文件。默认运行时配置叠加(ASCII 绝对路径);改 tracked 配置 → 默认拒绝,记 `decision` 事件(不 blocking user confirmation,报告 🟡 WARN)。详见 `reference.md`。
134
136
 
135
137
  ### 八、Token 缓存与复用
136
138
 
@@ -162,7 +164,7 @@ python <skills-root>/scripts/harness_test_guard.py record --project . --change-d
162
164
 
163
165
  ## 交互白名单
164
166
 
165
- 本 skill **仅允许**以下 AskUserQuestion;其余默认值 + `decision` 事件:
167
+ 本 skill **仅允许**以下 blocking user confirmation;其余默认值 + `decision` 事件:
166
168
 
167
169
  1. **Service Gate**:仅当 `harness_service.py ensure` 返回 `needs-user-decision`(用户进程占端口)时询问处理方式
168
170
 
@@ -179,3 +181,4 @@ python <skills-root>/scripts/harness_events.py append --change-dir ".harness/cha
179
181
  > **Task 4 §6.1 写入契约**:普通 `append` = 加锁 -> 追加一行 -> fsync -> 解锁,**不 load 历史、不渲染**(O(1),跨进程锁 `events.ndjson.lock`,UUID 用完整 `uuid4().hex` 无需去重扫描)。仅 `--type phase.end` append 在追加成功后渲染一次 `execution-log.md`;显式 `harness_events.py render` 随时从完整 events 重建;`harness_archive.py finalize` 在 collect 前强制 render 一次。高频 command append 期间 log 可能滞后,phase 边界保持最新。
180
182
 
181
183
  每次阶段结束必须写 `--status OK|WARN|FAIL|BLOCKED`;重试同一阶段时增加 `--attempt <n>`,不得覆盖或伪装成一次执行。跨工具继续执行时写 `--executor-tool <codex|claude-code|codebuddy|cursor>`,并在接棒事件写 `--handoff-from-tool` / `--handoff-reason`;也可由 `HUNTER_HARNESS_TOOL/AGENT/MODEL/RUN_ID` 环境变量统一注入。
184
+ > 片段:logging · phase=`test` · 事件:phase/command/verification/decision/issue/artifact
@@ -276,7 +276,7 @@ powershell.exe -NoProfile -Command "try { (Invoke-WebRequest -Uri 'http://127.0.
276
276
  - [ ] **只有首选执行器不可执行才降级**:PowerShell batch (.ps1) → 多次 MCP `browser_evaluate` → curl
277
277
  - [ ] **如果执行器在 PowerShell 可用,禁止使用 Playwright MCP 逐条执行接口测试**
278
278
  - [ ] 执行器直连本地 baseURL,复用认证凭证,不依赖浏览器 origin
279
- - [ ] 记录每个请求的 durationMs、status、code、message、assertionResult
279
+ - [ ] 记录每个请求的 requestDurationMs、status、code、message、assertionResult
280
280
  - [ ] 支持失败时继续执行后续非依赖场景
281
281
  - [ ] 报告区分四种执行器:**接口测试执行器 / PowerShell batch / Playwright MCP browser_evaluate / curl**(不得与"Playwright"混写)
282
282
 
@@ -287,11 +287,14 @@ powershell.exe -NoProfile -Command "try { (Invoke-WebRequest -Uri 'http://127.0.
287
287
  - [ ] 测试结束后清理可清理的数据
288
288
  - [ ] 不能清理的记录遗留数据和原因
289
289
 
290
- ### 请求耗时
290
+ ### 请求耗时(§5.22 分层耗时)
291
291
 
292
- - [ ] 每个请求记录 durationMs
293
- - [ ] durationMs > 10000 → 🟡SLOW,说明原因
294
- - [ ] durationMs > 30000❌TIMEOUT_RISK,说明原因
292
+ - [ ] 每个请求记录 requestDurationMs(单 HTTP 请求耗时)
293
+ - [ ] 整个批次记录 batchDurationMs(runner wall-clock)
294
+ - [ ] requestDurationMs > 10000🟡SLOW,说明原因
295
+ - [ ] requestDurationMs > 30000 → ❌TIMEOUT_RISK,说明原因
296
+ - [ ] batchDurationMs > 180000 → 🟡BATCH_SLOW
297
+ - [ ] 聚合合同套件场景只记录 batch reference + coveredTests,**禁止均摊生成伪 requestDurationMs**
295
298
 
296
299
  ### 覆盖标注诚实性(✅ 实测 vs 🟡 推断)
297
300
 
@@ -191,7 +191,7 @@ $cred = $resp.data.accessToken
191
191
  **严重度**:🟡WARN
192
192
  **场景**:测试期间为了切换外部服务配置直接 Edit tracked 配置文件(如 `application-local-dev.yml`),测试后再还原,留下 diff 噪声
193
193
  **后果**:git diff --stat 出现意外文件、提交风险、误以为有业务变更
194
- **正确做法**:生成运行时配置叠加 `.harness/changes/<change>/runtime/application-harness-test.yml`(不提交),启动用 `-Dspring.config.additional-location=file:...` 叠加(按技术栈);如必须改 tracked 配置,先 AskUserQuestion,最终报告至少 🟡 WARN
194
+ **正确做法**:生成运行时配置叠加 `.harness/changes/<change>/runtime/application-harness-test.yml`(不提交),启动用 `-Dspring.config.additional-location=file:...` 叠加(按技术栈);如必须改 tracked 配置,先 blocking user confirmation,最终报告至少 🟡 WARN
195
195
 
196
196
  ### 规则 22:唯一约束字段与本地预存数据冲突
197
197
  **严重度**:❌FAIL
@@ -150,7 +150,7 @@ spring:
150
150
  powershell.exe -NoProfile -Command "mvn spring-boot:run -pl <module> -Dspring-boot.run.profiles=<profile> -Dspring-boot.run.jvmArguments='-Dspring.config.additional-location=file:C:/temp/harness-test-overlay/<change-name>/application-harness-test.yml'"
151
151
  ```
152
152
 
153
- 不得默认使用 `.harness/changes/<change>/runtime/application-harness-test.yml` 相对路径作为 JVM `additional-location`。如必须修改 tracked 配置文件,先 AskUserQuestion,最终报告至少 🟡 WARN。
153
+ 不得默认使用 `.harness/changes/<change>/runtime/application-harness-test.yml` 相对路径作为 JVM `additional-location`。如必须修改 tracked 配置文件,先 blocking user confirmation,最终报告至少 🟡 WARN。
154
154
 
155
155
  ## 服务决策门(Service Decision Gate)与服务生命周期管理
156
156
 
@@ -361,11 +361,13 @@ async function cleanup(token) {
361
361
 
362
362
  // ===== 主流程 =====
363
363
  const token = await resolveCredential('admin');
364
+ const batchStart = Date.now();
364
365
  await setup(token);
365
366
  for (const scenario of scenarios) {
366
367
  await runOne(scenario, token);
367
368
  }
368
369
  const cleanupResult = await cleanup(token);
370
+ const batchDurationMs = Date.now() - batchStart;
369
371
 
370
372
  const summary = {
371
373
  runner: 'api-test-executor',
@@ -377,7 +379,11 @@ const summary = {
377
379
  setupErrors: setupState.errors,
378
380
  cleanupResult,
379
381
  credentialRefreshCount,
380
- startedAt: '<ISO>', finishedAt: '<ISO>', durationMs: 0
382
+ // §5.22: 分层耗时 schema batchDurationMs 是整个 runner wall-clock;
383
+ // 每个场景的 requestDurationMs 是单 HTTP 请求耗时。聚合合同套件场景
384
+ // 只记录 batch reference/coveredTests,不允许均摊生成伪请求耗时。
385
+ batchDurationMs,
386
+ startedAt: '<ISO>', finishedAt: '<ISO>'
381
387
  };
382
388
  writeFileSync(RESULTS_FILE, JSON.stringify({ results, summary }, null, 2));
383
389
  console.log(`\n结果已写入 ${RESULTS_FILE}`);
@@ -389,6 +395,7 @@ console.log(`\n结果已写入 ${RESULTS_FILE}`);
389
395
  > - 凭证从本地 cache 读 + 本地轻量接口验证,**完全不依赖浏览器当前页面 origin**
390
396
  > - 同一次执行内 `credentialRefreshCount` 不得 > 1
391
397
  > - payload 从数据契约/接口定义/真实样例生成,并在脚本注释中标注字段来源
398
+ > - **§5.22 分层耗时**:`batchDurationMs` 是 runner wall-clock(整个批次耗时),`requestDurationMs` 是单 HTTP 请求耗时。聚合合同套件场景(如 API-C01~C04 共享同一 pytest 批次)只记录 `batchReference` + `coveredTests`,**禁止用 `batchDurationMs / N` 均摊生成伪 `requestDurationMs`**
392
399
 
393
400
  ### 执行器降级方案(PowerShell batch runner .ps1)
394
401
 
@@ -445,18 +452,20 @@ $results | ConvertTo-Json -Depth 4
445
452
  | 接口测试执行器(普通) | 30s | **60s** | 抛错继续报告,不再重试 |
446
453
  | 接口测试执行器(复杂场景) | 60s | **120s** | 同上 |
447
454
  | 凭证获取(认证服务) | 5–10s | **15s** | 计 1 次重取;超过 1 次 → 🟡 WARN |
448
- | 单个 HTTP GET | < 1s | **8s** | durationMs > 10s → 🟡 SLOW |
449
- | 单个 HTTP POST/PUT | < 2s | **10s** | durationMs > 10s → 🟡 SLOW |
455
+ | 单个 HTTP GET | < 1s | **8s** | requestDurationMs > 10s → 🟡 SLOW |
456
+ | 单个 HTTP POST/PUT | < 2s | **10s** | requestDurationMs > 10s → 🟡 SLOW |
450
457
  | health probe | < 1s | **3s** | 计入启动状态机 |
451
458
  | 构建工具编译(如 Maven compile) | 10–60s | **180s** | 输出最后 30 行日志后停 |
452
459
  | 测试命令(如 Maven test) | 60–180s | **300s** | 输出失败用例后停 |
453
460
  | service start | 30–60s | **120s** | 4.1 启动状态机收敛 |
454
461
 
455
- **反馈规则**:
462
+ **§5.22 分层耗时与反馈规则**:
456
463
  - 等待 > 10s 必须输出一次状态行(启动等待、执行器、构建工具均适用)
457
464
  - 不得静默等待
458
- - `durationMs > 10000` → 🟡 SLOW
459
- - `durationMs > 30000` TIMEOUT_RISK
465
+ - **`requestDurationMs`**(单 HTTP 请求耗时):`> 10000` → 🟡 SLOW;`> 30000` → ❌ TIMEOUT_RISK
466
+ - **`batchDurationMs`**(runner wall-clock,整个批次耗时):`> 180000`(3 分钟)→ 🟡 BATCH_SLOW;`> 300000`(5 分钟)→ BATCH_TIMEOUT_RISK
467
+ - **聚合合同套件场景**(如 API-C01~C04 共享同一 pytest 批次):只记录 `batchReference` + `coveredTests`,**禁止用 `batchDurationMs / N` 均摊生成伪 `requestDurationMs`**
468
+ - 报告必须明确标注是哪一层越界(batch-level 还是 request-level),不得混用
460
469
  - 请求慢不得忽略,必须说明是服务、网络、认证还是工具调用开销
461
470
 
462
471
  ## 认证凭证缓存与复用
@@ -638,17 +647,23 @@ TEST_<change-name>_<timestamp>_<short-random>
638
647
  | cleanup | cleaned=X / leftover=Y | 接口 /<resource> 不支持删除,2 条遗留 |
639
648
 
640
649
  ### 接口测试
641
- | # | 场景 | 方法 | URL | 预期 | 实际 | durationMs | 状态 |
650
+ | # | 场景 | 方法 | URL | 预期 | 实际 | requestDurationMs | 状态 |
642
651
  |:--:|------|:----:|-----|------|------|----------:|:--:|
643
652
  | API-001 | 创建资源 | POST | /api/xxx | 200, code=0 | 200, code=0 | 245 | ✅ PASS |
644
653
  | API-002 | 参数校验 | POST | /api/xxx | code=xxx | code=xxx | 180 | ✅ PASS |
645
654
  | API-003 | 查询子规则 | GET | /api/xxx/{id} | 200 | — | — | 🟡 BLOCKED(setup.createRule 失败) |
646
655
 
647
- ### 请求耗时统计
648
- | 场景 | 方法 | URL | durationMs | 状态 |
649
- |------|:----:|-----|----------:|:----:|
650
- | API-001 | POST | /api/xxx | 245 | ✅ |
651
- | API-010 | GET | /api/yyy | 12450 | 🟡SLOW |
656
+ ### 请求耗时统计(§5.22 分层耗时)
657
+ | 层级 | 场景/批次 | 方法 | URL | 耗时 | 状态 |
658
+ |------|----------|:----:|-----|----:|:----:|
659
+ | request | API-001 | POST | /api/xxx | 245ms | ✅ |
660
+ | request | API-010 | GET | /api/yyy | 12450ms | 🟡SLOW |
661
+ | batch | pytest-batch-1 | — | — | 158000ms | 🟡BATCH_SLOW |
662
+
663
+ > **§5.22 分层耗时说明**:
664
+ > - `request` 层级记录单 HTTP 请求耗时(`requestDurationMs`)
665
+ > - `batch` 层级记录 runner wall-clock(`batchDurationMs`),整个 pytest 批次耗时
666
+ > - 聚合合同套件场景(如 API-C01~C04 共享同一批次)只记录一行 `batch` 条目 + `coveredTests` 列表,**不得均摊为 4 个伪 `request` 条目**
652
667
 
653
668
  ### 数据兼容
654
669
  | # | 场景 | 预期 | 实际 | 结果 |
@@ -1,5 +1,5 @@
1
1
  ---
2
- description: harness-submit 的确定性提交协议。用于避免 AskUserQuestion 参数错误、中文 commit message 失败、Co-Authored-By 违规和先 commit 后 amend。
2
+ description: harness-submit 的确定性提交协议。用于避免 blocking user confirmation 参数错误、中文 commit message 失败、Co-Authored-By 违规和先 commit 后 amend。
3
3
  ---
4
4
 
5
5
  # Submit Protocol
@@ -26,6 +26,10 @@ if hasattr(sys.stderr, "reconfigure"):
26
26
  sys.stderr.reconfigure(encoding="utf-8")
27
27
 
28
28
  INCLUDE_RE = re.compile(r"<!--\s*@include\s+shared/([^\s]+)\s*-->")
29
+ # retro §5.17: wiki links [[shared/xxx.md|alias]] reference shared fragments
30
+ # that are inlined by expand_includes. After expansion, replace the wiki link
31
+ # with the alias text (the shared content is already in the document).
32
+ WIKI_LINK_RE = re.compile(r"\[\[shared/([^\]|]+)\|([^\]]*)\]\]")
29
33
  # §7.5: overlay anchors support both new section-id and legacy section:"title".
30
34
  OVERRIDE_RE = re.compile(r'<!--\s*@override\s+(?:section-id:"([^"]+)"|section:"([^"]+)")\s*-->')
31
35
  APPEND_RE = re.compile(r'<!--\s*@append-after\s+(?:section-id:"([^"]+)"|section:"([^"]+)")\s*-->')
@@ -148,6 +152,9 @@ def expand_includes(text: str, shared_dir: Path) -> str:
148
152
  return body
149
153
 
150
154
  expanded = INCLUDE_RE.sub(repl, text)
155
+ # §5.17: replace [[shared/xxx.md|alias]] wiki links with the alias text,
156
+ # since the shared content has been inlined by @include expansion above.
157
+ expanded = WIKI_LINK_RE.sub(lambda m: m.group(2), expanded)
151
158
  lines = [ln for ln in expanded.splitlines() if not FRAGMENT_HINT_RE.match(ln)]
152
159
  return "\n".join(lines) + ("\n" if text.endswith("\n") else "")
153
160
 
@@ -1049,10 +1049,11 @@ class IntegrationTransaction:
1049
1049
  heavy_removed.append(name)
1050
1050
  except OSError as exc:
1051
1051
  heavy_failures.append({"path": name, "error": str(exc)})
1052
- self.runner.run(
1052
+ remove_result = self.runner.run(
1053
1053
  self.project_root, "worktree", "remove", "--force", str(resolved),
1054
1054
  check=False,
1055
1055
  )
1056
+ remove_returncode = getattr(remove_result, "returncode", 0)
1056
1057
  listed = self.runner.text(self.project_root, "worktree", "list", "--porcelain")
1057
1058
  registered_after = {
1058
1059
  str(Path(line.removeprefix("worktree ")).resolve())
@@ -1061,7 +1062,8 @@ class IntegrationTransaction:
1061
1062
  }
1062
1063
  still_registered = str(resolved) in registered_after
1063
1064
  residual_paths: list[str] = []
1064
- if resolved.exists():
1065
+ disk_path_present = resolved.exists()
1066
+ if disk_path_present:
1065
1067
  if resolved.is_file():
1066
1068
  residual_paths.append(resolved.name)
1067
1069
  else:
@@ -1075,6 +1077,27 @@ class IntegrationTransaction:
1075
1077
  break
1076
1078
  if len(residual_paths) >= 200:
1077
1079
  break
1080
+ # §5.30: Windows half-success — git worktree remove returned non-zero
1081
+ # but registration is already deleted and disk path remains. Return a
1082
+ # structured status distinct from generic CLEANUP_RESIDUAL so callers
1083
+ # can apply the allowlisted residual cleaner.
1084
+ if (
1085
+ remove_returncode != 0
1086
+ and not still_registered
1087
+ and disk_path_present
1088
+ and residual_paths
1089
+ and not heavy_failures
1090
+ ):
1091
+ return {
1092
+ "ok": False,
1093
+ "code": "REGISTRATION_REMOVED_RESIDUAL_PRESENT",
1094
+ "target": str(resolved),
1095
+ "heavyRootsRemoved": heavy_removed,
1096
+ "heavyRootFailures": heavy_failures,
1097
+ "worktreeRegistered": False,
1098
+ "diskPathPresent": True,
1099
+ "residualPaths": residual_paths,
1100
+ }
1078
1101
  ok = not heavy_failures and not still_registered and not residual_paths
1079
1102
  return {
1080
1103
  "ok": ok,
@@ -1083,6 +1106,7 @@ class IntegrationTransaction:
1083
1106
  "heavyRootsRemoved": heavy_removed,
1084
1107
  "heavyRootFailures": heavy_failures,
1085
1108
  "worktreeRegistered": still_registered,
1109
+ "diskPathPresent": disk_path_present,
1086
1110
  "residualPaths": residual_paths,
1087
1111
  }
1088
1112
 
@@ -300,6 +300,152 @@ def _terminal_exists(change_dir: Path, run_id: str, attempt: int) -> bool:
300
300
  )
301
301
 
302
302
 
303
+ def verify_plan(change_dir: Path) -> dict[str, Any]:
304
+ """Read-only verification of a finalized plan (retro §5.8).
305
+
306
+ Validates the published artifacts, receipt and events stream without
307
+ requiring the original staging directory. Returns the artifact hash,
308
+ phase.end count/status, frontmatter, and consistency checks. Any parse
309
+ error or inconsistency results in ok=false with a non-zero exit code.
310
+ """
311
+ change_dir = change_dir.resolve()
312
+ receipt_path = change_dir / "meta" / "plan-finalization.json"
313
+ if not receipt_path.is_file():
314
+ return _result_error("RECEIPT_MISSING", f"receipt not found: {receipt_path}")
315
+
316
+ try:
317
+ receipt = json.loads(receipt_path.read_text(encoding="utf-8-sig"))
318
+ except (OSError, json.JSONDecodeError) as exc:
319
+ return _result_error("RECEIPT_INVALID", str(exc))
320
+ if not isinstance(receipt, dict):
321
+ return _result_error("RECEIPT_INVALID", "receipt top-level must be an object")
322
+
323
+ change_name = str(receipt.get("changeName") or "").strip()
324
+ if not change_name:
325
+ return _result_error("RECEIPT_INVALID", "receipt missing changeName")
326
+
327
+ expected_hash = str(receipt.get("artifactsHash") or "").strip()
328
+ if not expected_hash.startswith("sha256:"):
329
+ return _result_error("RECEIPT_INVALID", "receipt artifactsHash missing or malformed")
330
+
331
+ files_list = receipt.get("files")
332
+ if not isinstance(files_list, list) or not files_list:
333
+ return _result_error("RECEIPT_INVALID", "receipt files list missing or empty")
334
+
335
+ # Recompute artifacts hash from published files.
336
+ digest = hashlib.sha256()
337
+ artifact_names: list[str] = []
338
+ for rel_text in files_list:
339
+ rel = rel_text.as_posix() if hasattr(rel_text, "as_posix") else str(rel_text)
340
+ target = change_dir / rel
341
+ if not target.is_file():
342
+ return _result_error(
343
+ "ARTIFACT_MISSING", f"published artifact missing: {rel}"
344
+ )
345
+ artifact_names.append(rel)
346
+ digest.update(rel.encode("utf-8"))
347
+ digest.update(b"\0")
348
+ digest.update(target.read_bytes())
349
+ digest.update(b"\0")
350
+ actual_hash = "sha256:" + digest.hexdigest()
351
+ if actual_hash != expected_hash:
352
+ return _result_error(
353
+ "ARTIFACT_HASH_DRIFT",
354
+ f"published artifacts hash {actual_hash} != receipt {expected_hash}",
355
+ )
356
+
357
+ # Validate frontmatter on markdown artifacts.
358
+ frontmatter_results: dict[str, Any] = {}
359
+ for rel in artifact_names:
360
+ if not rel.endswith(".md"):
361
+ continue
362
+ target = change_dir / rel
363
+ try:
364
+ text = target.read_text(encoding="utf-8-sig")
365
+ except OSError as exc:
366
+ return _result_error("ARTIFACT_UNREADABLE", f"{rel}: {exc}")
367
+ fm = _frontmatter(text)
368
+ if fm is None:
369
+ return _result_error(
370
+ "ARTIFACT_FRONTMATTER_MISSING", f"{rel}: no frontmatter"
371
+ )
372
+ if fm.get("change-name") != change_name:
373
+ return _result_error(
374
+ "ARTIFACT_FRONTMATTER_INVALID",
375
+ f"{rel}: change-name '{fm.get('change-name')}' != '{change_name}'",
376
+ )
377
+ frontmatter_results[rel] = {
378
+ "change-name": fm.get("change-name"),
379
+ "status": fm.get("status"),
380
+ }
381
+
382
+ # Validate gate-policy.json is parseable JSON.
383
+ gate_policy_path = change_dir / "meta" / "gate-policy.json"
384
+ gate_policy_consistent = True
385
+ if gate_policy_path.is_file():
386
+ try:
387
+ json.loads(gate_policy_path.read_text(encoding="utf-8-sig"))
388
+ except (OSError, json.JSONDecodeError):
389
+ gate_policy_consistent = False
390
+ else:
391
+ gate_policy_consistent = False
392
+
393
+ # Validate events.ndjson: count phase.end, check for parse errors.
394
+ events_path = change_dir / "events.ndjson"
395
+ phase_end_count = 0
396
+ phase_end_status: str | None = None
397
+ events_parse_errors: list[str] = []
398
+ if events_path.is_file():
399
+ for line_no, line in enumerate(events_path.read_text(encoding="utf-8").splitlines(), 1):
400
+ if not line.strip():
401
+ continue
402
+ try:
403
+ event = json.loads(line)
404
+ except json.JSONDecodeError as exc:
405
+ events_parse_errors.append(f"line {line_no}: {exc}")
406
+ continue
407
+ if not isinstance(event, dict):
408
+ events_parse_errors.append(f"line {line_no}: not an object")
409
+ continue
410
+ if (
411
+ event.get("phase") == "plan"
412
+ and event.get("type") == "phase.end"
413
+ ):
414
+ phase_end_count += 1
415
+ phase_end_status = str(event.get("status") or "").upper()
416
+ else:
417
+ events_parse_errors.append("events.ndjson missing")
418
+
419
+ if events_parse_errors:
420
+ return _result_error(
421
+ "EVENTS_PARSE_ERROR", "; ".join(events_parse_errors[:5])
422
+ )
423
+
424
+ if phase_end_count == 0:
425
+ return _result_error("PHASE_END_MISSING", "no plan phase.end event found")
426
+ if phase_end_count > 1:
427
+ return _result_error(
428
+ "PHASE_END_DUPLICATE", f"found {phase_end_count} phase.end events"
429
+ )
430
+
431
+ # Receipt consistency: status should be finalized.
432
+ receipt_consistent = receipt.get("status") == "finalized"
433
+
434
+ return {
435
+ "ok": True,
436
+ "action": "verify",
437
+ "changeDir": str(change_dir),
438
+ "changeName": change_name,
439
+ "artifactsHash": actual_hash,
440
+ "phaseEndCount": phase_end_count,
441
+ "phaseEndStatus": phase_end_status,
442
+ "frontmatter": frontmatter_results,
443
+ "gatePolicyConsistent": gate_policy_consistent,
444
+ "receiptConsistent": receipt_consistent,
445
+ "files": artifact_names,
446
+ }
447
+
448
+
303
449
  def _read_design_capabilities(staging: Path, change_name: str) -> list[str]:
304
450
  """Read capabilities from design frontmatter (retro §5.4)."""
305
451
  design_path = staging / "spec" / f"{change_name}-design.md"
@@ -569,18 +715,24 @@ def build_parser() -> argparse.ArgumentParser:
569
715
  finalize.add_argument("--run-id", required=True)
570
716
  finalize.add_argument("--attempt", required=True, type=int)
571
717
  finalize.add_argument("--json", action="store_true")
718
+ verify = sub.add_parser("verify")
719
+ verify.add_argument("--change-dir", required=True)
720
+ verify.add_argument("--json", action="store_true")
572
721
  return parser
573
722
 
574
723
 
575
724
  def main(argv: list[str] | None = None) -> int:
576
725
  args = build_parser().parse_args(argv)
577
- result = finalize_plan(
578
- Path(args.change_dir),
579
- Path(args.staging_dir),
580
- change_name=args.change,
581
- run_id=args.run_id,
582
- attempt=args.attempt,
583
- )
726
+ if args.command == "verify":
727
+ result = verify_plan(Path(args.change_dir))
728
+ else:
729
+ result = finalize_plan(
730
+ Path(args.change_dir),
731
+ Path(args.staging_dir),
732
+ change_name=args.change,
733
+ run_id=args.run_id,
734
+ attempt=args.attempt,
735
+ )
584
736
  stream = sys.stdout if result["ok"] else sys.stderr
585
737
  if args.json:
586
738
  stream.write(json.dumps(result, ensure_ascii=False, indent=2) + "\n")
@@ -0,0 +1,175 @@
1
+ #!/usr/bin/env python3
2
+ """Controlled cleanup helper for test fixtures (retro §5.23).
3
+
4
+ Provides a host-policy-friendly entry point for removing gitignored test
5
+ fixtures (e.g. .pytest_data) within an execution root. Performs realpath
6
+ containment, rejects symlink/reparse escapes, lists exact counts before
7
+ deletion, and outputs a structured receipt.
8
+ """
9
+ from __future__ import annotations
10
+
11
+ import argparse
12
+ import json
13
+ import os
14
+ import shutil
15
+ import sys
16
+ from pathlib import Path
17
+ from typing import Any
18
+
19
+ if hasattr(sys.stdout, "reconfigure"):
20
+ sys.stdout.reconfigure(encoding="utf-8")
21
+ if hasattr(sys.stderr, "reconfigure"):
22
+ sys.stderr.reconfigure(encoding="utf-8")
23
+
24
+
25
+ def _resolve_within(execution_root: Path, rel: str) -> Path | None:
26
+ """Resolve rel under execution_root, rejecting escapes and symlinks."""
27
+ if not rel or rel.startswith("/"):
28
+ return None
29
+ # Reject absolute paths (Windows drive letter or POSIX).
30
+ if len(rel) >= 2 and rel[1] == ":":
31
+ return None
32
+ parts = rel.replace("\\", "/").split("/")
33
+ if ".." in parts:
34
+ return None
35
+ candidate = (execution_root / rel).resolve(strict=False)
36
+ # Containment check: candidate must be under execution_root.
37
+ try:
38
+ candidate.relative_to(execution_root.resolve(strict=False))
39
+ except ValueError:
40
+ return None
41
+ # Reject symlink/reparse escape: if the path is a symlink, check its target.
42
+ if candidate.is_symlink():
43
+ target = Path(os.readlink(candidate)).resolve(strict=False)
44
+ try:
45
+ target.relative_to(execution_root.resolve(strict=False))
46
+ except ValueError:
47
+ return None
48
+ # Even if target is inside, a symlinked cleanup root is suspicious;
49
+ # reject to avoid following links into unexpected locations.
50
+ return None
51
+ return candidate
52
+
53
+
54
+ def _count_files(root: Path) -> tuple[int, int]:
55
+ """Count files and total bytes under root."""
56
+ count = 0
57
+ total = 0
58
+ for dirpath, _dirs, files in os.walk(root):
59
+ for name in files:
60
+ fp = Path(dirpath) / name
61
+ try:
62
+ total += fp.stat().st_size
63
+ count += 1
64
+ except OSError:
65
+ continue
66
+ return count, total
67
+
68
+
69
+ def cleanup(execution_root: Path, cleanup_roots: list[str]) -> dict[str, Any]:
70
+ """Remove allowlisted cleanup roots within execution_root.
71
+
72
+ Returns a structured receipt. Any path escape, symlink escape, or
73
+ unallowed root results in ok=false with zero files removed.
74
+ """
75
+ execution_root = Path(execution_root).resolve(strict=False)
76
+ if not execution_root.is_dir():
77
+ return {
78
+ "ok": False,
79
+ "code": "EXECUTION_ROOT_MISSING",
80
+ "executionRoot": str(execution_root),
81
+ "removedFiles": 0,
82
+ "removedBytes": 0,
83
+ "rejectedEscapes": [],
84
+ }
85
+
86
+ rejected: list[dict[str, str]] = []
87
+ removed_files = 0
88
+ removed_bytes = 0
89
+ cleaned: list[str] = []
90
+
91
+ for rel in cleanup_roots:
92
+ target = _resolve_within(execution_root, rel)
93
+ if target is None:
94
+ rejected.append({"path": rel, "reason": "PATH_ESCAPE_REJECTED"})
95
+ continue
96
+ if not target.exists():
97
+ # Already absent — idempotent reentry.
98
+ continue
99
+ # Re-check symlink after resolution (the parent might be a symlink).
100
+ if target.is_symlink():
101
+ rejected.append({"path": rel, "reason": "SYMLINK_ESCAPE_REJECTED"})
102
+ continue
103
+ # Count before deletion for the receipt.
104
+ try:
105
+ count, total = _count_files(target)
106
+ except OSError:
107
+ count, total = 0, 0
108
+ # Delete.
109
+ try:
110
+ shutil.rmtree(target)
111
+ except OSError as exc:
112
+ rejected.append({"path": rel, "reason": f"DELETE_FAILED: {exc}"})
113
+ continue
114
+ removed_files += count
115
+ removed_bytes += total
116
+ cleaned.append(rel)
117
+
118
+ if rejected:
119
+ return {
120
+ "ok": False,
121
+ "code": rejected[0]["reason"],
122
+ "executionRoot": str(execution_root),
123
+ "removedFiles": removed_files,
124
+ "removedBytes": removed_bytes,
125
+ "rejectedEscapes": rejected,
126
+ "cleaned": cleaned,
127
+ }
128
+
129
+ if removed_files == 0 and not cleaned:
130
+ return {
131
+ "ok": True,
132
+ "code": "ALREADY_ABSENT",
133
+ "executionRoot": str(execution_root),
134
+ "removedFiles": 0,
135
+ "removedBytes": 0,
136
+ "rejectedEscapes": [],
137
+ "cleaned": [],
138
+ }
139
+
140
+ return {
141
+ "ok": True,
142
+ "code": "CLEANUP_COMPLETE",
143
+ "executionRoot": str(execution_root),
144
+ "removedFiles": removed_files,
145
+ "removedBytes": removed_bytes,
146
+ "rejectedEscapes": [],
147
+ "cleaned": cleaned,
148
+ }
149
+
150
+
151
+ def main(argv: list[str] | None = None) -> int:
152
+ parser = argparse.ArgumentParser(description=__doc__)
153
+ sub = parser.add_subparsers(dest="action", required=True)
154
+ clean_parser = sub.add_parser("cleanup")
155
+ clean_parser.add_argument("--execution-root", required=True)
156
+ clean_parser.add_argument("--cleanup-roots", required=True, help="JSON array of relative paths")
157
+ clean_parser.add_argument("--json", action="store_true")
158
+ args = parser.parse_args(argv)
159
+
160
+ try:
161
+ roots = json.loads(args.cleanup_roots)
162
+ if not isinstance(roots, list):
163
+ raise ValueError("cleanup-roots must be a JSON array")
164
+ except (json.JSONDecodeError, ValueError) as exc:
165
+ result = {"ok": False, "code": "INVALID_CLEANUP_ROOTS", "error": str(exc)}
166
+ print(json.dumps(result, ensure_ascii=False, indent=2))
167
+ return 1
168
+
169
+ result = cleanup(Path(args.execution_root), roots)
170
+ print(json.dumps(result, ensure_ascii=False, indent=2 if args.json else None))
171
+ return 0 if result["ok"] else 1
172
+
173
+
174
+ if __name__ == "__main__":
175
+ raise SystemExit(main())
@@ -2,5 +2,5 @@
2
2
  "schemaVersion": 1,
3
3
  "overlay": "java",
4
4
  "agent": "claude-code",
5
- "coreHash": "822ced2e1d949aed"
5
+ "coreHash": "6e87bc67bd51cbe9"
6
6
  }