@hunter-harness/workflow-harness 0.2.39 → 0.2.41

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 (230) hide show
  1. package/harness/bundles/general/claude-code/.harness-build.json +1 -1
  2. package/harness/bundles/general/claude-code/harness-archive/SKILL.md +1 -1
  3. package/harness/bundles/general/claude-code/harness-archive/checklist.md +3 -0
  4. package/harness/bundles/general/claude-code/harness-archive/templates/render-summary.mjs +22 -0
  5. package/harness/bundles/general/claude-code/harness-codebase-map/SKILL.md +1 -1
  6. package/harness/bundles/general/claude-code/harness-knowledge-ingest/SKILL.md +1 -1
  7. package/harness/bundles/general/claude-code/harness-knowledge-query/SKILL.md +1 -1
  8. package/harness/bundles/general/claude-code/harness-plan/SKILL.md +1 -1
  9. package/harness/bundles/general/claude-code/harness-plan/reference.md +16 -16
  10. package/harness/bundles/general/claude-code/harness-review/SKILL.md +1 -1
  11. package/harness/bundles/general/claude-code/harness-run/SKILL.md +1 -1
  12. package/harness/bundles/general/claude-code/harness-run/reference.md +1 -1
  13. package/harness/bundles/general/claude-code/harness-submit/SKILL.md +1 -1
  14. package/harness/bundles/general/claude-code/harness-sync/SKILL.md +1 -1
  15. package/harness/bundles/general/claude-code/harness-test/SKILL.md +3 -1
  16. package/harness/bundles/general/claude-code/protocols/archive-report-protocol.md +10 -0
  17. package/harness/bundles/general/claude-code/protocols/ledger-protocol.md +12 -0
  18. package/harness/bundles/general/claude-code/protocols/powershell-protocol.md +21 -0
  19. package/harness/bundles/general/claude-code/protocols/state-layout-protocol.md +11 -0
  20. package/harness/bundles/general/claude-code/scripts/harness_archive.py +413 -2
  21. package/harness/bundles/general/claude-code/scripts/harness_deploy.py +10 -51
  22. package/harness/bundles/general/claude-code/scripts/harness_environment.py +556 -39
  23. package/harness/bundles/general/claude-code/scripts/harness_gate.py +145 -0
  24. package/harness/bundles/general/claude-code/scripts/harness_ledger.py +581 -55
  25. package/harness/bundles/general/claude-code/scripts/harness_plan_finalize.py +85 -6
  26. package/harness/bundles/general/claude-code/scripts/harness_service.py +291 -22
  27. package/harness/bundles/general/claude-code/scripts/harness_test_runner.py +405 -25
  28. package/harness/bundles/general/codebuddy/.harness-build.json +1 -1
  29. package/harness/bundles/general/codebuddy/harness-archive/SKILL.md +1 -1
  30. package/harness/bundles/general/codebuddy/harness-archive/checklist.md +3 -0
  31. package/harness/bundles/general/codebuddy/harness-archive/templates/render-summary.mjs +22 -0
  32. package/harness/bundles/general/codebuddy/harness-codebase-map/SKILL.md +1 -1
  33. package/harness/bundles/general/codebuddy/harness-knowledge-ingest/SKILL.md +1 -1
  34. package/harness/bundles/general/codebuddy/harness-knowledge-query/SKILL.md +1 -1
  35. package/harness/bundles/general/codebuddy/harness-plan/SKILL.md +1 -1
  36. package/harness/bundles/general/codebuddy/harness-plan/reference.md +16 -16
  37. package/harness/bundles/general/codebuddy/harness-review/SKILL.md +1 -1
  38. package/harness/bundles/general/codebuddy/harness-run/SKILL.md +1 -1
  39. package/harness/bundles/general/codebuddy/harness-run/reference.md +1 -1
  40. package/harness/bundles/general/codebuddy/harness-submit/SKILL.md +1 -1
  41. package/harness/bundles/general/codebuddy/harness-sync/SKILL.md +1 -1
  42. package/harness/bundles/general/codebuddy/harness-test/SKILL.md +3 -1
  43. package/harness/bundles/general/codebuddy/protocols/archive-report-protocol.md +10 -0
  44. package/harness/bundles/general/codebuddy/protocols/ledger-protocol.md +12 -0
  45. package/harness/bundles/general/codebuddy/protocols/powershell-protocol.md +21 -0
  46. package/harness/bundles/general/codebuddy/protocols/state-layout-protocol.md +11 -0
  47. package/harness/bundles/general/codebuddy/scripts/harness_archive.py +413 -2
  48. package/harness/bundles/general/codebuddy/scripts/harness_deploy.py +10 -51
  49. package/harness/bundles/general/codebuddy/scripts/harness_environment.py +556 -39
  50. package/harness/bundles/general/codebuddy/scripts/harness_gate.py +145 -0
  51. package/harness/bundles/general/codebuddy/scripts/harness_ledger.py +581 -55
  52. package/harness/bundles/general/codebuddy/scripts/harness_plan_finalize.py +85 -6
  53. package/harness/bundles/general/codebuddy/scripts/harness_service.py +291 -22
  54. package/harness/bundles/general/codebuddy/scripts/harness_test_runner.py +405 -25
  55. package/harness/bundles/general/codex/.harness-build.json +1 -1
  56. package/harness/bundles/general/codex/harness-archive/SKILL.md +1 -1
  57. package/harness/bundles/general/codex/harness-archive/checklist.md +3 -0
  58. package/harness/bundles/general/codex/harness-archive/templates/render-summary.mjs +22 -0
  59. package/harness/bundles/general/codex/harness-codebase-map/SKILL.md +1 -1
  60. package/harness/bundles/general/codex/harness-knowledge-ingest/SKILL.md +1 -1
  61. package/harness/bundles/general/codex/harness-knowledge-query/SKILL.md +1 -1
  62. package/harness/bundles/general/codex/harness-plan/SKILL.md +1 -1
  63. package/harness/bundles/general/codex/harness-plan/reference.md +16 -16
  64. package/harness/bundles/general/codex/harness-review/SKILL.md +1 -1
  65. package/harness/bundles/general/codex/harness-run/SKILL.md +1 -1
  66. package/harness/bundles/general/codex/harness-run/reference.md +1 -1
  67. package/harness/bundles/general/codex/harness-submit/SKILL.md +1 -1
  68. package/harness/bundles/general/codex/harness-sync/SKILL.md +1 -1
  69. package/harness/bundles/general/codex/harness-test/SKILL.md +3 -1
  70. package/harness/bundles/general/codex/protocols/archive-report-protocol.md +10 -0
  71. package/harness/bundles/general/codex/protocols/ledger-protocol.md +12 -0
  72. package/harness/bundles/general/codex/protocols/powershell-protocol.md +21 -0
  73. package/harness/bundles/general/codex/protocols/state-layout-protocol.md +11 -0
  74. package/harness/bundles/general/codex/scripts/harness_archive.py +413 -2
  75. package/harness/bundles/general/codex/scripts/harness_deploy.py +10 -51
  76. package/harness/bundles/general/codex/scripts/harness_environment.py +556 -39
  77. package/harness/bundles/general/codex/scripts/harness_gate.py +145 -0
  78. package/harness/bundles/general/codex/scripts/harness_ledger.py +581 -55
  79. package/harness/bundles/general/codex/scripts/harness_plan_finalize.py +85 -6
  80. package/harness/bundles/general/codex/scripts/harness_service.py +291 -22
  81. package/harness/bundles/general/codex/scripts/harness_test_runner.py +405 -25
  82. package/harness/bundles/general/cursor/.harness-build.json +1 -1
  83. package/harness/bundles/general/cursor/harness-archive/SKILL.md +1 -1
  84. package/harness/bundles/general/cursor/harness-archive/checklist.md +3 -0
  85. package/harness/bundles/general/cursor/harness-archive/templates/render-summary.mjs +22 -0
  86. package/harness/bundles/general/cursor/harness-codebase-map/SKILL.md +1 -1
  87. package/harness/bundles/general/cursor/harness-knowledge-ingest/SKILL.md +1 -1
  88. package/harness/bundles/general/cursor/harness-knowledge-query/SKILL.md +1 -1
  89. package/harness/bundles/general/cursor/harness-plan/SKILL.md +1 -1
  90. package/harness/bundles/general/cursor/harness-plan/reference.md +16 -16
  91. package/harness/bundles/general/cursor/harness-review/SKILL.md +1 -1
  92. package/harness/bundles/general/cursor/harness-run/SKILL.md +1 -1
  93. package/harness/bundles/general/cursor/harness-run/reference.md +1 -1
  94. package/harness/bundles/general/cursor/harness-submit/SKILL.md +1 -1
  95. package/harness/bundles/general/cursor/harness-sync/SKILL.md +1 -1
  96. package/harness/bundles/general/cursor/harness-test/SKILL.md +3 -1
  97. package/harness/bundles/general/cursor/protocols/archive-report-protocol.md +10 -0
  98. package/harness/bundles/general/cursor/protocols/ledger-protocol.md +12 -0
  99. package/harness/bundles/general/cursor/protocols/powershell-protocol.md +21 -0
  100. package/harness/bundles/general/cursor/protocols/state-layout-protocol.md +11 -0
  101. package/harness/bundles/general/cursor/scripts/harness_archive.py +413 -2
  102. package/harness/bundles/general/cursor/scripts/harness_deploy.py +10 -51
  103. package/harness/bundles/general/cursor/scripts/harness_environment.py +556 -39
  104. package/harness/bundles/general/cursor/scripts/harness_gate.py +145 -0
  105. package/harness/bundles/general/cursor/scripts/harness_ledger.py +581 -55
  106. package/harness/bundles/general/cursor/scripts/harness_plan_finalize.py +85 -6
  107. package/harness/bundles/general/cursor/scripts/harness_service.py +291 -22
  108. package/harness/bundles/general/cursor/scripts/harness_test_runner.py +405 -25
  109. package/harness/bundles/java/claude-code/.harness-build.json +1 -1
  110. package/harness/bundles/java/claude-code/harness-apidoc/SKILL.md +1 -1
  111. package/harness/bundles/java/claude-code/harness-archive/SKILL.md +1 -1
  112. package/harness/bundles/java/claude-code/harness-archive/checklist.md +3 -0
  113. package/harness/bundles/java/claude-code/harness-archive/templates/render-summary.mjs +22 -0
  114. package/harness/bundles/java/claude-code/harness-codebase-map/SKILL.md +1 -1
  115. package/harness/bundles/java/claude-code/harness-knowledge-ingest/SKILL.md +1 -1
  116. package/harness/bundles/java/claude-code/harness-knowledge-query/SKILL.md +1 -1
  117. package/harness/bundles/java/claude-code/harness-package/SKILL.md +1 -1
  118. package/harness/bundles/java/claude-code/harness-plan/SKILL.md +1 -1
  119. package/harness/bundles/java/claude-code/harness-plan/reference.md +16 -16
  120. package/harness/bundles/java/claude-code/harness-review/SKILL.md +1 -1
  121. package/harness/bundles/java/claude-code/harness-run/SKILL.md +1 -1
  122. package/harness/bundles/java/claude-code/harness-submit/SKILL.md +1 -1
  123. package/harness/bundles/java/claude-code/harness-sync/SKILL.md +1 -1
  124. package/harness/bundles/java/claude-code/harness-test/SKILL.md +3 -1
  125. package/harness/bundles/java/claude-code/protocols/archive-report-protocol.md +10 -0
  126. package/harness/bundles/java/claude-code/protocols/ledger-protocol.md +12 -0
  127. package/harness/bundles/java/claude-code/protocols/powershell-protocol.md +21 -0
  128. package/harness/bundles/java/claude-code/protocols/state-layout-protocol.md +11 -0
  129. package/harness/bundles/java/claude-code/scripts/harness_archive.py +413 -2
  130. package/harness/bundles/java/claude-code/scripts/harness_deploy.py +10 -51
  131. package/harness/bundles/java/claude-code/scripts/harness_environment.py +556 -39
  132. package/harness/bundles/java/claude-code/scripts/harness_gate.py +145 -0
  133. package/harness/bundles/java/claude-code/scripts/harness_ledger.py +581 -55
  134. package/harness/bundles/java/claude-code/scripts/harness_plan_finalize.py +85 -6
  135. package/harness/bundles/java/claude-code/scripts/harness_service.py +291 -22
  136. package/harness/bundles/java/claude-code/scripts/harness_test_runner.py +405 -25
  137. package/harness/bundles/java/codebuddy/.harness-build.json +1 -1
  138. package/harness/bundles/java/codebuddy/harness-apidoc/SKILL.md +1 -1
  139. package/harness/bundles/java/codebuddy/harness-archive/SKILL.md +1 -1
  140. package/harness/bundles/java/codebuddy/harness-archive/checklist.md +3 -0
  141. package/harness/bundles/java/codebuddy/harness-archive/templates/render-summary.mjs +22 -0
  142. package/harness/bundles/java/codebuddy/harness-codebase-map/SKILL.md +1 -1
  143. package/harness/bundles/java/codebuddy/harness-knowledge-ingest/SKILL.md +1 -1
  144. package/harness/bundles/java/codebuddy/harness-knowledge-query/SKILL.md +1 -1
  145. package/harness/bundles/java/codebuddy/harness-package/SKILL.md +1 -1
  146. package/harness/bundles/java/codebuddy/harness-plan/SKILL.md +1 -1
  147. package/harness/bundles/java/codebuddy/harness-plan/reference.md +16 -16
  148. package/harness/bundles/java/codebuddy/harness-review/SKILL.md +1 -1
  149. package/harness/bundles/java/codebuddy/harness-run/SKILL.md +1 -1
  150. package/harness/bundles/java/codebuddy/harness-submit/SKILL.md +1 -1
  151. package/harness/bundles/java/codebuddy/harness-sync/SKILL.md +1 -1
  152. package/harness/bundles/java/codebuddy/harness-test/SKILL.md +3 -1
  153. package/harness/bundles/java/codebuddy/protocols/archive-report-protocol.md +10 -0
  154. package/harness/bundles/java/codebuddy/protocols/ledger-protocol.md +12 -0
  155. package/harness/bundles/java/codebuddy/protocols/powershell-protocol.md +21 -0
  156. package/harness/bundles/java/codebuddy/protocols/state-layout-protocol.md +11 -0
  157. package/harness/bundles/java/codebuddy/scripts/harness_archive.py +413 -2
  158. package/harness/bundles/java/codebuddy/scripts/harness_deploy.py +10 -51
  159. package/harness/bundles/java/codebuddy/scripts/harness_environment.py +556 -39
  160. package/harness/bundles/java/codebuddy/scripts/harness_gate.py +145 -0
  161. package/harness/bundles/java/codebuddy/scripts/harness_ledger.py +581 -55
  162. package/harness/bundles/java/codebuddy/scripts/harness_plan_finalize.py +85 -6
  163. package/harness/bundles/java/codebuddy/scripts/harness_service.py +291 -22
  164. package/harness/bundles/java/codebuddy/scripts/harness_test_runner.py +405 -25
  165. package/harness/bundles/java/codex/.harness-build.json +1 -1
  166. package/harness/bundles/java/codex/harness-apidoc/SKILL.md +1 -1
  167. package/harness/bundles/java/codex/harness-archive/SKILL.md +1 -1
  168. package/harness/bundles/java/codex/harness-archive/checklist.md +3 -0
  169. package/harness/bundles/java/codex/harness-archive/templates/render-summary.mjs +22 -0
  170. package/harness/bundles/java/codex/harness-codebase-map/SKILL.md +1 -1
  171. package/harness/bundles/java/codex/harness-knowledge-ingest/SKILL.md +1 -1
  172. package/harness/bundles/java/codex/harness-knowledge-query/SKILL.md +1 -1
  173. package/harness/bundles/java/codex/harness-package/SKILL.md +1 -1
  174. package/harness/bundles/java/codex/harness-plan/SKILL.md +1 -1
  175. package/harness/bundles/java/codex/harness-plan/reference.md +16 -16
  176. package/harness/bundles/java/codex/harness-review/SKILL.md +1 -1
  177. package/harness/bundles/java/codex/harness-run/SKILL.md +1 -1
  178. package/harness/bundles/java/codex/harness-submit/SKILL.md +1 -1
  179. package/harness/bundles/java/codex/harness-sync/SKILL.md +1 -1
  180. package/harness/bundles/java/codex/harness-test/SKILL.md +3 -1
  181. package/harness/bundles/java/codex/protocols/archive-report-protocol.md +10 -0
  182. package/harness/bundles/java/codex/protocols/ledger-protocol.md +12 -0
  183. package/harness/bundles/java/codex/protocols/powershell-protocol.md +21 -0
  184. package/harness/bundles/java/codex/protocols/state-layout-protocol.md +11 -0
  185. package/harness/bundles/java/codex/scripts/harness_archive.py +413 -2
  186. package/harness/bundles/java/codex/scripts/harness_deploy.py +10 -51
  187. package/harness/bundles/java/codex/scripts/harness_environment.py +556 -39
  188. package/harness/bundles/java/codex/scripts/harness_gate.py +145 -0
  189. package/harness/bundles/java/codex/scripts/harness_ledger.py +581 -55
  190. package/harness/bundles/java/codex/scripts/harness_plan_finalize.py +85 -6
  191. package/harness/bundles/java/codex/scripts/harness_service.py +291 -22
  192. package/harness/bundles/java/codex/scripts/harness_test_runner.py +405 -25
  193. package/harness/bundles/java/cursor/.harness-build.json +1 -1
  194. package/harness/bundles/java/cursor/harness-apidoc/SKILL.md +1 -1
  195. package/harness/bundles/java/cursor/harness-archive/SKILL.md +1 -1
  196. package/harness/bundles/java/cursor/harness-archive/checklist.md +3 -0
  197. package/harness/bundles/java/cursor/harness-archive/templates/render-summary.mjs +22 -0
  198. package/harness/bundles/java/cursor/harness-codebase-map/SKILL.md +1 -1
  199. package/harness/bundles/java/cursor/harness-knowledge-ingest/SKILL.md +1 -1
  200. package/harness/bundles/java/cursor/harness-knowledge-query/SKILL.md +1 -1
  201. package/harness/bundles/java/cursor/harness-package/SKILL.md +1 -1
  202. package/harness/bundles/java/cursor/harness-plan/SKILL.md +1 -1
  203. package/harness/bundles/java/cursor/harness-plan/reference.md +16 -16
  204. package/harness/bundles/java/cursor/harness-review/SKILL.md +1 -1
  205. package/harness/bundles/java/cursor/harness-run/SKILL.md +1 -1
  206. package/harness/bundles/java/cursor/harness-submit/SKILL.md +1 -1
  207. package/harness/bundles/java/cursor/harness-sync/SKILL.md +1 -1
  208. package/harness/bundles/java/cursor/harness-test/SKILL.md +3 -1
  209. package/harness/bundles/java/cursor/protocols/archive-report-protocol.md +10 -0
  210. package/harness/bundles/java/cursor/protocols/ledger-protocol.md +12 -0
  211. package/harness/bundles/java/cursor/protocols/powershell-protocol.md +21 -0
  212. package/harness/bundles/java/cursor/protocols/state-layout-protocol.md +11 -0
  213. package/harness/bundles/java/cursor/scripts/harness_archive.py +413 -2
  214. package/harness/bundles/java/cursor/scripts/harness_deploy.py +10 -51
  215. package/harness/bundles/java/cursor/scripts/harness_environment.py +556 -39
  216. package/harness/bundles/java/cursor/scripts/harness_gate.py +145 -0
  217. package/harness/bundles/java/cursor/scripts/harness_ledger.py +581 -55
  218. package/harness/bundles/java/cursor/scripts/harness_plan_finalize.py +85 -6
  219. package/harness/bundles/java/cursor/scripts/harness_service.py +291 -22
  220. package/harness/bundles/java/cursor/scripts/harness_test_runner.py +405 -25
  221. package/harness/manifests/general/claude-code.json +29 -29
  222. package/harness/manifests/general/codebuddy.json +29 -29
  223. package/harness/manifests/general/codex.json +29 -29
  224. package/harness/manifests/general/cursor.json +29 -29
  225. package/harness/manifests/java/claude-code.json +30 -30
  226. package/harness/manifests/java/codebuddy.json +30 -30
  227. package/harness/manifests/java/codex.json +30 -30
  228. package/harness/manifests/java/cursor.json +30 -30
  229. package/hunter-workflow-family.json +4 -4
  230. package/package.json +1 -1
@@ -24,6 +24,12 @@ from dataclasses import dataclass
24
24
  from pathlib import Path
25
25
  from typing import Mapping, Sequence
26
26
 
27
+ SCRIPT_DIR = Path(__file__).resolve().parent
28
+ if str(SCRIPT_DIR) not in sys.path:
29
+ sys.path.insert(0, str(SCRIPT_DIR))
30
+
31
+ import harness_environment as henv
32
+
27
33
  DEFAULT_MAX_WORKERS = 2
28
34
  DEFAULT_TIMEOUT_SECONDS = 300.0
29
35
  RESOURCE_INTENSIVE_PROFILES = frozenset({"system", "full"})
@@ -161,6 +167,115 @@ def _pid_is_running(pid: int) -> bool:
161
167
  return True
162
168
 
163
169
 
170
+ def classify_test_lock(path: Path) -> dict[str, object]:
171
+ """Classify one lock without mutating it or terminating a process."""
172
+ try:
173
+ payload = json.loads(path.read_text(encoding="utf-8"))
174
+ except FileNotFoundError:
175
+ return {"classification": "ABSENT", "path": str(path)}
176
+ except (OSError, json.JSONDecodeError) as exc:
177
+ return {
178
+ "classification": "LOCK_UNREADABLE",
179
+ "path": str(path),
180
+ "message": str(exc),
181
+ }
182
+ if not isinstance(payload, dict):
183
+ return {"classification": "LOCK_UNREADABLE", "path": str(path)}
184
+ owner = payload.get("owner")
185
+ expires = payload.get("expiresAtUnix")
186
+ if not isinstance(expires, (int, float)) or isinstance(expires, bool):
187
+ return {
188
+ "classification": "OWNER_IDENTITY_INCOMPLETE",
189
+ "path": str(path),
190
+ "token": payload.get("token"),
191
+ }
192
+ if time.time() < float(expires):
193
+ return {
194
+ "classification": "ACTIVE",
195
+ "path": str(path),
196
+ "token": payload.get("token"),
197
+ "owner": owner,
198
+ }
199
+ if not isinstance(owner, dict) or any(
200
+ not owner.get(field) for field in ("pid", "executable", "startedAt")
201
+ ):
202
+ return {
203
+ "classification": "OWNER_IDENTITY_INCOMPLETE",
204
+ "path": str(path),
205
+ "token": payload.get("token"),
206
+ }
207
+ try:
208
+ owner_pid = int(owner["pid"])
209
+ except (TypeError, ValueError):
210
+ return {
211
+ "classification": "OWNER_IDENTITY_INCOMPLETE",
212
+ "path": str(path),
213
+ "token": payload.get("token"),
214
+ }
215
+ if not _pid_is_running(owner_pid):
216
+ return {
217
+ "classification": "RECLAIMABLE",
218
+ "path": str(path),
219
+ "token": payload.get("token"),
220
+ "owner": owner,
221
+ }
222
+ verified = henv.hservice.verify_process_identity(
223
+ {
224
+ "pid": owner_pid,
225
+ "startedAt": owner["startedAt"],
226
+ "processIdentity": {"executable": owner["executable"]},
227
+ }
228
+ )
229
+ classification = (
230
+ "OWNER_ACTIVE"
231
+ if verified is True
232
+ else (
233
+ "OWNER_IDENTITY_UNCONFIRMED"
234
+ if verified is None
235
+ else "OWNER_IDENTITY_MISMATCH"
236
+ )
237
+ )
238
+ return {
239
+ "classification": classification,
240
+ "path": str(path),
241
+ "token": payload.get("token"),
242
+ "owner": owner,
243
+ }
244
+
245
+
246
+ def reap_test_lock(path: Path) -> dict[str, object]:
247
+ """Reap only an expired lock whose exact recorded owner is absent."""
248
+ classification = classify_test_lock(path)
249
+ reaped = classification.get("classification") == "RECLAIMABLE"
250
+ if reaped:
251
+ try:
252
+ path.unlink()
253
+ except FileNotFoundError:
254
+ pass
255
+ receipt = {
256
+ "schemaVersion": 1,
257
+ "action": "test-run-lock-reap",
258
+ "status": "REAPED",
259
+ "completedAtUnix": time.time(),
260
+ "lockPath": str(path),
261
+ "prior": classification,
262
+ }
263
+ receipt_dir = path.parent / "receipts"
264
+ receipt_dir.mkdir(parents=True, exist_ok=True)
265
+ target = receipt_dir / f"{path.stem}-reap-{uuid.uuid4().hex}.json"
266
+ target.write_text(
267
+ json.dumps(receipt, ensure_ascii=False, indent=2) + "\n",
268
+ encoding="utf-8",
269
+ newline="\n",
270
+ )
271
+ return {
272
+ "ok": True,
273
+ "code": "TEST_RUN_LOCK_REAPED" if reaped else "TEST_RUN_LOCK_REPORT_ONLY",
274
+ "reaped": reaped,
275
+ "lock": classification,
276
+ }
277
+
278
+
164
279
  class TestRunLock:
165
280
  """Atomic per-project single-instance lock with stale-owner recovery."""
166
281
 
@@ -182,6 +297,7 @@ class TestRunLock:
182
297
  self.path = root / f"{digest}.lock"
183
298
  self.token = uuid.uuid4().hex
184
299
  self._owned = False
300
+ self._ttl_seconds = 3600.0
185
301
 
186
302
  def _owner(self) -> dict[str, object]:
187
303
  try:
@@ -199,34 +315,42 @@ class TestRunLock:
199
315
  0o600,
200
316
  )
201
317
  except FileExistsError:
202
- owner = self._owner()
203
- try:
204
- owner_pid = int(owner.get("pid", 0))
205
- except (TypeError, ValueError):
206
- owner_pid = 0
207
- try:
208
- age_seconds = time.time() - self.path.stat().st_mtime
209
- except OSError:
210
- age_seconds = 0
211
- if _pid_is_running(owner_pid) or age_seconds < 30:
318
+ classification = classify_test_lock(self.path)
319
+ if classification.get("classification") != "RECLAIMABLE":
212
320
  raise TestRunAlreadyActive(
213
321
  "TEST_RUN_ALREADY_ACTIVE: "
214
- f"lock={self.path} ownerPid={owner_pid or 'unknown'}"
322
+ f"lock={self.path} "
323
+ f"classification={classification.get('classification')}"
215
324
  )
216
- try:
217
- self.path.unlink()
218
- except FileNotFoundError:
219
- pass
325
+ reap_test_lock(self.path)
220
326
  if attempt == 0:
221
327
  continue
222
328
  raise TestRunAlreadyActive(
223
329
  f"TEST_RUN_ALREADY_ACTIVE: could not reclaim {self.path}"
224
330
  )
225
331
  else:
332
+ now = time.time()
333
+ created = henv.hservice.get_process_create_time(os.getpid())
334
+ executable = (
335
+ henv.hservice.get_process_executable(os.getpid())
336
+ or str(Path(sys.executable).resolve())
337
+ )
226
338
  payload = {
227
- "pid": os.getpid(),
339
+ "schemaVersion": 2,
228
340
  "token": self.token,
229
- "startedAtUnix": time.time(),
341
+ "status": "ACTIVE",
342
+ "createdAtUnix": now,
343
+ "heartbeatAtUnix": now,
344
+ "expiresAtUnix": now + self._ttl_seconds,
345
+ "owner": {
346
+ "pid": os.getpid(),
347
+ "executable": executable,
348
+ "startedAt": (
349
+ created.isoformat(timespec="seconds")
350
+ if created is not None
351
+ else ""
352
+ ),
353
+ },
230
354
  }
231
355
  with os.fdopen(descriptor, "w", encoding="utf-8") as stream:
232
356
  json.dump(payload, stream, ensure_ascii=False)
@@ -235,11 +359,49 @@ class TestRunLock:
235
359
  return
236
360
  raise TestRunAlreadyActive(f"TEST_RUN_ALREADY_ACTIVE: {self.path}")
237
361
 
238
- def release(self) -> None:
362
+ def heartbeat(self) -> None:
363
+ if not self._owned:
364
+ return
365
+ owner = self._owner()
366
+ if owner.get("token") != self.token:
367
+ raise TestRunAlreadyActive(
368
+ f"TEST_RUN_LOCK_OWNERSHIP_LOST: lock={self.path}"
369
+ )
370
+ now = time.time()
371
+ owner["heartbeatAtUnix"] = now
372
+ owner["expiresAtUnix"] = now + self._ttl_seconds
373
+ with self.path.open("w", encoding="utf-8", newline="\n") as stream:
374
+ json.dump(owner, stream, ensure_ascii=False)
375
+ stream.flush()
376
+ os.fsync(stream.fileno())
377
+
378
+ def _write_closeout_receipt(self, *, status: str) -> None:
379
+ receipt = {
380
+ "schemaVersion": 1,
381
+ "action": "test-run-lock-closeout",
382
+ "status": status,
383
+ "token": self.token,
384
+ "lockPath": str(self.path),
385
+ "closedAtUnix": time.time(),
386
+ "ownerPid": os.getpid(),
387
+ }
388
+ receipt_dir = self.path.parent / "receipts"
389
+ receipt_dir.mkdir(parents=True, exist_ok=True)
390
+ target = receipt_dir / f"{self.path.stem}-{self.token}.json"
391
+ temporary = target.with_name(f".{target.name}.{uuid.uuid4().hex}.tmp")
392
+ temporary.write_text(
393
+ json.dumps(receipt, ensure_ascii=False, indent=2) + "\n",
394
+ encoding="utf-8",
395
+ newline="\n",
396
+ )
397
+ os.replace(temporary, target)
398
+
399
+ def release(self, *, status: str = "COMPLETED") -> None:
239
400
  if not self._owned:
240
401
  return
241
402
  owner = self._owner()
242
403
  if owner.get("token") == self.token:
404
+ self._write_closeout_receipt(status=status)
243
405
  try:
244
406
  self.path.unlink()
245
407
  except FileNotFoundError:
@@ -251,7 +413,7 @@ class TestRunLock:
251
413
  return self
252
414
 
253
415
  def __exit__(self, exc_type, exc, traceback) -> None:
254
- self.release()
416
+ self.release(status="ABNORMAL" if exc_type is not None else "COMPLETED")
255
417
 
256
418
 
257
419
  def lower_process_priority() -> bool:
@@ -709,6 +871,81 @@ def _execution_environment(profile: str, max_workers: int) -> dict[str, str]:
709
871
  }
710
872
 
711
873
 
874
+ def load_argv_envelope(path: Path) -> tuple[list[str], dict[str, str]]:
875
+ """Load a shell-neutral UTF-8 argument file without reparsing command text."""
876
+ try:
877
+ raw = json.loads(path.read_text(encoding="utf-8-sig"))
878
+ except (OSError, json.JSONDecodeError) as exc:
879
+ raise ValueError(f"argument file is unreadable: {exc}") from exc
880
+ argv = raw.get("argv") if isinstance(raw, dict) else None
881
+ powershell = raw.get("powershell") if isinstance(raw, dict) else None
882
+ if (
883
+ not isinstance(raw, dict)
884
+ or raw.get("schemaVersion") != 1
885
+ or raw.get("transport") != "utf8-json-argument-file"
886
+ or not isinstance(argv, list)
887
+ or not argv
888
+ or any(not isinstance(item, str) or "\0" in item for item in argv)
889
+ or not isinstance(powershell, dict)
890
+ ):
891
+ raise ValueError(
892
+ "argument file must declare schemaVersion=1, "
893
+ "transport=utf8-json-argument-file, argv[], and powershell metadata"
894
+ )
895
+ shell = {
896
+ field: str(powershell.get(field) or "").strip()
897
+ for field in ("edition", "version")
898
+ }
899
+ if not all(shell.values()):
900
+ raise ValueError("argument file powershell edition/version are required")
901
+ return list(argv), shell
902
+
903
+
904
+ def _is_persistent_service_command(command: Sequence[str]) -> bool:
905
+ """Recognize formal service-manager launches that must outlive this runner."""
906
+ normalized = [str(item).replace("\\", "/").lower() for item in command]
907
+ return "ensure" in normalized and any(
908
+ item.rsplit("/", 1)[-1] == "harness_service.py" for item in normalized
909
+ )
910
+
911
+
912
+ def _write_exec_runtime_receipt(
913
+ path: Path,
914
+ *,
915
+ command: Sequence[str],
916
+ powershell: Mapping[str, str],
917
+ result: CommandResult,
918
+ ) -> None:
919
+ canonical_argv = json.dumps(
920
+ list(command),
921
+ ensure_ascii=False,
922
+ separators=(",", ":"),
923
+ ).encode("utf-8")
924
+ payload = {
925
+ "schemaVersion": 1,
926
+ "action": "managed-exec",
927
+ "transport": "utf8-json-argument-file",
928
+ "completedAtEpochSeconds": int(time.time()),
929
+ "argvHash": "sha256:" + hashlib.sha256(canonical_argv).hexdigest(),
930
+ "argumentCount": len(command),
931
+ "powershell": {
932
+ "edition": str(powershell["edition"]),
933
+ "version": str(powershell["version"]),
934
+ },
935
+ "returncode": result.returncode,
936
+ "timedOut": result.timed_out,
937
+ "processTreeIsolated": result.process_tree_isolated,
938
+ }
939
+ path.parent.mkdir(parents=True, exist_ok=True)
940
+ temporary = path.with_name(f".{path.name}.{uuid.uuid4().hex}.tmp")
941
+ temporary.write_text(
942
+ json.dumps(payload, ensure_ascii=False, indent=2) + "\n",
943
+ encoding="utf-8",
944
+ newline="\n",
945
+ )
946
+ os.replace(temporary, path)
947
+
948
+
712
949
  def _print_plan(profile: str, plan: Sequence[TestModule]) -> None:
713
950
  payload = {
714
951
  "profile": profile,
@@ -757,8 +994,9 @@ def _run_unittest(args: argparse.Namespace) -> int:
757
994
  detached_service_modules = set(args.detached_service_module)
758
995
  detached_capability_checked = False
759
996
  try:
760
- with TestRunLock(project):
997
+ with TestRunLock(project) as run_lock:
761
998
  for index, module in enumerate(plan, start=1):
999
+ run_lock.heartbeat()
762
1000
  if (
763
1001
  module.name in detached_service_modules
764
1002
  and not detached_capability_checked
@@ -800,6 +1038,7 @@ def _run_unittest(args: argparse.Namespace) -> int:
800
1038
  capture_output=args.verbosity == 0,
801
1039
  allow_detached_processes=module.name in detached_service_modules,
802
1040
  )
1041
+ run_lock.heartbeat()
803
1042
  executed_count += 1
804
1043
  module_failure_count = len(failures)
805
1044
  if result.timed_out:
@@ -837,11 +1076,45 @@ def _run_unittest(args: argparse.Namespace) -> int:
837
1076
 
838
1077
  def _run_exec(args: argparse.Namespace) -> int:
839
1078
  command = list(args.command)
840
- if command and command[0] == "--":
1079
+ powershell: dict[str, str] | None = None
1080
+ if args.runtime_receipt and not args.argv_file:
1081
+ print(
1082
+ "RUNTIME_RECEIPT_REQUIRES_ARGUMENT_FILE: "
1083
+ "--runtime-receipt requires --argv-file",
1084
+ file=sys.stderr,
1085
+ )
1086
+ return 2
1087
+ if args.argv_file:
1088
+ if command:
1089
+ print(
1090
+ "ARGUMENT_TRANSPORT_CONFLICT: use either --argv-file or a command",
1091
+ file=sys.stderr,
1092
+ )
1093
+ return 2
1094
+ argv_file = Path(args.argv_file)
1095
+ project_for_path = Path(args.project).resolve()
1096
+ if not argv_file.is_absolute():
1097
+ argv_file = project_for_path / argv_file
1098
+ try:
1099
+ command, powershell = load_argv_envelope(argv_file)
1100
+ except ValueError as exc:
1101
+ print(f"ARGUMENT_FILE_INVALID: {exc}", file=sys.stderr)
1102
+ return 2
1103
+ elif command and command[0] == "--":
841
1104
  command = command[1:]
842
1105
  if not command:
843
- print("managed exec requires a command after --", file=sys.stderr)
1106
+ print(
1107
+ "managed exec requires a command after -- or --argv-file",
1108
+ file=sys.stderr,
1109
+ )
844
1110
  return 2
1111
+ if _is_persistent_service_command(command):
1112
+ print(
1113
+ "PERSISTENT_SERVICE_MODE_REQUIRED: invoke harness_service.py ensure "
1114
+ "outside the bounded test runner",
1115
+ file=sys.stderr,
1116
+ )
1117
+ return 7
845
1118
  if not resource_profile_allowed(
846
1119
  args.profile,
847
1120
  args.confirm_resource_intensive,
@@ -855,8 +1128,46 @@ def _run_exec(args: argparse.Namespace) -> int:
855
1128
  return 2
856
1129
  project = Path(args.project).resolve()
857
1130
  environment = _execution_environment(args.profile, args.max_workers)
1131
+ required_environment = list(args.required_environment_field or [])
1132
+ if required_environment and not args.environment_receipt:
1133
+ print(
1134
+ json.dumps(
1135
+ {
1136
+ "ok": False,
1137
+ "code": "VERIFICATION_ENVIRONMENT_INCOMPLETE",
1138
+ "missing": sorted(set(required_environment)),
1139
+ "changed": [],
1140
+ },
1141
+ ensure_ascii=False,
1142
+ ),
1143
+ file=sys.stderr,
1144
+ )
1145
+ return 6
1146
+ if args.environment_receipt:
1147
+ receipt_path = Path(args.environment_receipt)
1148
+ if not receipt_path.is_absolute():
1149
+ receipt_path = project / receipt_path
1150
+ try:
1151
+ receipt = json.loads(receipt_path.read_text(encoding="utf-8-sig"))
1152
+ except (OSError, json.JSONDecodeError) as exc:
1153
+ print(
1154
+ "VERIFICATION_ENVIRONMENT_INCOMPLETE: "
1155
+ f"environment receipt is unreadable: {exc}",
1156
+ file=sys.stderr,
1157
+ )
1158
+ return 6
1159
+ resolved = henv.resolve_verification_environment(
1160
+ receipt,
1161
+ required_fields=required_environment,
1162
+ source_environment=dict(os.environ),
1163
+ )
1164
+ if not resolved.get("ok"):
1165
+ print(json.dumps(resolved, ensure_ascii=False), file=sys.stderr)
1166
+ return 6
1167
+ environment.update(resolved["environment"])
858
1168
  try:
859
- with TestRunLock(project):
1169
+ with TestRunLock(project) as run_lock:
1170
+ run_lock.heartbeat()
860
1171
  if args.allow_detached_processes:
861
1172
  available, detail = detached_process_capability(project)
862
1173
  if not available:
@@ -874,6 +1185,7 @@ def _run_exec(args: argparse.Namespace) -> int:
874
1185
  environ=environment,
875
1186
  allow_detached_processes=args.allow_detached_processes,
876
1187
  )
1188
+ run_lock.heartbeat()
877
1189
  except TestRunAlreadyActive as exc:
878
1190
  print(str(exc), file=sys.stderr)
879
1191
  return 3
@@ -886,9 +1198,42 @@ def _run_exec(args: argparse.Namespace) -> int:
886
1198
  file=sys.stderr,
887
1199
  )
888
1200
  return 124
1201
+ if args.runtime_receipt:
1202
+ assert powershell is not None
1203
+ receipt_path = Path(args.runtime_receipt)
1204
+ if not receipt_path.is_absolute():
1205
+ receipt_path = project / receipt_path
1206
+ _write_exec_runtime_receipt(
1207
+ receipt_path,
1208
+ command=command,
1209
+ powershell=powershell,
1210
+ result=result,
1211
+ )
889
1212
  return result.returncode
890
1213
 
891
1214
 
1215
+ def _run_lock_status(args: argparse.Namespace) -> int:
1216
+ lock = TestRunLock(Path(args.project).resolve())
1217
+ print(
1218
+ json.dumps(
1219
+ {
1220
+ "ok": True,
1221
+ "action": "lock-status",
1222
+ "lock": classify_test_lock(lock.path),
1223
+ },
1224
+ ensure_ascii=False,
1225
+ indent=2,
1226
+ )
1227
+ )
1228
+ return 0
1229
+
1230
+
1231
+ def _run_lock_reap(args: argparse.Namespace) -> int:
1232
+ lock = TestRunLock(Path(args.project).resolve())
1233
+ print(json.dumps(reap_test_lock(lock.path), ensure_ascii=False, indent=2))
1234
+ return 0
1235
+
1236
+
892
1237
  def build_parser() -> argparse.ArgumentParser:
893
1238
  parser = argparse.ArgumentParser(
894
1239
  description="Run tests with bounded workers, locking, timeout, and cleanup."
@@ -951,21 +1296,56 @@ def build_parser() -> argparse.ArgumentParser:
951
1296
  help="Run one arbitrary test command under the same safety controls.",
952
1297
  )
953
1298
  exec_parser.add_argument("command", nargs=argparse.REMAINDER)
1299
+ exec_parser.add_argument(
1300
+ "--argv-file",
1301
+ help=(
1302
+ "Read argv from a UTF-8 JSON parameter file so PowerShell 5.1 and 7 "
1303
+ "use identical native-process argument semantics."
1304
+ ),
1305
+ )
1306
+ exec_parser.add_argument(
1307
+ "--runtime-receipt",
1308
+ help="Write a secret-free argv hash and PowerShell runtime receipt.",
1309
+ )
1310
+ exec_parser.add_argument(
1311
+ "--environment-receipt",
1312
+ help="Typed environment receipt created by harness_environment.py prepare.",
1313
+ )
1314
+ exec_parser.add_argument(
1315
+ "--required-environment-field",
1316
+ action="append",
1317
+ default=[],
1318
+ help="Dynamic environment field required by the command; may be repeated.",
1319
+ )
954
1320
  exec_parser.add_argument(
955
1321
  "--allow-detached-processes",
956
1322
  action="store_true",
957
1323
  help="Use exact Windows PID-lineage tracking instead of a Job Object.",
958
1324
  )
959
1325
  exec_parser.set_defaults(handler=_run_exec)
1326
+
1327
+ lock_status = subparsers.add_parser(
1328
+ "lock-status",
1329
+ help="Classify the project test lock without mutating it.",
1330
+ )
1331
+ lock_status.add_argument("--project", default=".")
1332
+ lock_status.set_defaults(handler=_run_lock_status)
1333
+
1334
+ lock_reap = subparsers.add_parser(
1335
+ "lock-reap",
1336
+ help="Reap only an expired lock whose exact owner is absent.",
1337
+ )
1338
+ lock_reap.add_argument("--project", default=".")
1339
+ lock_reap.set_defaults(handler=_run_lock_reap)
960
1340
  return parser
961
1341
 
962
1342
 
963
1343
  def main(argv: Sequence[str] | None = None) -> int:
964
1344
  parser = build_parser()
965
1345
  args = parser.parse_args(argv)
966
- if args.timeout_seconds <= 0:
1346
+ if getattr(args, "timeout_seconds", DEFAULT_TIMEOUT_SECONDS) <= 0:
967
1347
  parser.error("--timeout-seconds must be positive")
968
- if args.max_workers <= 0:
1348
+ if getattr(args, "max_workers", DEFAULT_MAX_WORKERS) <= 0:
969
1349
  parser.error("--max-workers must be positive")
970
1350
  return int(args.handler(args))
971
1351
 
@@ -2,5 +2,5 @@
2
2
  "schemaVersion": 1,
3
3
  "overlay": "java",
4
4
  "agent": "codebuddy",
5
- "coreHash": "5756e5211caf00c1"
5
+ "coreHash": "3a5923039708d7bb"
6
6
  }
@@ -2,7 +2,7 @@
2
2
  name: harness-apidoc
3
3
  description: 从 Java 后端代码生成结构化前端 API 文档(Controller/VO/ErrorCode)。使用场景:接口文档、前端接口文档、API 文档生成
4
4
  ---
5
- <!-- generated by harness_deploy.py; core=5756e5211caf00c1; overlay=java; agent=codebuddy; do not edit -->
5
+ <!-- generated by harness_deploy.py; core=3a5923039708d7bb; overlay=java; agent=codebuddy; do not edit -->
6
6
  # harness-apidoc — 前端接口文档(Java overlay)
7
7
 
8
8
  ## Purpose
@@ -3,7 +3,7 @@ name: harness-archive
3
3
  description: 归档所有变更产出(计划/测试报告/审查/SQL/API文档)到
4
4
  .harness/archive/,含归档元数据和可视化最终总结。使用场景:归档、完成归档、收尾、打包产出物
5
5
  ---
6
- <!-- generated by harness_deploy.py; core=5756e5211caf00c1; overlay=java; agent=codebuddy; do not edit -->
6
+ <!-- generated by harness_deploy.py; core=3a5923039708d7bb; overlay=java; agent=codebuddy; do not edit -->
7
7
  # harness-archive — 变更归档
8
8
 
9
9
  ## Purpose
@@ -13,6 +13,9 @@ description: harness-archive 的归档前检查项和归档后验证项。仅在
13
13
  - final-summary 的统计只能来自 summary-data 或 manifest。
14
14
  - before/after checksum 不一致时,不得删除原目录。
15
15
  - 默认渲染器 `templates/render-summary.mjs`(finalize 内嵌调用)。
16
+ - 需要跨工作区恢复时传 `--durable-root <独立存储>`;成功必须为 `ARCHIVED_DURABLE`,并验证 content-addressed object 与 durable receipt 可读回。
17
+ - `restore-durable` 只恢复到不存在的目标;目标已存在或对象 digest 不符时 fail closed,禁止覆盖。
18
+ - 未配置 durable root 时显式记录 `ARCHIVED_LOCAL_ONLY` 风险,不得把同工作区 move 宣称为独立备份。
16
19
 
17
20
  ### Wave-A 状态机与身份(IA-1 / IA-4)
18
21
 
@@ -48,6 +48,8 @@ const identity = Object.keys(record(normalized.identity)).length
48
48
  const verification = Object.keys(record(normalized.verification)).length
49
49
  ? record(normalized.verification)
50
50
  : record(data.verification);
51
+ const scenarioCoverage = record(data.scenarioCoverage);
52
+ const unexecutedScenarios = list(scenarioCoverage.unexecuted);
51
53
  const timing = Object.keys(record(normalized.timing)).length
52
54
  ? record(normalized.timing)
53
55
  : record(data.timing);
@@ -200,6 +202,21 @@ const remoteCost = record(measurements.remoteCost);
200
202
  const remoteCostTotals = record(remoteCost.totals);
201
203
  const storage = record(measurements.artifactStorage);
202
204
  const projection = record(data.projection);
205
+ const durability = record(data.archiveDurability);
206
+ const durabilityStatus = durability.status || "ARCHIVED_LOCAL_ONLY";
207
+ const scenarioCoverageStatus = scenarioCoverage.skipped
208
+ ? "SKIPPED"
209
+ : scenarioCoverage.ok
210
+ ? "OK"
211
+ : scenarioCoverage.code || "NOT_RUN";
212
+ const scenarioCoverageHtml = `
213
+ <article class="card"><h2>场景执行闭环</h2>
214
+ <div class="fact"><span>结构化运行回执</span>${pill(scenarioCoverageStatus)}</div>
215
+ <div class="fact"><span>已执行并通过</span><strong>${list(scenarioCoverage.passed).length}</strong></div>
216
+ <div class="fact"><span>未执行 / 未通过</span><span>${unexecutedScenarios.length
217
+ ? `<code>${esc(unexecutedScenarios.join(", "))}</code>`
218
+ : "无"}</span></div>
219
+ </article>`;
203
220
 
204
221
  const html = `<!doctype html>
205
222
  <html lang="zh-CN"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
@@ -227,12 +244,14 @@ const html = `<!doctype html>
227
244
  <article class="metric"><small>代码范围</small><strong>${esc(record(data.diffStat).filesChanged ?? files.length)} 个文件</strong><small class="positive">+${esc(record(data.diffStat).insertions ?? 0)} · <span class="negative">-${esc(record(data.diffStat).deletions ?? 0)}</span></small></article>
228
245
  </section>
229
246
  <article class="card"><h2>验证概览</h2><div class="verification-grid">${groupHtml}</div></article>
247
+ ${scenarioCoverageHtml}
230
248
  <section class="two-column">
231
249
  <article class="card"><h2>风险与动作</h2><div class="risk-actions"><section><h3>当前风险</h3><ul>${risksHtml}</ul></section><section><h3>人工动作</h3><ul>${actionsHtml}</ul></section></div></article>
232
250
  <div>
233
251
  ${releaseHtml}
234
252
  <article class="card"><h2>事实完整性</h2>
235
253
  <div class="fact"><span>归档完整性</span>${pill(record(data.archiveIntegrity).ok === false ? "FAIL" : record(data.archiveManifest).checksumStatus || "UNKNOWN")}</div>
254
+ <div class="fact"><span>归档持久性</span><span>${pill(durabilityStatus === "ARCHIVED_DURABLE" ? "OK" : "WARN")} <code>${esc(durabilityStatus)}</code></span></div>
236
255
  <div class="fact"><span>时间守恒</span>${pill(numeric(timing.conservationDeltaMs) === 0 ? "OK" : "WARN")}</div>
237
256
  </article>
238
257
  </div>
@@ -259,6 +278,9 @@ const html = `<!doctype html>
259
278
  <dt>基线提交</dt><dd><code>${esc(identity.baseCommit || data.baseCommit || "N/A")}</code></dd>
260
279
  <dt>报告数据版本</dt><dd>${esc(data.schemaVersion || "N/A")}</dd>
261
280
  <dt>归档意图</dt><dd title="${esc(release.intent)}">${recordOnly ? "仅记录(未请求发布)" : esc(release.intent || "未声明")}</dd>
281
+ <dt>归档持久性</dt><dd><code>${esc(durabilityStatus)}</code>${durability.risk ? ` · ${esc(durability.risk)}` : ""}</dd>
282
+ <dt>保留策略</dt><dd>${esc(durability.retentionPolicy || "N/A")}</dd>
283
+ <dt>场景回执状态</dt><dd><code>${esc(scenarioCoverage.code || scenarioCoverageStatus)}</code></dd>
262
284
  <dt>尝试记录</dt><dd>${attempts.length}</dd>
263
285
  <dt>历史质量</dt><dd>${numeric(timing.unclosedAttemptCount) === 0 ? "无未闭合尝试" : `${esc(timing.unclosedAttemptCount ?? "N/A")} 个未闭合尝试`}</dd>
264
286
  <dt>投影状态</dt><dd title="Projection / Fallback"><code>${esc(projection.code || projection.mode || "N/A")}</code></dd>
@@ -3,7 +3,7 @@ name: harness-codebase-map
3
3
  description: 分析现有代码库并生成 .harness/codebase/map/
4
4
  下的结构化代码库地图。用于陌生项目理解、重大改造前梳理、harness-sync 提示 map 过期后的人工刷新。
5
5
  ---
6
- <!-- generated by harness_deploy.py; core=5756e5211caf00c1; overlay=java; agent=codebuddy; do not edit -->
6
+ <!-- generated by harness_deploy.py; core=3a5923039708d7bb; overlay=java; agent=codebuddy; do not edit -->
7
7
  # harness-codebase-map — 代码库地图
8
8
 
9
9
  ## Purpose
@@ -3,7 +3,7 @@ name: harness-knowledge-ingest
3
3
  description: 从 .harness/archive 归档整理、同步和维护项目知识索引。适用场景:ingest knowledge、sync
4
4
  knowledge、rebuild knowledge index、promote knowledge、确认知识条目、检查知识库是否过期。
5
5
  ---
6
- <!-- generated by harness_deploy.py; core=5756e5211caf00c1; overlay=java; agent=codebuddy; do not edit -->
6
+ <!-- generated by harness_deploy.py; core=3a5923039708d7bb; overlay=java; agent=codebuddy; do not edit -->
7
7
  # harness-knowledge-ingest
8
8
 
9
9
  从 `.harness/archive/**/reports/final/summary-data.json` 抽取项目知识,生成并维护本地 `.harness/knowledge/` 索引。