@hunter-harness/workflow-harness 0.2.21 → 0.2.23

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 (214) 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 +12 -3
  3. package/harness/bundles/general/claude-code/harness-archive/checklist.md +14 -0
  4. package/harness/bundles/general/claude-code/harness-archive/templates/render-summary.mjs +24 -3
  5. package/harness/bundles/general/claude-code/harness-archive/templates/summary-data-template.json +11 -0
  6. package/harness/bundles/general/claude-code/harness-codebase-map/SKILL.md +1 -1
  7. package/harness/bundles/general/claude-code/harness-knowledge-ingest/SKILL.md +1 -1
  8. package/harness/bundles/general/claude-code/harness-knowledge-query/SKILL.md +1 -1
  9. package/harness/bundles/general/claude-code/harness-plan/SKILL.md +2 -1
  10. package/harness/bundles/general/claude-code/harness-plan/checklist.md +1 -0
  11. package/harness/bundles/general/claude-code/harness-plan/reference.md +17 -15
  12. package/harness/bundles/general/claude-code/harness-review/SKILL.md +1 -1
  13. package/harness/bundles/general/claude-code/harness-run/SKILL.md +1 -1
  14. package/harness/bundles/general/claude-code/harness-submit/SKILL.md +3 -1
  15. package/harness/bundles/general/claude-code/harness-submit/checklist.md +7 -1
  16. package/harness/bundles/general/claude-code/harness-sync/SKILL.md +1 -1
  17. package/harness/bundles/general/claude-code/harness-test/SKILL.md +1 -1
  18. package/harness/bundles/general/claude-code/harness-test/checklist.md +6 -0
  19. package/harness/bundles/general/claude-code/scripts/harness_archive.py +1168 -28
  20. package/harness/bundles/general/claude-code/scripts/harness_environment.py +459 -0
  21. package/harness/bundles/general/claude-code/scripts/harness_events.py +93 -14
  22. package/harness/bundles/general/claude-code/scripts/harness_integration.py +126 -14
  23. package/harness/bundles/general/claude-code/scripts/harness_ledger.py +30 -6
  24. package/harness/bundles/general/claude-code/scripts/harness_profile.py +33 -0
  25. package/harness/bundles/general/claude-code/scripts/harness_test_guard.py +30 -8
  26. package/harness/bundles/general/codebuddy/.harness-build.json +1 -1
  27. package/harness/bundles/general/codebuddy/harness-archive/SKILL.md +12 -3
  28. package/harness/bundles/general/codebuddy/harness-archive/checklist.md +14 -0
  29. package/harness/bundles/general/codebuddy/harness-archive/templates/render-summary.mjs +24 -3
  30. package/harness/bundles/general/codebuddy/harness-archive/templates/summary-data-template.json +11 -0
  31. package/harness/bundles/general/codebuddy/harness-codebase-map/SKILL.md +1 -1
  32. package/harness/bundles/general/codebuddy/harness-knowledge-ingest/SKILL.md +1 -1
  33. package/harness/bundles/general/codebuddy/harness-knowledge-query/SKILL.md +1 -1
  34. package/harness/bundles/general/codebuddy/harness-plan/SKILL.md +2 -1
  35. package/harness/bundles/general/codebuddy/harness-plan/checklist.md +1 -0
  36. package/harness/bundles/general/codebuddy/harness-plan/reference.md +17 -15
  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-submit/SKILL.md +3 -1
  40. package/harness/bundles/general/codebuddy/harness-submit/checklist.md +7 -1
  41. package/harness/bundles/general/codebuddy/harness-sync/SKILL.md +1 -1
  42. package/harness/bundles/general/codebuddy/harness-test/SKILL.md +1 -1
  43. package/harness/bundles/general/codebuddy/harness-test/checklist.md +6 -0
  44. package/harness/bundles/general/codebuddy/scripts/harness_archive.py +1168 -28
  45. package/harness/bundles/general/codebuddy/scripts/harness_environment.py +459 -0
  46. package/harness/bundles/general/codebuddy/scripts/harness_events.py +93 -14
  47. package/harness/bundles/general/codebuddy/scripts/harness_integration.py +126 -14
  48. package/harness/bundles/general/codebuddy/scripts/harness_ledger.py +30 -6
  49. package/harness/bundles/general/codebuddy/scripts/harness_profile.py +33 -0
  50. package/harness/bundles/general/codebuddy/scripts/harness_test_guard.py +30 -8
  51. package/harness/bundles/general/codex/.harness-build.json +1 -1
  52. package/harness/bundles/general/codex/harness-archive/SKILL.md +12 -3
  53. package/harness/bundles/general/codex/harness-archive/checklist.md +14 -0
  54. package/harness/bundles/general/codex/harness-archive/templates/render-summary.mjs +24 -3
  55. package/harness/bundles/general/codex/harness-archive/templates/summary-data-template.json +11 -0
  56. package/harness/bundles/general/codex/harness-codebase-map/SKILL.md +1 -1
  57. package/harness/bundles/general/codex/harness-knowledge-ingest/SKILL.md +1 -1
  58. package/harness/bundles/general/codex/harness-knowledge-query/SKILL.md +1 -1
  59. package/harness/bundles/general/codex/harness-plan/SKILL.md +2 -1
  60. package/harness/bundles/general/codex/harness-plan/checklist.md +1 -0
  61. package/harness/bundles/general/codex/harness-plan/reference.md +17 -15
  62. package/harness/bundles/general/codex/harness-review/SKILL.md +1 -1
  63. package/harness/bundles/general/codex/harness-run/SKILL.md +1 -1
  64. package/harness/bundles/general/codex/harness-submit/SKILL.md +3 -1
  65. package/harness/bundles/general/codex/harness-submit/checklist.md +7 -1
  66. package/harness/bundles/general/codex/harness-sync/SKILL.md +1 -1
  67. package/harness/bundles/general/codex/harness-test/SKILL.md +1 -1
  68. package/harness/bundles/general/codex/harness-test/checklist.md +6 -0
  69. package/harness/bundles/general/codex/scripts/harness_archive.py +1168 -28
  70. package/harness/bundles/general/codex/scripts/harness_environment.py +459 -0
  71. package/harness/bundles/general/codex/scripts/harness_events.py +93 -14
  72. package/harness/bundles/general/codex/scripts/harness_integration.py +126 -14
  73. package/harness/bundles/general/codex/scripts/harness_ledger.py +30 -6
  74. package/harness/bundles/general/codex/scripts/harness_profile.py +33 -0
  75. package/harness/bundles/general/codex/scripts/harness_test_guard.py +30 -8
  76. package/harness/bundles/general/cursor/.harness-build.json +1 -1
  77. package/harness/bundles/general/cursor/harness-archive/SKILL.md +12 -3
  78. package/harness/bundles/general/cursor/harness-archive/checklist.md +14 -0
  79. package/harness/bundles/general/cursor/harness-archive/templates/render-summary.mjs +24 -3
  80. package/harness/bundles/general/cursor/harness-archive/templates/summary-data-template.json +11 -0
  81. package/harness/bundles/general/cursor/harness-codebase-map/SKILL.md +1 -1
  82. package/harness/bundles/general/cursor/harness-knowledge-ingest/SKILL.md +1 -1
  83. package/harness/bundles/general/cursor/harness-knowledge-query/SKILL.md +1 -1
  84. package/harness/bundles/general/cursor/harness-plan/SKILL.md +2 -1
  85. package/harness/bundles/general/cursor/harness-plan/checklist.md +1 -0
  86. package/harness/bundles/general/cursor/harness-plan/reference.md +17 -15
  87. package/harness/bundles/general/cursor/harness-review/SKILL.md +1 -1
  88. package/harness/bundles/general/cursor/harness-run/SKILL.md +1 -1
  89. package/harness/bundles/general/cursor/harness-submit/SKILL.md +3 -1
  90. package/harness/bundles/general/cursor/harness-submit/checklist.md +7 -1
  91. package/harness/bundles/general/cursor/harness-sync/SKILL.md +1 -1
  92. package/harness/bundles/general/cursor/harness-test/SKILL.md +1 -1
  93. package/harness/bundles/general/cursor/harness-test/checklist.md +6 -0
  94. package/harness/bundles/general/cursor/scripts/harness_archive.py +1168 -28
  95. package/harness/bundles/general/cursor/scripts/harness_environment.py +459 -0
  96. package/harness/bundles/general/cursor/scripts/harness_events.py +93 -14
  97. package/harness/bundles/general/cursor/scripts/harness_integration.py +126 -14
  98. package/harness/bundles/general/cursor/scripts/harness_ledger.py +30 -6
  99. package/harness/bundles/general/cursor/scripts/harness_profile.py +33 -0
  100. package/harness/bundles/general/cursor/scripts/harness_test_guard.py +30 -8
  101. package/harness/bundles/java/claude-code/.harness-build.json +1 -1
  102. package/harness/bundles/java/claude-code/harness-apidoc/SKILL.md +1 -1
  103. package/harness/bundles/java/claude-code/harness-archive/SKILL.md +12 -3
  104. package/harness/bundles/java/claude-code/harness-archive/checklist.md +14 -0
  105. package/harness/bundles/java/claude-code/harness-archive/templates/render-summary.mjs +24 -3
  106. package/harness/bundles/java/claude-code/harness-archive/templates/summary-data-template.json +11 -0
  107. package/harness/bundles/java/claude-code/harness-codebase-map/SKILL.md +1 -1
  108. package/harness/bundles/java/claude-code/harness-knowledge-ingest/SKILL.md +1 -1
  109. package/harness/bundles/java/claude-code/harness-knowledge-query/SKILL.md +1 -1
  110. package/harness/bundles/java/claude-code/harness-package/SKILL.md +1 -1
  111. package/harness/bundles/java/claude-code/harness-plan/SKILL.md +2 -1
  112. package/harness/bundles/java/claude-code/harness-plan/checklist.md +1 -0
  113. package/harness/bundles/java/claude-code/harness-plan/reference.md +17 -15
  114. package/harness/bundles/java/claude-code/harness-review/SKILL.md +1 -1
  115. package/harness/bundles/java/claude-code/harness-run/SKILL.md +1 -1
  116. package/harness/bundles/java/claude-code/harness-submit/SKILL.md +3 -1
  117. package/harness/bundles/java/claude-code/harness-submit/checklist.md +7 -1
  118. package/harness/bundles/java/claude-code/harness-sync/SKILL.md +1 -1
  119. package/harness/bundles/java/claude-code/harness-test/SKILL.md +1 -1
  120. package/harness/bundles/java/claude-code/scripts/harness_archive.py +1168 -28
  121. package/harness/bundles/java/claude-code/scripts/harness_environment.py +459 -0
  122. package/harness/bundles/java/claude-code/scripts/harness_events.py +93 -14
  123. package/harness/bundles/java/claude-code/scripts/harness_integration.py +126 -14
  124. package/harness/bundles/java/claude-code/scripts/harness_ledger.py +30 -6
  125. package/harness/bundles/java/claude-code/scripts/harness_profile.py +33 -0
  126. package/harness/bundles/java/claude-code/scripts/harness_test_guard.py +30 -8
  127. package/harness/bundles/java/codebuddy/.harness-build.json +1 -1
  128. package/harness/bundles/java/codebuddy/harness-apidoc/SKILL.md +1 -1
  129. package/harness/bundles/java/codebuddy/harness-archive/SKILL.md +12 -3
  130. package/harness/bundles/java/codebuddy/harness-archive/checklist.md +14 -0
  131. package/harness/bundles/java/codebuddy/harness-archive/templates/render-summary.mjs +24 -3
  132. package/harness/bundles/java/codebuddy/harness-archive/templates/summary-data-template.json +11 -0
  133. package/harness/bundles/java/codebuddy/harness-codebase-map/SKILL.md +1 -1
  134. package/harness/bundles/java/codebuddy/harness-knowledge-ingest/SKILL.md +1 -1
  135. package/harness/bundles/java/codebuddy/harness-knowledge-query/SKILL.md +1 -1
  136. package/harness/bundles/java/codebuddy/harness-package/SKILL.md +1 -1
  137. package/harness/bundles/java/codebuddy/harness-plan/SKILL.md +2 -1
  138. package/harness/bundles/java/codebuddy/harness-plan/checklist.md +1 -0
  139. package/harness/bundles/java/codebuddy/harness-plan/reference.md +17 -15
  140. package/harness/bundles/java/codebuddy/harness-review/SKILL.md +1 -1
  141. package/harness/bundles/java/codebuddy/harness-run/SKILL.md +1 -1
  142. package/harness/bundles/java/codebuddy/harness-submit/SKILL.md +3 -1
  143. package/harness/bundles/java/codebuddy/harness-submit/checklist.md +7 -1
  144. package/harness/bundles/java/codebuddy/harness-sync/SKILL.md +1 -1
  145. package/harness/bundles/java/codebuddy/harness-test/SKILL.md +1 -1
  146. package/harness/bundles/java/codebuddy/scripts/harness_archive.py +1168 -28
  147. package/harness/bundles/java/codebuddy/scripts/harness_environment.py +459 -0
  148. package/harness/bundles/java/codebuddy/scripts/harness_events.py +93 -14
  149. package/harness/bundles/java/codebuddy/scripts/harness_integration.py +126 -14
  150. package/harness/bundles/java/codebuddy/scripts/harness_ledger.py +30 -6
  151. package/harness/bundles/java/codebuddy/scripts/harness_profile.py +33 -0
  152. package/harness/bundles/java/codebuddy/scripts/harness_test_guard.py +30 -8
  153. package/harness/bundles/java/codex/.harness-build.json +1 -1
  154. package/harness/bundles/java/codex/harness-apidoc/SKILL.md +1 -1
  155. package/harness/bundles/java/codex/harness-archive/SKILL.md +12 -3
  156. package/harness/bundles/java/codex/harness-archive/checklist.md +14 -0
  157. package/harness/bundles/java/codex/harness-archive/templates/render-summary.mjs +24 -3
  158. package/harness/bundles/java/codex/harness-archive/templates/summary-data-template.json +11 -0
  159. package/harness/bundles/java/codex/harness-codebase-map/SKILL.md +1 -1
  160. package/harness/bundles/java/codex/harness-knowledge-ingest/SKILL.md +1 -1
  161. package/harness/bundles/java/codex/harness-knowledge-query/SKILL.md +1 -1
  162. package/harness/bundles/java/codex/harness-package/SKILL.md +1 -1
  163. package/harness/bundles/java/codex/harness-plan/SKILL.md +2 -1
  164. package/harness/bundles/java/codex/harness-plan/checklist.md +1 -0
  165. package/harness/bundles/java/codex/harness-plan/reference.md +17 -15
  166. package/harness/bundles/java/codex/harness-review/SKILL.md +1 -1
  167. package/harness/bundles/java/codex/harness-run/SKILL.md +1 -1
  168. package/harness/bundles/java/codex/harness-submit/SKILL.md +3 -1
  169. package/harness/bundles/java/codex/harness-submit/checklist.md +7 -1
  170. package/harness/bundles/java/codex/harness-sync/SKILL.md +1 -1
  171. package/harness/bundles/java/codex/harness-test/SKILL.md +1 -1
  172. package/harness/bundles/java/codex/scripts/harness_archive.py +1168 -28
  173. package/harness/bundles/java/codex/scripts/harness_environment.py +459 -0
  174. package/harness/bundles/java/codex/scripts/harness_events.py +93 -14
  175. package/harness/bundles/java/codex/scripts/harness_integration.py +126 -14
  176. package/harness/bundles/java/codex/scripts/harness_ledger.py +30 -6
  177. package/harness/bundles/java/codex/scripts/harness_profile.py +33 -0
  178. package/harness/bundles/java/codex/scripts/harness_test_guard.py +30 -8
  179. package/harness/bundles/java/cursor/.harness-build.json +1 -1
  180. package/harness/bundles/java/cursor/harness-apidoc/SKILL.md +1 -1
  181. package/harness/bundles/java/cursor/harness-archive/SKILL.md +12 -3
  182. package/harness/bundles/java/cursor/harness-archive/checklist.md +14 -0
  183. package/harness/bundles/java/cursor/harness-archive/templates/render-summary.mjs +24 -3
  184. package/harness/bundles/java/cursor/harness-archive/templates/summary-data-template.json +11 -0
  185. package/harness/bundles/java/cursor/harness-codebase-map/SKILL.md +1 -1
  186. package/harness/bundles/java/cursor/harness-knowledge-ingest/SKILL.md +1 -1
  187. package/harness/bundles/java/cursor/harness-knowledge-query/SKILL.md +1 -1
  188. package/harness/bundles/java/cursor/harness-package/SKILL.md +1 -1
  189. package/harness/bundles/java/cursor/harness-plan/SKILL.md +2 -1
  190. package/harness/bundles/java/cursor/harness-plan/checklist.md +1 -0
  191. package/harness/bundles/java/cursor/harness-plan/reference.md +17 -15
  192. package/harness/bundles/java/cursor/harness-review/SKILL.md +1 -1
  193. package/harness/bundles/java/cursor/harness-run/SKILL.md +1 -1
  194. package/harness/bundles/java/cursor/harness-submit/SKILL.md +3 -1
  195. package/harness/bundles/java/cursor/harness-submit/checklist.md +7 -1
  196. package/harness/bundles/java/cursor/harness-sync/SKILL.md +1 -1
  197. package/harness/bundles/java/cursor/harness-test/SKILL.md +1 -1
  198. package/harness/bundles/java/cursor/scripts/harness_archive.py +1168 -28
  199. package/harness/bundles/java/cursor/scripts/harness_environment.py +459 -0
  200. package/harness/bundles/java/cursor/scripts/harness_events.py +93 -14
  201. package/harness/bundles/java/cursor/scripts/harness_integration.py +126 -14
  202. package/harness/bundles/java/cursor/scripts/harness_ledger.py +30 -6
  203. package/harness/bundles/java/cursor/scripts/harness_profile.py +33 -0
  204. package/harness/bundles/java/cursor/scripts/harness_test_guard.py +30 -8
  205. package/harness/manifests/general/claude-code.json +29 -25
  206. package/harness/manifests/general/codebuddy.json +29 -25
  207. package/harness/manifests/general/codex.json +29 -25
  208. package/harness/manifests/general/cursor.json +29 -25
  209. package/harness/manifests/java/claude-code.json +30 -26
  210. package/harness/manifests/java/codebuddy.json +30 -26
  211. package/harness/manifests/java/codex.json +30 -26
  212. package/harness/manifests/java/cursor.json +30 -26
  213. package/hunter-workflow-family.json +2 -2
  214. package/package.json +1 -1
@@ -43,6 +43,7 @@ if hasattr(sys.stderr, "reconfigure"):
43
43
  sys.stderr.reconfigure(encoding="utf-8")
44
44
 
45
45
  STEP_ORDER = ["preflight", "prepare", "merge", "verify", "push", "cleanup"]
46
+ DEFAULT_VERIFY_TIMEOUT_SECONDS = 1800
46
47
  HEAVY_WORKTREE_ROOTS = (
47
48
  "node_modules", ".venv", "venv", "build", "dist", "target", ".cache", "__pycache__"
48
49
  )
@@ -56,6 +57,48 @@ def now_iso() -> str:
56
57
  return dt.datetime.now().astimezone().isoformat(timespec="milliseconds")
57
58
 
58
59
 
60
+ def _split_verify_command(raw: str, *, platform: str = sys.platform) -> list[str]:
61
+ # Harness profile/CLI commands use one portable quoting contract. Keep
62
+ # POSIX tokenization on Windows too; executable normalization happens later.
63
+ return shlex.split(raw, posix=True)
64
+
65
+
66
+ def _dedupe_verify_commands(
67
+ commands: Sequence[Sequence[str]],
68
+ ) -> list[list[str]]:
69
+ """Preserve order while preventing explicit/profile duplicate execution."""
70
+ result: list[list[str]] = []
71
+ seen: set[tuple[str, ...]] = set()
72
+ for command in commands:
73
+ normalized = tuple(str(item) for item in command)
74
+ if normalized in seen:
75
+ continue
76
+ seen.add(normalized)
77
+ result.append(list(normalized))
78
+ return result
79
+
80
+
81
+ def _normalize_verify_command(
82
+ command: Sequence[str],
83
+ *,
84
+ platform: str = sys.platform,
85
+ resolver: Callable[[str], str | None] = shutil.which,
86
+ ) -> list[str]:
87
+ """Resolve Windows package-manager wrappers without enabling shell mode."""
88
+ result = [str(item) for item in command]
89
+ if not result or not platform.lower().startswith("win"):
90
+ return result
91
+ executable = Path(result[0]).name.lower()
92
+ if Path(executable).suffix:
93
+ return result
94
+ if executable not in {"npm", "npx", "pnpm", "yarn", "corepack"}:
95
+ return result
96
+ resolved = resolver(executable + ".cmd") or resolver(executable)
97
+ if resolved:
98
+ result[0] = resolved
99
+ return result
100
+
101
+
59
102
  class IntegrationError(Exception):
60
103
  code = "INTEGRATION_ERROR"
61
104
 
@@ -224,6 +267,11 @@ def load_journal(project_root: Path, transaction_id: str) -> dict[str, Any]:
224
267
  def _pid_alive(pid: int) -> bool:
225
268
  if pid <= 0:
226
269
  return False
270
+ # On Windows, os.kill(pid, 0) can transiently report EINVAL for the
271
+ # current low-priority/background process. The current process is
272
+ # unambiguously alive, so avoid reclaiming its lock on that platform race.
273
+ if pid == os.getpid():
274
+ return True
227
275
  try:
228
276
  os.kill(pid, 0)
229
277
  except ProcessLookupError:
@@ -878,7 +926,7 @@ class IntegrationTransaction:
878
926
  commands: list[list[str]] = []
879
927
  for index, item in enumerate(required):
880
928
  if isinstance(item, str) and item.strip():
881
- commands.append(shlex.split(item.strip()))
929
+ commands.append(_split_verify_command(item.strip()))
882
930
  continue
883
931
  if not isinstance(item, dict):
884
932
  raise ValueError(f"requiredOnMerge[{index}] must be string or object")
@@ -886,18 +934,26 @@ class IntegrationTransaction:
886
934
  if isinstance(raw, list) and raw and all(isinstance(x, str) and x for x in raw):
887
935
  commands.append([str(x) for x in raw])
888
936
  elif isinstance(raw, str) and raw.strip():
889
- commands.append(shlex.split(raw.strip()))
937
+ commands.append(_split_verify_command(raw.strip()))
890
938
  else:
891
939
  raise ValueError(f"requiredOnMerge[{index}].command missing")
892
940
  return commands
893
941
 
894
- def verify(self, commands: Sequence[Sequence[str]] | None = None) -> dict[str, Any]:
942
+ def verify(
943
+ self,
944
+ commands: Sequence[Sequence[str]] | None = None,
945
+ *,
946
+ timeout_seconds: float = DEFAULT_VERIFY_TIMEOUT_SECONDS,
947
+ ) -> dict[str, Any]:
895
948
  commands = list(commands or [])
896
949
  # Wave-2 H-9: append profile mergeVerification.requiredOnMerge commands.
897
950
  try:
898
951
  commands.extend(self._required_on_merge_commands())
899
952
  except Exception as exc: # noqa: BLE001 — surface as verify plan error
900
953
  raise VerifyPlanMissingError(f"mergeVerification load failed: {exc}") from exc
954
+ commands = _dedupe_verify_commands(commands)
955
+ if timeout_seconds <= 0:
956
+ raise VerifyPlanMissingError("verification timeout must be greater than zero")
901
957
 
902
958
  def action(journal: dict[str, Any]) -> None:
903
959
  if not commands or any(
@@ -910,21 +966,67 @@ class IntegrationTransaction:
910
966
  intg_root = Path(journal["integrationRoot"]).resolve()
911
967
  results: list[dict[str, Any]] = []
912
968
  for command in commands:
969
+ executable_command = _normalize_verify_command(command)
913
970
  started = dt.datetime.now().astimezone()
914
- proc = subprocess.run(
915
- list(command),
916
- cwd=str(intg_root),
917
- capture_output=True,
918
- text=True,
919
- encoding="utf-8",
920
- errors="replace",
921
- check=False,
922
- )
971
+ try:
972
+ proc = subprocess.run(
973
+ executable_command,
974
+ cwd=str(intg_root),
975
+ capture_output=True,
976
+ text=True,
977
+ encoding="utf-8",
978
+ errors="replace",
979
+ check=False,
980
+ timeout=timeout_seconds,
981
+ )
982
+ except subprocess.TimeoutExpired:
983
+ finished = dt.datetime.now().astimezone()
984
+ results.append(
985
+ {
986
+ "command": list(command),
987
+ "resolvedCommand": executable_command,
988
+ "exitCode": None,
989
+ "timedOut": True,
990
+ "timeoutSeconds": timeout_seconds,
991
+ "startedAt": started.isoformat(timespec="milliseconds"),
992
+ "finishedAt": finished.isoformat(timespec="milliseconds"),
993
+ "durationMs": int(
994
+ (finished - started).total_seconds() * 1000
995
+ ),
996
+ }
997
+ )
998
+ journal["verifyResults"] = results
999
+ self._save(journal)
1000
+ raise VerificationFailedError(
1001
+ f"command timed out after {timeout_seconds}s: {' '.join(command)}"
1002
+ )
1003
+ except FileNotFoundError as exc:
1004
+ finished = dt.datetime.now().astimezone()
1005
+ results.append(
1006
+ {
1007
+ "command": list(command),
1008
+ "resolvedCommand": executable_command,
1009
+ "exitCode": None,
1010
+ "error": str(exc),
1011
+ "startedAt": started.isoformat(timespec="milliseconds"),
1012
+ "finishedAt": finished.isoformat(timespec="milliseconds"),
1013
+ "durationMs": int(
1014
+ (finished - started).total_seconds() * 1000
1015
+ ),
1016
+ }
1017
+ )
1018
+ journal["verifyResults"] = results
1019
+ self._save(journal)
1020
+ raise VerifyPlanMissingError(
1021
+ f"verification executable not found: {executable_command[0]}"
1022
+ ) from exc
923
1023
  finished = dt.datetime.now().astimezone()
924
1024
  results.append(
925
1025
  {
926
1026
  "command": list(command),
1027
+ "resolvedCommand": executable_command,
927
1028
  "exitCode": proc.returncode,
1029
+ "timedOut": False,
928
1030
  "startedAt": started.isoformat(timespec="milliseconds"),
929
1031
  "finishedAt": finished.isoformat(timespec="milliseconds"),
930
1032
  "durationMs": int(
@@ -1449,11 +1551,16 @@ def _parse_verify_commands(values: Sequence[str] | None) -> list[list[str]]:
1449
1551
  POSIX shlex semantics: single/double quotes group tokens, backslash
1450
1552
  escapes the next character (quote Windows paths).
1451
1553
  """
1452
- return [shlex.split(value) for value in (values or [])]
1554
+ return [_split_verify_command(value) for value in (values or [])]
1453
1555
 
1454
1556
 
1455
1557
  def cmd_verify(args: argparse.Namespace) -> int:
1456
- return _emit(_txn_from_args(args).verify(commands=_parse_verify_commands(args.command)))
1558
+ return _emit(
1559
+ _txn_from_args(args).verify(
1560
+ commands=_parse_verify_commands(args.command),
1561
+ timeout_seconds=float(args.timeout_seconds),
1562
+ )
1563
+ )
1457
1564
 
1458
1565
 
1459
1566
  def cmd_push(args: argparse.Namespace) -> int:
@@ -1543,6 +1650,11 @@ def build_parser() -> argparse.ArgumentParser:
1543
1650
  p_verify.add_argument("--feature-branch", required=True)
1544
1651
  p_verify.add_argument("--temp-root", required=True)
1545
1652
  p_verify.add_argument("--command", action="append")
1653
+ p_verify.add_argument(
1654
+ "--timeout-seconds",
1655
+ type=float,
1656
+ default=DEFAULT_VERIFY_TIMEOUT_SECONDS,
1657
+ )
1546
1658
  p_verify.set_defaults(func=cmd_verify)
1547
1659
 
1548
1660
  return parser
@@ -219,14 +219,30 @@ def sha256_file(path: Path) -> str:
219
219
  return digest.hexdigest()
220
220
 
221
221
 
222
- def compute_inputs_hash(file_paths: list[str]) -> tuple[str, list[str]]:
222
+ def compute_inputs_hash(
223
+ file_paths: list[str], *, project_root: Path | str | None = None
224
+ ) -> tuple[str, list[str]]:
223
225
  """Per-file content sha256 → sort digests → hash again (order-independent)."""
226
+ root = (
227
+ Path(project_root).expanduser().resolve()
228
+ if project_root is not None
229
+ else None
230
+ )
224
231
  by_path: dict[str, str] = {}
225
232
  for raw in file_paths:
226
233
  path = resolve_path(raw)
227
234
  if not path.is_file():
228
235
  raise FileNotFoundError(f"file not found: {raw}")
229
- by_path[path.as_posix()] = sha256_file(path)
236
+ if root is not None:
237
+ try:
238
+ logical_path = path.relative_to(root).as_posix()
239
+ except ValueError as exc:
240
+ raise ValueError(
241
+ f"INPUT_OUTSIDE_PROJECT: {path} is outside {root}"
242
+ ) from exc
243
+ else:
244
+ logical_path = path.as_posix()
245
+ by_path[logical_path] = sha256_file(path)
230
246
 
231
247
  # Stable file list for callers; bind every resolved path to its digest.
232
248
  # Hashing only the content multiset let a path swap incorrectly reuse a
@@ -1056,6 +1072,7 @@ def decide_can_reuse(
1056
1072
  requested_profile_hash: str | None = None,
1057
1073
  requested_environment_hash: str | None = None,
1058
1074
  requested_db_schema_hash: str | None = None,
1075
+ project_root: Path | str | None = None,
1059
1076
  ) -> dict[str, Any]:
1060
1077
  ledger, ledger_path = load_ledger(change_dir)
1061
1078
  if ledger is None:
@@ -1220,8 +1237,10 @@ def decide_can_reuse(
1220
1237
  }
1221
1238
 
1222
1239
  try:
1223
- current_hash, current_files = compute_inputs_hash(files)
1224
- except FileNotFoundError as exc:
1240
+ current_hash, current_files = compute_inputs_hash(
1241
+ files, project_root=project_root
1242
+ )
1243
+ except (FileNotFoundError, ValueError) as exc:
1225
1244
  return {
1226
1245
  "ok": True,
1227
1246
  "reuse": False,
@@ -1351,7 +1370,9 @@ def cmd_hash(args: argparse.Namespace) -> int:
1351
1370
  files, project_root = input_files_from_args(args)
1352
1371
  if not files:
1353
1372
  return emit_error("hash requires --files or --files-from", as_json=as_json)
1354
- inputs_hash, inputs_files = compute_inputs_hash(files)
1373
+ inputs_hash, inputs_files = compute_inputs_hash(
1374
+ files, project_root=project_root
1375
+ )
1355
1376
  except (OSError, FileNotFoundError) as exc:
1356
1377
  return emit_error(str(exc), as_json=as_json)
1357
1378
 
@@ -1418,6 +1439,7 @@ def cmd_can_reuse(args: argparse.Namespace) -> int:
1418
1439
  requested_profile_hash=getattr(args, "profile_hash", None),
1419
1440
  requested_environment_hash=getattr(args, "environment_hash", None),
1420
1441
  requested_db_schema_hash=getattr(args, "db_schema_hash", None),
1442
+ project_root=project_root,
1421
1443
  )
1422
1444
  except (OSError, ValueError, json.JSONDecodeError) as exc:
1423
1445
  return emit_error(f"can-reuse failed: {exc}", as_json=as_json)
@@ -1470,7 +1492,9 @@ def cmd_record(args: argparse.Namespace) -> int:
1470
1492
 
1471
1493
  try:
1472
1494
  status = normalize_status(args.status)
1473
- inputs_hash, inputs_files = compute_inputs_hash(files)
1495
+ inputs_hash, inputs_files = compute_inputs_hash(
1496
+ files, project_root=project_root
1497
+ )
1474
1498
  ledger, _existing_path = load_ledger(change_dir)
1475
1499
  if ledger is None:
1476
1500
  ledger = {
@@ -66,6 +66,16 @@ DEFAULT_EXCLUDED_ROOTS: tuple[str, ...] = (
66
66
  ".cache",
67
67
  )
68
68
 
69
+
70
+ def profile_defaults_fingerprint() -> str:
71
+ """Fingerprint generated defaults so old profiles are refreshed safely."""
72
+ payload = {
73
+ "schemaVersion": SCHEMA_VERSION,
74
+ "excludedRoots": list(DEFAULT_EXCLUDED_ROOTS),
75
+ }
76
+ encoded = json.dumps(payload, ensure_ascii=False, sort_keys=True).encode("utf-8")
77
+ return f"sha256:{hashlib.sha256(encoded).hexdigest()}"
78
+
69
79
  # 覆盖层级(spec §3.2):unitTest ⊂ unitTestFull;package 独立;submit 复用 unitTestFull。
70
80
  VERIFICATION_KEYS: tuple[str, ...] = (
71
81
  "compile",
@@ -330,6 +340,7 @@ def _node_commands(project: Path) -> dict[str, Any]:
330
340
  def empty_profile_skeleton(excluded: tuple[str, ...] | list[str]) -> dict[str, Any]:
331
341
  return {
332
342
  "schemaVersion": SCHEMA_VERSION,
343
+ "defaultsFingerprint": profile_defaults_fingerprint(),
333
344
  "detectedAt": "",
334
345
  "projectType": "unknown",
335
346
  "toolPaths": {"node": "", "mvn": ""},
@@ -596,6 +607,28 @@ def check(project: Path) -> dict[str, Any]:
596
607
  issues: list[str] = []
597
608
  stale = False
598
609
 
610
+ expected_defaults_fingerprint = profile_defaults_fingerprint()
611
+ if profile.get("defaultsFingerprint") != expected_defaults_fingerprint:
612
+ stale = True
613
+ issues.append(
614
+ "defaultsFingerprint is missing or outdated; run detect to refresh generated defaults"
615
+ )
616
+
617
+ excluded_roots = {
618
+ str(item).replace("\\", "/").strip("/")
619
+ for item in (profile.get("excludedRoots") or [])
620
+ if isinstance(item, str) and item.strip("/")
621
+ }
622
+ missing_excluded_roots = [
623
+ item for item in DEFAULT_EXCLUDED_ROOTS if item not in excluded_roots
624
+ ]
625
+ if missing_excluded_roots:
626
+ stale = True
627
+ issues.append(
628
+ "excludedRoots missing generated defaults: "
629
+ + ", ".join(missing_excluded_roots)
630
+ )
631
+
599
632
  fp = profile.get("fingerprint") or {}
600
633
  stored_pom = fp.get("pomHash") or "" if isinstance(fp, dict) else ""
601
634
  current_pom = sha256_file(project / "pom.xml") if (project / "pom.xml").is_file() else ""
@@ -32,6 +32,24 @@ MODE = "force-track-touched"
32
32
  MANIFEST_REL = Path("evidence") / "test-tracking.json"
33
33
  SNAPSHOT_REL = Path("evidence") / "test-guard-snapshot.json"
34
34
  PROFILE_REL = Path(".harness") / "config" / "build-profile.json"
35
+ MANDATORY_EXCLUDED_ROOTS = (
36
+ ".git",
37
+ ".harness",
38
+ ".worktrees",
39
+ ".claude/worktrees",
40
+ ".codex/worktrees",
41
+ ".cursor/worktrees",
42
+ ".codebuddy/worktrees",
43
+ ".codeium/worktrees",
44
+ "target",
45
+ "build",
46
+ "dist",
47
+ "node_modules",
48
+ ".gradle",
49
+ "__pycache__",
50
+ ".pytest_cache",
51
+ ".cache",
52
+ )
35
53
  REASONS = ("tdd-created", "stale-test-repair", "test-updated")
36
54
 
37
55
 
@@ -213,11 +231,14 @@ def _profile_config(project: Path) -> tuple[list[str], list[str]] | None:
213
231
  if not isinstance(paths, list):
214
232
  return [], []
215
233
  excluded = profile.get("excludedRoots") if isinstance(profile, dict) else None
216
- excluded_roots = (
234
+ configured_roots = (
217
235
  [item.replace("\\", "/").strip("/") for item in excluded if isinstance(item, str) and item]
218
236
  if isinstance(excluded, list)
219
237
  else []
220
238
  )
239
+ excluded_roots = list(
240
+ dict.fromkeys([*configured_roots, *MANDATORY_EXCLUDED_ROOTS])
241
+ )
221
242
  patterns = [
222
243
  item.replace("\\", "/") for item in paths if isinstance(item, str) and item
223
244
  ]
@@ -254,11 +275,8 @@ def _allowed_test_path(project: Path, rel: str) -> bool:
254
275
  if config is None:
255
276
  return _standard_test_path(rel)
256
277
  patterns, excluded_roots = config
257
- rel_parts = tuple(os.path.normcase(part) for part in rel.split("/"))
258
- for excluded in excluded_roots:
259
- excluded_parts = tuple(os.path.normcase(part) for part in excluded.split("/"))
260
- if rel_parts[: len(excluded_parts)] == excluded_parts:
261
- return False
278
+ if _path_is_excluded(rel, excluded_roots):
279
+ return False
262
280
  return any(_matches_pattern(rel, pattern) for pattern in patterns)
263
281
 
264
282
 
@@ -820,8 +838,12 @@ def _path_is_excluded(rel: str, excluded_roots: list[str]) -> bool:
820
838
  excluded_parts = tuple(
821
839
  os.path.normcase(part) for part in excluded.split("/") if part
822
840
  )
823
- if excluded_parts and rel_parts[: len(excluded_parts)] == excluded_parts:
824
- return True
841
+ if not excluded_parts:
842
+ continue
843
+ width = len(excluded_parts)
844
+ for index in range(len(rel_parts) - width + 1):
845
+ if rel_parts[index : index + width] == excluded_parts:
846
+ return True
825
847
  return False
826
848
 
827
849
 
@@ -2,5 +2,5 @@
2
2
  "schemaVersion": 1,
3
3
  "overlay": "java",
4
4
  "agent": "claude-code",
5
- "coreHash": "a9c096220b88ca56"
5
+ "coreHash": "506c7d9df54c0f80"
6
6
  }
@@ -20,7 +20,7 @@ disallowed-tools:
20
20
  - Bash(awk *)
21
21
  - Bash(curl *)
22
22
  ---
23
- <!-- generated by harness_deploy.py; core=a9c096220b88ca56; overlay=java; agent=claude-code; do not edit -->
23
+ <!-- generated by harness_deploy.py; core=506c7d9df54c0f80; overlay=java; agent=claude-code; do not edit -->
24
24
  # harness-apidoc — 前端接口文档(Java overlay)
25
25
 
26
26
  ## Purpose
@@ -20,7 +20,7 @@ disallowed-tools:
20
20
  - Bash(awk *)
21
21
  - Bash(curl *)
22
22
  ---
23
- <!-- generated by harness_deploy.py; core=a9c096220b88ca56; overlay=java; agent=claude-code; do not edit -->
23
+ <!-- generated by harness_deploy.py; core=506c7d9df54c0f80; overlay=java; agent=claude-code; do not edit -->
24
24
  # harness-archive — 变更归档
25
25
 
26
26
  ## Purpose
@@ -56,6 +56,11 @@ disallowed-tools:
56
56
  - `evidence/verification-ledger.json`
57
57
  - 至少一个 test 或 review 报告/证据
58
58
  - 缺失任一项 → `archivable=false`;可从 `.harness/cache/change-snapshots/<change>/` 恢复正式层后再重试
59
+ - **产品候选验证**:
60
+ - 有远端 CI:保留 `product-candidate-ci.json` 兼容证据;归档会把有效的 legacy schema v1 证据迁移为 `remote-claimed` 的 schema v2 `product-candidate-verification.json`,不会冒充 `remote-attested`。
61
+ - `remote-attested` 必须同时带远端运行 URL 与 `verification.attestationDigest`;只有 URL、旧 JSON 哈希或 ledger 哈希只能达到 `remote-claimed`。
62
+ - 无 CI:先运行 `harness_archive.py certify-local --change-dir ... --project . --json`,只复用身份一致且完整的 `unitTestFull` ledger 证据,不重复执行测试。
63
+ - `release-candidate` 默认要求候选验证通过;仅封存未完成事实时显式使用 `--intent record-only`,归档结果必须为 `releaseEligible=false`,不得用于发布。
59
64
 
60
65
  ## Inputs
61
66
 
@@ -109,9 +114,9 @@ blocking user confirmation 让用户确认归档操作。**用户拒绝 → 终
109
114
 
110
115
  ### Phase 3:执行归档
111
116
 
112
- 1. 运行 `python <skills-root>/scripts/harness_archive.py status --change-dir ".harness/changes/<change-name>" --json` 前置检查。
117
+ 1. 运行 `python <skills-root>/scripts/harness_archive.py status --change-dir ".harness/changes/<change-name>" --intent release-candidate --json` 前置检查。无 CI 项目若已有完整全量 ledger,先运行 `certify-local`;若用户明确只封存事实,则 status/finalize 均使用 `--intent record-only`。
113
118
  2. `meta/archive-meta.md` **由 `harness_archive.py finalize` 自动生成**(与 summary-data `finalStatus` 同源);**禁止 agent 手写**该文件,手写视为数据丢失。维护者结论写入 events(decision/issue)即可,finalize 会汇总到 summary / archive-meta。
114
- 3. 运行 `python <skills-root>/scripts/harness_archive.py finalize --change-dir ".harness/changes/<change-name>" --archive-root ".harness/archive" --json`;读 JSON(cleanup、事件、移动、collect、render、validate、manifest 比对、archive-meta)。finalize 内部负责且仅负责一次 `phase.start` / `phase.end`,调用者不得重复追加。**finalize 不再同步执行知识维护**(§8.2):它写一个 `pending` maintenance-outbox 项即返回,`knowledgeMaintenance=QUEUED`;写 outbox 失败时 `NOT_QUEUED`(warning,不回滚 archive,总状态 CONDITIONAL)。后续由 `harness-sync` / `harness_knowledge.py maintain` 异步推进 outbox。**finalize 失败或 validate 报错时不删除原目录**。
119
+ 3. 运行 `python <skills-root>/scripts/harness_archive.py finalize --change-dir ".harness/changes/<change-name>" --archive-root ".harness/archive" --intent <release-candidate|record-only> --json`;读 JSON(cleanup、事件、移动、collect、render、validate、manifest 比对、archive-meta)。finalize 内部负责且仅负责一次 `phase.start` / `phase.end`,调用者不得重复追加。**finalize 不再同步执行知识维护**(§8.2):它写一个 `pending` maintenance-outbox 项即返回,`knowledgeMaintenance=QUEUED`;写 outbox 失败时 `NOT_QUEUED`(warning,不回滚 archive,总状态 CONDITIONAL)。后续由 `harness-sync` / `harness_knowledge.py maintain` 异步推进 outbox。**finalize 失败或 validate 报错时不删除原目录**。
115
120
 
116
121
  - **Read `reference.md`** — finalize 输出字段、archive-meta 格式、CONDITIONAL_OK 规则
117
122
  - **Read `templates/summary-data-template.json`** — summary-data 数据结构
@@ -187,6 +192,10 @@ finalize 内部负责且仅负责一次 `phase.start` / `phase.end`,并在移
187
192
 
188
193
  git 命令通过 `powershell.exe -Command "..."` 执行;archive-meta.md 和 final-summary.html 不得含明文 token/密码/密钥;归档报告必须区分 ✅真实成功 / 🟡跳过·静态验证 / ❌失败。
189
194
 
195
+ ### 十三、归档完整性与发布资格分离
196
+
197
+ `archiveIntegrity` 表示归档内容是否完整;`candidateVerification` 表示候选验证证据及保证级别;`releaseEligible` 只有两者均满足发布策略时才为 true。`record-only` 允许保留证据不足的历史事实,但绝不等价于 CI 绿灯或可发布。
198
+
190
199
  ## Output Format
191
200
 
192
201
  > 归档元数据格式见 `reference.md` 的 archive-meta 模板;最终报告由 `templates/summary-data-template.json` 数据结构 + `templates/render-summary.mjs` 固定脚本渲染。
@@ -14,6 +14,20 @@ description: harness-archive 的归档前检查项和归档后验证项。仅在
14
14
  - before/after checksum 不一致时,不得删除原目录。
15
15
  - 默认渲染器 `templates/render-summary.mjs`(finalize 内嵌调用)。
16
16
 
17
+ ### Wave-A 状态机与身份(IA-1 / IA-4)
18
+
19
+ 硬顺序:feature frozen → local gates → product candidate CI → **CI green** → merge → authoritative CI → archive product candidate → archive-only governance → release。
20
+
21
+ - [ ] `evidence/product-candidate-ci.json`(或等价 ledger 字段)`conclusion=success`,含 `runUrl` + `commit`;否则 `PRODUCT_CI_NOT_GREEN` 阻断
22
+ - [ ] summary/identity 含 `productCommit` / `productTreeHash` / `archiveCommit`;`productTreeHash` 排除 `.harness/**`
23
+ - [ ] 产品输入在 archive 后变化 → `ARCHIVE_EVIDENCE_REOPEN_REQUIRED`,旧 archive 不可作发布证据
24
+ - [ ] Manifest:coverage 字节在最终 snapshot 后不得静默漂移;后写报告必须 `exclusionReasons`,禁止假绿 `checksumStatus=OK`
25
+
26
+ ### Environment 合同(IA-3)
27
+
28
+ - [ ] 使用可写环境栈前:`harness_environment.py fingerprint` → `acquire` lease → run → `release`
29
+ - [ ] 租约落在主仓 `.harness/runtime/env-leases/`;跨 change 默认不得共享可写 volume
30
+
17
31
  ## 归档前检查(Phase 1)
18
32
 
19
33
  > ⚠️ **单一所有权**:finalize 内部负责且仅负责一次 `phase.start` / `phase.end`。归档前检查不得自行追加 archive 阶段边界。
@@ -42,6 +42,16 @@ const duration = (minutes) => {
42
42
  if (value < 60) return `${Math.round(value * 10) / 10} 分钟`;
43
43
  return `${Math.floor(value / 60)} 小时 ${Math.round(value % 60)} 分钟`;
44
44
  };
45
+ const durationMs = (ms) => {
46
+ const value = number(ms);
47
+ if (value <= 0) return "0 秒";
48
+ if (value < 1000) return `${value}ms`;
49
+ const seconds = value / 1000;
50
+ if (seconds < 60) return `${Math.round(seconds * 10) / 10} 秒`;
51
+ const minutes = seconds / 60;
52
+ if (minutes < 60) return `${Math.round(minutes * 10) / 10} 分钟`;
53
+ return `${Math.floor(minutes / 60)} 小时 ${Math.round(minutes % 60)} 分钟`;
54
+ };
45
55
 
46
56
  const stages = Object.entries(record(data.stageStatus));
47
57
  const verification = record(data.verification);
@@ -49,6 +59,17 @@ const unit = record(verification.unitTests);
49
59
  const api = record(verification.apiTests);
50
60
  const durations = record(data.durations);
51
61
  const durationStages = list(durations.stages);
62
+ const timing = record(data.timing);
63
+ const wallLabel = timing.workflowWallClockMs != null
64
+ ? durationMs(timing.workflowWallClockMs)
65
+ : (durations.totalLabel || duration(durations.totalMinutes));
66
+ const timingColumnsHtml = `
67
+ <article class="card" id="timingColumns"><h2>时长(三列)</h2>
68
+ <div class="row"><div><strong>活动执行</strong><small>stageActiveExecution</small></div><span>${esc(durationMs(timing.stageActiveExecutionMs))}</span></div>
69
+ <div class="row"><div><strong>阶段墙钟</strong><small>stageWallClockSpan</small></div><span>${esc(durationMs(timing.stageWallClockSpanMs))}</span></div>
70
+ <div class="row"><div><strong>全流程墙钟</strong><small>workflowWallClock</small></div><span>${esc(durationMs(timing.workflowWallClockMs))}</span></div>
71
+ <p><small>reportCutoffAt=<code id="reportCutoffAt">${esc(timing.reportCutoffAt || "N/A")}</code> · durations.totalMinutes 仅为活动执行,不冒充墙钟</small></p>
72
+ </article>`;
52
73
  const maxMinutes = Math.max(1, ...durationStages.map((item) => number(item.minutes)));
53
74
  const diff = record(data.diffStat);
54
75
  const risks = list(data.knownRisks);
@@ -86,12 +107,12 @@ const html = `<!doctype html><html lang="zh-CN"><head><meta charset="utf-8"><met
86
107
  @media(max-width:800px){.metrics{grid-template-columns:1fr 1fr}.grid,.risk{grid-template-columns:1fr}}@media(max-width:480px){main{width:calc(100% - 18px);margin-top:9px}.metrics{grid-template-columns:1fr}.hero{padding:19px}h1{font-size:23px}dl{grid-template-columns:1fr}}
87
108
  </style></head><body><main>
88
109
  <section class="hero"><div class="eyebrow">HARNESS EXECUTION REPORT</div><h1>${esc(data.changeName || "未命名变更")}</h1><p class="goal">${esc(data.businessGoal || "未记录业务目标")}</p><div class="status">${pill(data.finalStatus)}<span>基于事件、验证账本与 Git 证据生成${data.riskTier ? ` · riskTier=${esc(data.riskTier)}` : ""}</span></div>${reasonHtml ? `<div class="status">${reasonHtml}</div>` : ""}</section>
89
- <section class="metrics"><article class="metric"><small>任务提交</small><strong><code title="${esc(data.finalCommit)}">${esc(shortHash(data.finalCommit))}</code></strong><small>${esc(data.finalCommitBranch || "未记录分支")}</small></article><article class="metric"><small>代码范围</small><strong>${number(diff.filesChanged)} 个文件</strong><small><span class="plus">+${number(diff.insertions)}</span> · <span class="minus">-${number(diff.deletions)}</span></small></article><article class="metric"><small>总耗时</small><strong>${esc(durations.totalLabel || duration(durations.totalMinutes))}</strong><small>${durationStages.length} 个阶段</small></article><article class="metric"><small>归档完整性</small><strong>${esc(record(data.archiveManifest).checksumStatus || "UNKNOWN")}</strong><small>${number(record(data.archiveManifest).totalArchiveFiles)} 个归档文件</small></article></section>
90
- <section class="grid"><div><article class="card"><h2>验证结论</h2>${verificationHtml}</article><article class="card"><h2>阶段耗时</h2>${durationHtml}</article></div><div><article class="card"><h2>阶段状态</h2>${stageHtml}</article><article class="card"><h2>风险与后续</h2><div class="risk"><div><h3>已知风险</h3><ul>${riskHtml}</ul></div><div><h3>人工动作</h3><ul>${actionHtml}</ul></div></div></article></div></section>
110
+ <section class="metrics"><article class="metric"><small>产品提交</small><strong><code title="${esc(data.productCommit || data.finalCommit)}">${esc(shortHash(data.productCommit || data.finalCommit))}</code></strong><small>archive=${esc(shortHash(data.archiveCommit || data.finalCommit))}</small></article><article class="metric"><small>代码范围</small><strong>${number(diff.filesChanged)} 个文件</strong><small><span class="plus">+${number(diff.insertions)}</span> · <span class="minus">-${number(diff.deletions)}</span></small></article><article class="metric"><small>全流程墙钟</small><strong>${esc(wallLabel)}</strong><small>活动=${esc(durationMs(timing.stageActiveExecutionMs))} · ${durationStages.length} 阶段</small></article><article class="metric"><small>归档完整性</small><strong>${esc(record(data.archiveManifest).checksumStatus || "UNKNOWN")}</strong><small>${number(record(data.archiveManifest).totalArchiveFiles)} 个归档文件</small></article></section>
111
+ <section class="grid"><div><article class="card"><h2>验证结论</h2>${verificationHtml}</article><article class="card"><h2>阶段耗时(活动)</h2>${durationHtml}</article>${timingColumnsHtml}</div><div><article class="card"><h2>阶段状态</h2>${stageHtml}</article><article class="card"><h2>风险与后续</h2><div class="risk"><div><h3>已知风险</h3><ul>${riskHtml}</ul></div><div><h3>人工动作</h3><ul>${actionHtml}</ul></div></div></article></div></section>
91
112
  <details><summary>变更文件(${files.length})</summary><div><table><thead><tr><th>文件</th><th>新增</th><th>删除</th></tr></thead><tbody>${fileRows}</tbody></table></div></details>
92
113
  <details><summary>执行时间线与工具交接(${timeline.length})</summary><div><table><thead><tr><th>阶段</th><th>尝试</th><th>状态</th><th>来源 / 摘要</th></tr></thead><tbody>${timelineRows}</tbody></table></div></details>
93
114
  <details><summary>命令证据(${commands.length})</summary><div><table><thead><tr><th>阶段</th><th>命令</th><th>结果</th></tr></thead><tbody>${commandRows}</tbody></table></div></details>
94
- <details><summary>技术元数据</summary><div><dl><dt>完整提交</dt><dd><code>${esc(data.finalCommit || "N/A")}</code></dd><dt>基线提交</dt><dd><code>${esc(data.baseCommit || "N/A")}</code></dd><dt>Git 范围</dt><dd><code>${esc(diff.range || "N/A")}</code></dd><dt>报告数据版本</dt><dd>${esc(data.schemaVersion || "N/A")}</dd><dt>事实来源</dt><dd>${esc(list(record(data.reportPipeline).sources).join(" · ") || "N/A")}</dd></dl></div></details>
115
+ <details><summary>技术元数据</summary><div><dl><dt>产品提交</dt><dd><code>${esc(data.productCommit || data.finalCommit || "N/A")}</code></dd><dt>产品树哈希</dt><dd><code>${esc(data.productTreeHash || "N/A")}</code></dd><dt>归档提交</dt><dd><code>${esc(data.archiveCommit || data.finalCommit || "N/A")}</code></dd><dt>基线提交</dt><dd><code>${esc(data.baseCommit || "N/A")}</code></dd><dt>Git 范围</dt><dd><code>${esc(diff.range || "N/A")}</code></dd><dt>报告数据版本</dt><dd>${esc(data.schemaVersion || "N/A")}</dd><dt>事实来源</dt><dd>${esc(list(record(data.reportPipeline).sources).join(" · ") || "N/A")}</dd></dl></div></details>
95
116
  </main></body></html>`;
96
117
 
97
118
  fs.mkdirSync(path.dirname(outPath), { recursive: true });
@@ -3,6 +3,17 @@
3
3
  "changeName": "<change-name>",
4
4
  "businessGoal": "本次变更为了做什么",
5
5
  "finalStatus": "OK | WARN | CONDITIONAL_OK | FAIL",
6
+ "archiveIntent": "release-candidate | record-only",
7
+ "archiveIntegrity": {
8
+ "ok": true
9
+ },
10
+ "candidateVerification": {
11
+ "ok": false,
12
+ "code": "PRODUCT_CANDIDATE_VERIFIED | PRODUCT_CANDIDATE_NOT_VERIFIED | PRODUCT_CI_GREEN | PRODUCT_CI_NOT_GREEN",
13
+ "assurance": "local-reproducible | remote-claimed | remote-attested",
14
+ "message": ""
15
+ },
16
+ "releaseEligible": false,
6
17
  "finalCommit": "<commit-hash>",
7
18
  "finalCommitBranch": "<origin/branch>",
8
19
  "baseCommit": "<base-commit-hash>",
@@ -20,7 +20,7 @@ disallowed-tools:
20
20
  - Bash(awk *)
21
21
  - Bash(curl *)
22
22
  ---
23
- <!-- generated by harness_deploy.py; core=a9c096220b88ca56; overlay=java; agent=claude-code; do not edit -->
23
+ <!-- generated by harness_deploy.py; core=506c7d9df54c0f80; overlay=java; agent=claude-code; do not edit -->
24
24
  # harness-codebase-map — 代码库地图
25
25
 
26
26
  ## Purpose
@@ -20,7 +20,7 @@ disallowed-tools:
20
20
  - Bash(awk *)
21
21
  - Bash(curl *)
22
22
  ---
23
- <!-- generated by harness_deploy.py; core=a9c096220b88ca56; overlay=java; agent=claude-code; do not edit -->
23
+ <!-- generated by harness_deploy.py; core=506c7d9df54c0f80; overlay=java; agent=claude-code; do not edit -->
24
24
  # harness-knowledge-ingest
25
25
 
26
26
  从 `.harness/archive/**/reports/final/summary-data.json` 抽取项目知识,生成并维护本地 `.harness/knowledge/` 索引。
@@ -20,7 +20,7 @@ disallowed-tools:
20
20
  - Bash(awk *)
21
21
  - Bash(curl *)
22
22
  ---
23
- <!-- generated by harness_deploy.py; core=a9c096220b88ca56; overlay=java; agent=claude-code; do not edit -->
23
+ <!-- generated by harness_deploy.py; core=506c7d9df54c0f80; overlay=java; agent=claude-code; do not edit -->
24
24
  # harness-knowledge-query
25
25
 
26
26
  并行 change 中查询必须传 `--change <change-id>`;成功后以该 change 的 `meta/knowledge-context.json` 为稳定指针。全局 `context-packs/latest.json` 仅向后兼容,不得用于判断多个 active change 的上下文归属。
@@ -20,7 +20,7 @@ disallowed-tools:
20
20
  - Bash(awk *)
21
21
  - Bash(curl *)
22
22
  ---
23
- <!-- generated by harness_deploy.py; core=a9c096220b88ca56; overlay=java; agent=claude-code; do not edit -->
23
+ <!-- generated by harness_deploy.py; core=506c7d9df54c0f80; overlay=java; agent=claude-code; do not edit -->
24
24
  # harness-package — 增量模块打包(Java overlay)
25
25
 
26
26
  ## Purpose
@@ -20,7 +20,7 @@ disallowed-tools:
20
20
  - Bash(awk *)
21
21
  - Bash(curl *)
22
22
  ---
23
- <!-- generated by harness_deploy.py; core=a9c096220b88ca56; overlay=java; agent=claude-code; do not edit -->
23
+ <!-- generated by harness_deploy.py; core=506c7d9df54c0f80; overlay=java; agent=claude-code; do not edit -->
24
24
  # harness-plan — 需求规划
25
25
 
26
26
  ## Purpose
@@ -109,6 +109,7 @@ change-name 范围变更 → 提示重命名或记 🟡WARN(→ `reference.md`
109
109
  | 歧义优先检查 | 否定、对比、动作对象或范围存在多种合理解释时,最小取证后先给推荐理解并一次一问;确认前不深挖错误方向 |
110
110
  | 简单修复探索预算 | 预计不超过 2 个代码文件、且不涉及认证/安全/迁移/并发/API 契约重设时,最多 1 次合并 CodeGraph 查询 + 1 次定向补查、1 个用户澄清问题;无关发现只记非阻断说明 |
111
111
  | 精简产物 | 简单修复只保留实现所需的设计、任务、边界和测试;禁止在 spec/plan/detail/scenarios 中重复同一背景和结论 |
112
+ | 测试执行成本 | 场景表必须设计快速反馈层级、预计时长、资源预算、超时和可复用证据;默认先跑受影响测试,再跑模块门禁,候选验证只复用身份一致的全量证据 |
112
113
  | state snapshot | 读取 `state-snapshot.json`(`harness_state.py` / state-layout-protocol §state-snapshot.json)了解 project/worktree root、HEAD/base、profile/rules/map/knowledge 指纹;失效由脚本刷新,**不得仅凭缓存跳过代码探索或验证门禁**(design §3.6) |
113
114
  | 协议 | sensitive-info / evidence-based-reporting / state-layout |
114
115