@hunter-harness/workflow-harness 0.2.8 → 0.2.9

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 (222) hide show
  1. package/harness/bundles/general/claude-code/.harness-build.json +1 -1
  2. package/harness/bundles/general/claude-code/contracts/workflow-policy.json +3 -3
  3. package/harness/bundles/general/claude-code/harness-archive/SKILL.md +3 -3
  4. package/harness/bundles/general/claude-code/harness-archive/reference.md +18 -10
  5. package/harness/bundles/general/claude-code/harness-archive/templates/render-summary.mjs +5 -1
  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 +1 -1
  10. package/harness/bundles/general/claude-code/harness-plan/checklist.md +6 -0
  11. package/harness/bundles/general/claude-code/harness-review/SKILL.md +1 -1
  12. package/harness/bundles/general/claude-code/harness-run/SKILL.md +1 -1
  13. package/harness/bundles/general/claude-code/harness-run/checklist.md +5 -0
  14. package/harness/bundles/general/claude-code/harness-run/reference.md +31 -1
  15. package/harness/bundles/general/claude-code/harness-submit/SKILL.md +1 -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 +9 -0
  19. package/harness/bundles/general/claude-code/harness-test/pitfalls-java.md +14 -0
  20. package/harness/bundles/general/claude-code/harness-test/reference.md +36 -0
  21. package/harness/bundles/general/claude-code/protocols/archive-report-protocol.md +3 -1
  22. package/harness/bundles/general/claude-code/protocols/ledger-protocol.md +2 -0
  23. package/harness/bundles/general/claude-code/protocols/powershell-protocol.md +16 -0
  24. package/harness/bundles/general/claude-code/protocols/report-pipeline-protocol.md +17 -0
  25. package/harness/bundles/general/claude-code/scripts/harness_archive.py +568 -58
  26. package/harness/bundles/general/claude-code/scripts/harness_events.py +70 -19
  27. package/harness/bundles/general/claude-code/scripts/harness_gate.py +178 -25
  28. package/harness/bundles/general/claude-code/scripts/harness_ledger.py +17 -0
  29. package/harness/bundles/general/codebuddy/.harness-build.json +1 -1
  30. package/harness/bundles/general/codebuddy/contracts/workflow-policy.json +3 -3
  31. package/harness/bundles/general/codebuddy/harness-archive/SKILL.md +3 -3
  32. package/harness/bundles/general/codebuddy/harness-archive/reference.md +18 -10
  33. package/harness/bundles/general/codebuddy/harness-archive/templates/render-summary.mjs +5 -1
  34. package/harness/bundles/general/codebuddy/harness-codebase-map/SKILL.md +1 -1
  35. package/harness/bundles/general/codebuddy/harness-knowledge-ingest/SKILL.md +1 -1
  36. package/harness/bundles/general/codebuddy/harness-knowledge-query/SKILL.md +1 -1
  37. package/harness/bundles/general/codebuddy/harness-plan/SKILL.md +1 -1
  38. package/harness/bundles/general/codebuddy/harness-plan/checklist.md +6 -0
  39. package/harness/bundles/general/codebuddy/harness-review/SKILL.md +1 -1
  40. package/harness/bundles/general/codebuddy/harness-run/SKILL.md +1 -1
  41. package/harness/bundles/general/codebuddy/harness-run/checklist.md +5 -0
  42. package/harness/bundles/general/codebuddy/harness-run/reference.md +31 -1
  43. package/harness/bundles/general/codebuddy/harness-submit/SKILL.md +1 -1
  44. package/harness/bundles/general/codebuddy/harness-sync/SKILL.md +1 -1
  45. package/harness/bundles/general/codebuddy/harness-test/SKILL.md +1 -1
  46. package/harness/bundles/general/codebuddy/harness-test/checklist.md +9 -0
  47. package/harness/bundles/general/codebuddy/harness-test/pitfalls-java.md +14 -0
  48. package/harness/bundles/general/codebuddy/harness-test/reference.md +36 -0
  49. package/harness/bundles/general/codebuddy/protocols/archive-report-protocol.md +3 -1
  50. package/harness/bundles/general/codebuddy/protocols/ledger-protocol.md +2 -0
  51. package/harness/bundles/general/codebuddy/protocols/powershell-protocol.md +16 -0
  52. package/harness/bundles/general/codebuddy/protocols/report-pipeline-protocol.md +17 -0
  53. package/harness/bundles/general/codebuddy/scripts/harness_archive.py +568 -58
  54. package/harness/bundles/general/codebuddy/scripts/harness_events.py +70 -19
  55. package/harness/bundles/general/codebuddy/scripts/harness_gate.py +178 -25
  56. package/harness/bundles/general/codebuddy/scripts/harness_ledger.py +17 -0
  57. package/harness/bundles/general/codex/.harness-build.json +1 -1
  58. package/harness/bundles/general/codex/contracts/workflow-policy.json +3 -3
  59. package/harness/bundles/general/codex/harness-archive/SKILL.md +3 -3
  60. package/harness/bundles/general/codex/harness-archive/reference.md +18 -10
  61. package/harness/bundles/general/codex/harness-archive/templates/render-summary.mjs +5 -1
  62. package/harness/bundles/general/codex/harness-codebase-map/SKILL.md +1 -1
  63. package/harness/bundles/general/codex/harness-knowledge-ingest/SKILL.md +1 -1
  64. package/harness/bundles/general/codex/harness-knowledge-query/SKILL.md +1 -1
  65. package/harness/bundles/general/codex/harness-plan/SKILL.md +1 -1
  66. package/harness/bundles/general/codex/harness-plan/checklist.md +6 -0
  67. package/harness/bundles/general/codex/harness-review/SKILL.md +1 -1
  68. package/harness/bundles/general/codex/harness-run/SKILL.md +1 -1
  69. package/harness/bundles/general/codex/harness-run/checklist.md +5 -0
  70. package/harness/bundles/general/codex/harness-run/reference.md +31 -1
  71. package/harness/bundles/general/codex/harness-submit/SKILL.md +1 -1
  72. package/harness/bundles/general/codex/harness-sync/SKILL.md +1 -1
  73. package/harness/bundles/general/codex/harness-test/SKILL.md +1 -1
  74. package/harness/bundles/general/codex/harness-test/checklist.md +9 -0
  75. package/harness/bundles/general/codex/harness-test/pitfalls-java.md +14 -0
  76. package/harness/bundles/general/codex/harness-test/reference.md +36 -0
  77. package/harness/bundles/general/codex/protocols/archive-report-protocol.md +3 -1
  78. package/harness/bundles/general/codex/protocols/ledger-protocol.md +2 -0
  79. package/harness/bundles/general/codex/protocols/powershell-protocol.md +16 -0
  80. package/harness/bundles/general/codex/protocols/report-pipeline-protocol.md +17 -0
  81. package/harness/bundles/general/codex/scripts/harness_archive.py +568 -58
  82. package/harness/bundles/general/codex/scripts/harness_events.py +70 -19
  83. package/harness/bundles/general/codex/scripts/harness_gate.py +178 -25
  84. package/harness/bundles/general/codex/scripts/harness_ledger.py +17 -0
  85. package/harness/bundles/general/cursor/.harness-build.json +1 -1
  86. package/harness/bundles/general/cursor/contracts/workflow-policy.json +3 -3
  87. package/harness/bundles/general/cursor/harness-archive/SKILL.md +3 -3
  88. package/harness/bundles/general/cursor/harness-archive/reference.md +18 -10
  89. package/harness/bundles/general/cursor/harness-archive/templates/render-summary.mjs +5 -1
  90. package/harness/bundles/general/cursor/harness-codebase-map/SKILL.md +1 -1
  91. package/harness/bundles/general/cursor/harness-knowledge-ingest/SKILL.md +1 -1
  92. package/harness/bundles/general/cursor/harness-knowledge-query/SKILL.md +1 -1
  93. package/harness/bundles/general/cursor/harness-plan/SKILL.md +1 -1
  94. package/harness/bundles/general/cursor/harness-plan/checklist.md +6 -0
  95. package/harness/bundles/general/cursor/harness-review/SKILL.md +1 -1
  96. package/harness/bundles/general/cursor/harness-run/SKILL.md +1 -1
  97. package/harness/bundles/general/cursor/harness-run/checklist.md +5 -0
  98. package/harness/bundles/general/cursor/harness-run/reference.md +31 -1
  99. package/harness/bundles/general/cursor/harness-submit/SKILL.md +1 -1
  100. package/harness/bundles/general/cursor/harness-sync/SKILL.md +1 -1
  101. package/harness/bundles/general/cursor/harness-test/SKILL.md +1 -1
  102. package/harness/bundles/general/cursor/harness-test/checklist.md +9 -0
  103. package/harness/bundles/general/cursor/harness-test/pitfalls-java.md +14 -0
  104. package/harness/bundles/general/cursor/harness-test/reference.md +36 -0
  105. package/harness/bundles/general/cursor/protocols/archive-report-protocol.md +3 -1
  106. package/harness/bundles/general/cursor/protocols/ledger-protocol.md +2 -0
  107. package/harness/bundles/general/cursor/protocols/powershell-protocol.md +16 -0
  108. package/harness/bundles/general/cursor/protocols/report-pipeline-protocol.md +17 -0
  109. package/harness/bundles/general/cursor/scripts/harness_archive.py +568 -58
  110. package/harness/bundles/general/cursor/scripts/harness_events.py +70 -19
  111. package/harness/bundles/general/cursor/scripts/harness_gate.py +178 -25
  112. package/harness/bundles/general/cursor/scripts/harness_ledger.py +17 -0
  113. package/harness/bundles/java/claude-code/.harness-build.json +1 -1
  114. package/harness/bundles/java/claude-code/contracts/workflow-policy.json +3 -3
  115. package/harness/bundles/java/claude-code/harness-apidoc/SKILL.md +1 -1
  116. package/harness/bundles/java/claude-code/harness-archive/SKILL.md +3 -3
  117. package/harness/bundles/java/claude-code/harness-archive/reference.md +18 -10
  118. package/harness/bundles/java/claude-code/harness-archive/templates/render-summary.mjs +5 -1
  119. package/harness/bundles/java/claude-code/harness-codebase-map/SKILL.md +1 -1
  120. package/harness/bundles/java/claude-code/harness-knowledge-ingest/SKILL.md +1 -1
  121. package/harness/bundles/java/claude-code/harness-knowledge-query/SKILL.md +1 -1
  122. package/harness/bundles/java/claude-code/harness-package/SKILL.md +1 -1
  123. package/harness/bundles/java/claude-code/harness-plan/SKILL.md +1 -1
  124. package/harness/bundles/java/claude-code/harness-plan/checklist.md +6 -0
  125. package/harness/bundles/java/claude-code/harness-review/SKILL.md +1 -1
  126. package/harness/bundles/java/claude-code/harness-run/SKILL.md +1 -1
  127. package/harness/bundles/java/claude-code/harness-submit/SKILL.md +1 -1
  128. package/harness/bundles/java/claude-code/harness-sync/SKILL.md +1 -1
  129. package/harness/bundles/java/claude-code/harness-test/SKILL.md +1 -1
  130. package/harness/bundles/java/claude-code/protocols/archive-report-protocol.md +3 -1
  131. package/harness/bundles/java/claude-code/protocols/ledger-protocol.md +2 -0
  132. package/harness/bundles/java/claude-code/protocols/powershell-protocol.md +16 -0
  133. package/harness/bundles/java/claude-code/protocols/report-pipeline-protocol.md +17 -0
  134. package/harness/bundles/java/claude-code/scripts/harness_archive.py +568 -58
  135. package/harness/bundles/java/claude-code/scripts/harness_events.py +70 -19
  136. package/harness/bundles/java/claude-code/scripts/harness_gate.py +178 -25
  137. package/harness/bundles/java/claude-code/scripts/harness_ledger.py +17 -0
  138. package/harness/bundles/java/codebuddy/.harness-build.json +1 -1
  139. package/harness/bundles/java/codebuddy/contracts/workflow-policy.json +3 -3
  140. package/harness/bundles/java/codebuddy/harness-apidoc/SKILL.md +1 -1
  141. package/harness/bundles/java/codebuddy/harness-archive/SKILL.md +3 -3
  142. package/harness/bundles/java/codebuddy/harness-archive/reference.md +18 -10
  143. package/harness/bundles/java/codebuddy/harness-archive/templates/render-summary.mjs +5 -1
  144. package/harness/bundles/java/codebuddy/harness-codebase-map/SKILL.md +1 -1
  145. package/harness/bundles/java/codebuddy/harness-knowledge-ingest/SKILL.md +1 -1
  146. package/harness/bundles/java/codebuddy/harness-knowledge-query/SKILL.md +1 -1
  147. package/harness/bundles/java/codebuddy/harness-package/SKILL.md +1 -1
  148. package/harness/bundles/java/codebuddy/harness-plan/SKILL.md +1 -1
  149. package/harness/bundles/java/codebuddy/harness-plan/checklist.md +6 -0
  150. package/harness/bundles/java/codebuddy/harness-review/SKILL.md +1 -1
  151. package/harness/bundles/java/codebuddy/harness-run/SKILL.md +1 -1
  152. package/harness/bundles/java/codebuddy/harness-submit/SKILL.md +1 -1
  153. package/harness/bundles/java/codebuddy/harness-sync/SKILL.md +1 -1
  154. package/harness/bundles/java/codebuddy/harness-test/SKILL.md +1 -1
  155. package/harness/bundles/java/codebuddy/protocols/archive-report-protocol.md +3 -1
  156. package/harness/bundles/java/codebuddy/protocols/ledger-protocol.md +2 -0
  157. package/harness/bundles/java/codebuddy/protocols/powershell-protocol.md +16 -0
  158. package/harness/bundles/java/codebuddy/protocols/report-pipeline-protocol.md +17 -0
  159. package/harness/bundles/java/codebuddy/scripts/harness_archive.py +568 -58
  160. package/harness/bundles/java/codebuddy/scripts/harness_events.py +70 -19
  161. package/harness/bundles/java/codebuddy/scripts/harness_gate.py +178 -25
  162. package/harness/bundles/java/codebuddy/scripts/harness_ledger.py +17 -0
  163. package/harness/bundles/java/codex/.harness-build.json +1 -1
  164. package/harness/bundles/java/codex/contracts/workflow-policy.json +3 -3
  165. package/harness/bundles/java/codex/harness-apidoc/SKILL.md +1 -1
  166. package/harness/bundles/java/codex/harness-archive/SKILL.md +3 -3
  167. package/harness/bundles/java/codex/harness-archive/reference.md +18 -10
  168. package/harness/bundles/java/codex/harness-archive/templates/render-summary.mjs +5 -1
  169. package/harness/bundles/java/codex/harness-codebase-map/SKILL.md +1 -1
  170. package/harness/bundles/java/codex/harness-knowledge-ingest/SKILL.md +1 -1
  171. package/harness/bundles/java/codex/harness-knowledge-query/SKILL.md +1 -1
  172. package/harness/bundles/java/codex/harness-package/SKILL.md +1 -1
  173. package/harness/bundles/java/codex/harness-plan/SKILL.md +1 -1
  174. package/harness/bundles/java/codex/harness-plan/checklist.md +6 -0
  175. package/harness/bundles/java/codex/harness-review/SKILL.md +1 -1
  176. package/harness/bundles/java/codex/harness-run/SKILL.md +1 -1
  177. package/harness/bundles/java/codex/harness-submit/SKILL.md +1 -1
  178. package/harness/bundles/java/codex/harness-sync/SKILL.md +1 -1
  179. package/harness/bundles/java/codex/harness-test/SKILL.md +1 -1
  180. package/harness/bundles/java/codex/protocols/archive-report-protocol.md +3 -1
  181. package/harness/bundles/java/codex/protocols/ledger-protocol.md +2 -0
  182. package/harness/bundles/java/codex/protocols/powershell-protocol.md +16 -0
  183. package/harness/bundles/java/codex/protocols/report-pipeline-protocol.md +17 -0
  184. package/harness/bundles/java/codex/scripts/harness_archive.py +568 -58
  185. package/harness/bundles/java/codex/scripts/harness_events.py +70 -19
  186. package/harness/bundles/java/codex/scripts/harness_gate.py +178 -25
  187. package/harness/bundles/java/codex/scripts/harness_ledger.py +17 -0
  188. package/harness/bundles/java/cursor/.harness-build.json +1 -1
  189. package/harness/bundles/java/cursor/contracts/workflow-policy.json +3 -3
  190. package/harness/bundles/java/cursor/harness-apidoc/SKILL.md +1 -1
  191. package/harness/bundles/java/cursor/harness-archive/SKILL.md +3 -3
  192. package/harness/bundles/java/cursor/harness-archive/reference.md +18 -10
  193. package/harness/bundles/java/cursor/harness-archive/templates/render-summary.mjs +5 -1
  194. package/harness/bundles/java/cursor/harness-codebase-map/SKILL.md +1 -1
  195. package/harness/bundles/java/cursor/harness-knowledge-ingest/SKILL.md +1 -1
  196. package/harness/bundles/java/cursor/harness-knowledge-query/SKILL.md +1 -1
  197. package/harness/bundles/java/cursor/harness-package/SKILL.md +1 -1
  198. package/harness/bundles/java/cursor/harness-plan/SKILL.md +1 -1
  199. package/harness/bundles/java/cursor/harness-plan/checklist.md +6 -0
  200. package/harness/bundles/java/cursor/harness-review/SKILL.md +1 -1
  201. package/harness/bundles/java/cursor/harness-run/SKILL.md +1 -1
  202. package/harness/bundles/java/cursor/harness-submit/SKILL.md +1 -1
  203. package/harness/bundles/java/cursor/harness-sync/SKILL.md +1 -1
  204. package/harness/bundles/java/cursor/harness-test/SKILL.md +1 -1
  205. package/harness/bundles/java/cursor/protocols/archive-report-protocol.md +3 -1
  206. package/harness/bundles/java/cursor/protocols/ledger-protocol.md +2 -0
  207. package/harness/bundles/java/cursor/protocols/powershell-protocol.md +16 -0
  208. package/harness/bundles/java/cursor/protocols/report-pipeline-protocol.md +17 -0
  209. package/harness/bundles/java/cursor/scripts/harness_archive.py +568 -58
  210. package/harness/bundles/java/cursor/scripts/harness_events.py +70 -19
  211. package/harness/bundles/java/cursor/scripts/harness_gate.py +178 -25
  212. package/harness/bundles/java/cursor/scripts/harness_ledger.py +17 -0
  213. package/harness/manifests/general/claude-code.json +32 -28
  214. package/harness/manifests/general/codebuddy.json +32 -28
  215. package/harness/manifests/general/codex.json +32 -28
  216. package/harness/manifests/general/cursor.json +32 -28
  217. package/harness/manifests/java/claude-code.json +26 -26
  218. package/harness/manifests/java/codebuddy.json +26 -26
  219. package/harness/manifests/java/codex.json +26 -26
  220. package/harness/manifests/java/cursor.json +26 -26
  221. package/hunter-workflow-family.json +2 -2
  222. package/package.json +1 -1
@@ -43,6 +43,7 @@ EVENT_TYPES = frozenset(
43
43
  )
44
44
 
45
45
  SCHEMA_VERSION = 3
46
+ _NOTE_FALLBACK_MAXLEN = 60
46
47
  HEADER_LINE = (
47
48
  "本文件由 harness_events.py 自动渲染,请勿手工编辑;事实源为 events.ndjson"
48
49
  )
@@ -316,6 +317,8 @@ def build_event(args: argparse.Namespace, existing: list[dict[str, Any]]) -> dic
316
317
  def status_symbol(status: Any, reason: Any = None) -> str:
317
318
  text = str(status or "").strip().lower()
318
319
  reason_text = str(reason or "").strip()
320
+ if not text:
321
+ return "—"
319
322
  ok_set = {"ok", "passed", "pass", "success", "green", "✅", "✅ok"}
320
323
  warn_set = {"warn", "warning", "yellow", "skipped", "skip", "🟡", "🟡warn"}
321
324
  fail_set = {"fail", "failed", "error", "red", "blocked", "❌", "❌fail"}
@@ -327,7 +330,7 @@ def status_symbol(status: Any, reason: Any = None) -> str:
327
330
  return f"❌FAIL({reason_text})" if reason_text else "❌FAIL"
328
331
  if reason_text:
329
332
  return f"{status}({reason_text})"
330
- return str(status or "unknown")
333
+ return str(status)
331
334
 
332
335
 
333
336
  def severity_symbol(severity: Any, message: Any = None) -> str:
@@ -339,7 +342,10 @@ def severity_symbol(severity: Any, message: Any = None) -> str:
339
342
  return f"🟡WARN({msg})" if msg else "🟡WARN"
340
343
  if text in {"info", "ok", "note"}:
341
344
  return f"✅OK({msg})" if msg else "✅OK"
342
- return f"{severity}: {msg}" if msg else str(severity or "issue")
345
+ # Empty/unknown severity: never emit literal "issue"/"None".
346
+ if not text:
347
+ return ""
348
+ return f"{severity}: {msg}" if msg else str(severity)
343
349
 
344
350
 
345
351
  def format_duration(ms: int | None) -> str:
@@ -440,35 +446,54 @@ def phase_duration_ms(phase_events: list[dict[str, Any]]) -> int | None:
440
446
  def render_command_block(commands: list[dict[str, Any]]) -> list[str]:
441
447
  if not commands:
442
448
  return []
449
+
450
+ def command_display(event: dict[str, Any]) -> str:
451
+ cmd = str(event.get("command") or "").strip()
452
+ if cmd:
453
+ return cmd
454
+ return str(event.get("note") or "").strip()
455
+
443
456
  if len(commands) == 1:
444
457
  event = commands[0]
445
- cmd = event.get("command") or ""
458
+ display = command_display(event)
459
+ if not display:
460
+ return []
446
461
  exit_code = event.get("exit_code")
447
462
  exit_text = "?" if exit_code is None else str(exit_code)
448
463
  duration = format_duration(
449
464
  int(event["duration_ms"]) if isinstance(event.get("duration_ms"), int) else None
450
465
  )
451
466
  note = str(event.get("note") or "").strip()
452
- parts = [f"- command: `{cmd}`", f"exit={exit_text}", f"duration={duration}"]
453
- if note:
467
+ parts = [f"- command: `{display}`", f"exit={exit_text}", f"duration={duration}"]
468
+ # Avoid duplicating note when it was already used as the display text.
469
+ if note and note != display:
454
470
  parts.append(f"note={note}")
455
471
  return [" · ".join(parts)]
456
472
 
457
- lines = [
458
- "",
459
- "| 命令 | exit | duration | note |",
460
- "| --- | ---: | ---: | --- |",
461
- ]
473
+ rows: list[str] = []
462
474
  for event in commands:
463
- cmd = str(event.get("command") or "").replace("|", "\\|")
475
+ display = command_display(event)
476
+ if not display:
477
+ continue
478
+ cmd = display.replace("|", "\\|")
464
479
  exit_code = event.get("exit_code")
465
480
  exit_text = "?" if exit_code is None else str(exit_code)
466
481
  duration = format_duration(
467
482
  int(event["duration_ms"]) if isinstance(event.get("duration_ms"), int) else None
468
483
  )
469
484
  note = str(event.get("note") or "").replace("|", "\\|")
470
- lines.append(f"| `{cmd}` | {exit_text} | {duration} | {note} |")
471
- lines.append("")
485
+ # If note was promoted to the command cell, leave the note column empty.
486
+ note_cell = "" if note == display else note
487
+ rows.append(f"| `{cmd}` | {exit_text} | {duration} | {note_cell} |")
488
+ if not rows:
489
+ return []
490
+ lines = [
491
+ "",
492
+ "| 命令 | exit | duration | note |",
493
+ "| --- | ---: | ---: | --- |",
494
+ *rows,
495
+ "",
496
+ ]
472
497
  return lines
473
498
 
474
499
 
@@ -491,23 +516,37 @@ def render_event_line(event: dict[str, Any]) -> list[str]:
491
516
  suffix = f" — {note}" if note else ""
492
517
  return [f"- phase.end @ {event.get('timestamp', '')}{suffix}"]
493
518
  if etype == "verification":
494
- name = event.get("name") or "(unnamed)"
495
- symbol = status_symbol(event.get("status"), event.get("reason") or event.get("note"))
519
+ note = str(event.get("note") or "").strip()
520
+ name = str(event.get("name") or "").strip() or (
521
+ note[:_NOTE_FALLBACK_MAXLEN] if note else ""
522
+ )
523
+ if not name:
524
+ return []
525
+ symbol = status_symbol(event.get("status"), event.get("reason"))
496
526
  return [f"- verification: {name} → {symbol}"]
497
527
  if etype == "decision":
498
- decision = event.get("decision") or ""
499
- reason = event.get("reason") or event.get("note") or ""
528
+ decision = str(event.get("decision") or "").strip()
529
+ reason = str(event.get("reason") or event.get("note") or "").strip()
530
+ if not decision:
531
+ return [f"- decision: {reason}"] if reason else []
500
532
  if reason:
501
533
  return [f"- decision: {decision} — {reason}"]
502
534
  return [f"- decision: {decision}"]
503
535
  if etype == "issue":
504
- symbol = severity_symbol(event.get("severity"), event.get("message"))
536
+ note = str(event.get("note") or "").strip()
537
+ message = str(event.get("message") or "").strip() or note
538
+ symbol = severity_symbol(event.get("severity"), message)
539
+ if not symbol:
540
+ return [f"- issue: {note}"] if note else []
505
541
  code = event.get("code")
506
542
  prefix = f"[{code}] " if code else ""
507
543
  return [f"- issue: {prefix}{symbol}"]
508
544
  if etype == "artifact":
509
- path = event.get("path") or ""
545
+ path = str(event.get("path") or "").strip()
546
+ note = str(event.get("note") or "").strip()
510
547
  kind = event.get("kind")
548
+ if not path:
549
+ return [f"- artifact: {note}"] if note else []
511
550
  if kind:
512
551
  return [f"- artifact: `{path}` ({kind})"]
513
552
  return [f"- artifact: `{path}`"]
@@ -653,6 +692,18 @@ def cmd_append(args: argparse.Namespace) -> int:
653
692
  # new_event_id 用完整 uuid,无需扫描去重。锁覆盖 atomic_append_line 的
654
693
  # open/write/flush/fsync 全过程。
655
694
  event = build_event(args, [])
695
+ # Semantic hints (non-fatal): guide agents toward required fields.
696
+ if args.type == "issue" and not args.severity:
697
+ event["severity"] = "info"
698
+ print(
699
+ "warning: issue without --severity, defaulted to info",
700
+ file=sys.stderr,
701
+ )
702
+ if args.type == "verification" and (not args.name or not args.status):
703
+ print(
704
+ "warning: verification missing --name or --status",
705
+ file=sys.stderr,
706
+ )
656
707
  line = json.dumps(event, ensure_ascii=False, separators=(",", ":"))
657
708
  try:
658
709
  with event_file_lock(lock_path):
@@ -149,7 +149,8 @@ def checkpoint_status(checkpoints: dict[str, Any] | None, checkpoint_id: str) ->
149
149
  def foundation_gate_blocks(task_number: int | None, change_dir: Path) -> dict[str, Any] | None:
150
150
  checkpoints = load_checkpoints(change_dir)
151
151
  status = checkpoint_status(checkpoints, "foundation-gate")
152
- if status == "approved":
152
+ # missing = checkpoint not enabled for this change (no file / no entry).
153
+ if status in {"approved", "missing"}:
153
154
  return None
154
155
  if task_number is None:
155
156
  return {
@@ -174,8 +175,25 @@ def foundation_gate_blocks(task_number: int | None, change_dir: Path) -> dict[st
174
175
  }
175
176
 
176
177
 
177
- def validate_ledger_entry_v2(entry: dict[str, Any], verification: str) -> list[str]:
178
+ def is_degraded_ledger_entry(entry: dict[str, Any]) -> bool:
179
+ """NOT_RUN + evidence starting with 'DEGRADED: <non-empty reason>'."""
180
+ if entry.get("status") != "NOT_RUN":
181
+ return False
182
+ evidence = str(entry.get("evidence") or "").strip()
183
+ if not evidence.startswith("DEGRADED:"):
184
+ return False
185
+ reason = evidence.split(":", 1)[1].strip()
186
+ return bool(reason)
187
+
188
+
189
+ def validate_ledger_entry_v2(entry: dict[str, Any], verification: str) -> tuple[list[str], bool]:
190
+ """Internal helper for ledger close validation (not a public API).
191
+
192
+ Returns ``(missing_fields, degraded_ok)`` where ``degraded_ok`` is True only when
193
+ the entry is a valid DEGRADED NOT_RUN record with no other missing fields.
194
+ """
178
195
  missing: list[str] = []
196
+ degraded = is_degraded_ledger_entry(entry)
179
197
  for field in LEDGER_V2_REQUIRED_ENTRY_FIELDS:
180
198
  value = entry.get(field)
181
199
  if field == "inputsFiles":
@@ -184,7 +202,7 @@ def validate_ledger_entry_v2(entry: dict[str, Any], verification: str) -> list[s
184
202
  elif verification == "unitTestFull" and not value:
185
203
  missing.append("inputsFiles(non-empty)")
186
204
  elif field == "status":
187
- if value != "OK":
205
+ if value != "OK" and not degraded:
188
206
  missing.append("status=OK")
189
207
  elif not (isinstance(value, str) and value.strip()):
190
208
  missing.append(field)
@@ -192,7 +210,7 @@ def validate_ledger_entry_v2(entry: dict[str, Any], verification: str) -> list[s
192
210
  missing.append("coverage(valid)")
193
211
  elif field == "algorithmVersion" and str(value).strip() != hl.LEDGER_VERSION:
194
212
  missing.append("algorithmVersion(harness-ledger-2)")
195
- return missing
213
+ return missing, degraded and not missing
196
214
 
197
215
 
198
216
  def validate_ledger_for_phase_close(
@@ -225,6 +243,7 @@ def validate_ledger_for_phase_close(
225
243
  }
226
244
 
227
245
  problems: list[dict[str, Any]] = []
246
+ degraded: list[str] = []
228
247
  for verification in required:
229
248
  entry = validations.get(verification)
230
249
  if not isinstance(entry, dict):
@@ -236,7 +255,7 @@ def validate_ledger_for_phase_close(
236
255
  }
237
256
  )
238
257
  continue
239
- missing = validate_ledger_entry_v2(entry, verification)
258
+ missing, is_degraded = validate_ledger_entry_v2(entry, verification)
240
259
  if missing:
241
260
  code = (
242
261
  "MISSING_V2_FIELDS"
@@ -258,6 +277,8 @@ def validate_ledger_for_phase_close(
258
277
  "code": code,
259
278
  }
260
279
  )
280
+ elif is_degraded:
281
+ degraded.append(verification)
261
282
 
262
283
  if problems:
263
284
  return {
@@ -269,6 +290,15 @@ def validate_ledger_for_phase_close(
269
290
  "ledgerPath": str(ledger_path) if ledger_path else None,
270
291
  "detail": "natural-language override is not permitted",
271
292
  }
293
+ if degraded:
294
+ return {
295
+ "ok": True,
296
+ "code": "LEDGER_OK_DEGRADED",
297
+ "phase": phase,
298
+ "validated": required,
299
+ "degraded": degraded,
300
+ "ledgerPath": str(ledger_path) if ledger_path else None,
301
+ }
272
302
  return {
273
303
  "ok": True,
274
304
  "code": "LEDGER_OK",
@@ -417,7 +447,11 @@ def change_code_root(change_dir: Path) -> Path:
417
447
  return project
418
448
 
419
449
 
420
- def classify_risk(change_dir: Path, stage: str) -> dict[str, Any]:
450
+ def classify_risk(
451
+ change_dir: Path,
452
+ stage: str,
453
+ workflow: dict[str, Any] | None = None,
454
+ ) -> dict[str, Any]:
421
455
  tier = "full"
422
456
  source = "default-full"
423
457
  plan_path = change_dir / "plans"
@@ -484,7 +518,31 @@ def classify_risk(change_dir: Path, stage: str) -> dict[str, Any]:
484
518
  tier = observed
485
519
  source = f"{source}+post-run"
486
520
  main_project = change_dir.parents[2]
487
- workflow = _load_workflow_policy(project=main_project)
521
+ if workflow is None:
522
+ workflow = _load_workflow_policy(project=main_project)
523
+ tier_policy = workflow["riskTiers"][tier]
524
+ unique_signals = sorted(set(signals))
525
+ payload = {
526
+ "ok": True,
527
+ "code": "CLASSIFIED",
528
+ "stage": stage,
529
+ "tier": tier,
530
+ "source": source,
531
+ "changeId": change_dir.name,
532
+ "signals": unique_signals,
533
+ "defaultPhases": list(tier_policy["defaultPhases"]),
534
+ "requiredValidations": list(tier_policy["requiredValidations"]),
535
+ "conditionalStages": list(tier_policy["conditionalStages"]),
536
+ "stageDecisions": _stage_decisions_for_tier(workflow, tier, unique_signals),
537
+ }
538
+ if stage == "post-run":
539
+ _write_json(change_dir / "meta" / "risk-classification.json", payload)
540
+ return payload
541
+
542
+
543
+ def _stage_decisions_for_tier(
544
+ workflow: dict[str, Any], tier: str, signals: list[str]
545
+ ) -> dict[str, dict[str, Any]]:
488
546
  tier_policy = workflow["riskTiers"][tier]
489
547
  unique_signals = sorted(set(signals))
490
548
  stage_decisions: dict[str, dict[str, Any]] = {}
@@ -501,22 +559,66 @@ def classify_risk(change_dir: Path, stage: str) -> dict[str, Any]:
501
559
  ),
502
560
  "matchedSignals": matched,
503
561
  }
504
- payload = {
562
+ return stage_decisions
563
+
564
+
565
+ def apply_tier_override(
566
+ payload: dict[str, Any],
567
+ workflow: dict[str, Any],
568
+ *,
569
+ tier: str,
570
+ override_by: str,
571
+ ) -> dict[str, Any]:
572
+ """Rebind payload to an explicit tier override (source=override)."""
573
+ tier_policy = workflow["riskTiers"][tier]
574
+ now = hc.now_iso()
575
+ signals = list(payload.get("signals") or [])
576
+ payload = dict(payload)
577
+ payload["tier"] = tier
578
+ payload["source"] = "override"
579
+ payload["defaultPhases"] = list(tier_policy["defaultPhases"])
580
+ payload["requiredValidations"] = list(tier_policy["requiredValidations"])
581
+ payload["conditionalStages"] = list(tier_policy["conditionalStages"])
582
+ payload["stageDecisions"] = _stage_decisions_for_tier(workflow, tier, signals)
583
+ payload["tierOverride"] = {"tier": tier, "by": override_by, "at": now}
584
+ return payload
585
+
586
+
587
+ def gate_policy_document(payload: dict[str, Any]) -> dict[str, Any]:
588
+ """Cross-change contract: meta/gate-policy.json (schemaVersion=1)."""
589
+ return {
590
+ "schemaVersion": 1,
591
+ "tier": payload["tier"],
592
+ "source": payload["source"],
593
+ "defaultPhases": list(payload.get("defaultPhases") or []),
594
+ "requiredValidations": list(payload.get("requiredValidations") or []),
595
+ "classifiedAt": payload.get("classifiedAt") or hc.now_iso(),
596
+ "tierOverride": payload.get("tierOverride"),
597
+ }
598
+
599
+
600
+ def classify_defaults(
601
+ workflow: dict[str, Any],
602
+ *,
603
+ change_id: str,
604
+ stage: str,
605
+ ) -> dict[str, Any]:
606
+ tier = "full"
607
+ source = "default-full"
608
+ tier_policy = workflow["riskTiers"][tier]
609
+ return {
505
610
  "ok": True,
506
611
  "code": "CLASSIFIED",
507
612
  "stage": stage,
508
613
  "tier": tier,
509
614
  "source": source,
510
- "changeId": change_dir.name,
511
- "signals": unique_signals,
615
+ "changeId": change_id,
616
+ "signals": [],
512
617
  "defaultPhases": list(tier_policy["defaultPhases"]),
513
618
  "requiredValidations": list(tier_policy["requiredValidations"]),
514
619
  "conditionalStages": list(tier_policy["conditionalStages"]),
515
- "stageDecisions": stage_decisions,
620
+ "stageDecisions": _stage_decisions_for_tier(workflow, tier, []),
516
621
  }
517
- if stage == "post-run":
518
- _write_json(change_dir / "meta" / "risk-classification.json", payload)
519
- return payload
520
622
 
521
623
 
522
624
  def lint_skill_tree(skills_root: Path) -> dict[str, Any]:
@@ -823,6 +925,14 @@ def cmd_close(args: argparse.Namespace) -> int:
823
925
  extra={k: v for k, v in ledger_result.items() if k not in {"ok", "message", "code"}},
824
926
  )
825
927
 
928
+ close_status = args.status
929
+ close_code = "PHASE_CLOSED"
930
+ if ledger_result.get("code") == "LEDGER_OK_DEGRADED":
931
+ close_code = "CLOSED_DEGRADED"
932
+ # Degraded close: phase.end status must not exceed WARN (OK → WARN).
933
+ if close_status == "OK":
934
+ close_status = "WARN"
935
+
826
936
  guard_result = None
827
937
  if args.phase in {"run", "test"}:
828
938
  project_root = args.project or str(project)
@@ -839,7 +949,7 @@ def cmd_close(args: argparse.Namespace) -> int:
839
949
  change_dir,
840
950
  phase=args.phase,
841
951
  type_="phase.end",
842
- status=args.status,
952
+ status=close_status,
843
953
  note=args.note or "",
844
954
  run_id=run_id,
845
955
  ) if not _phase_event_exists(change_dir, args.phase, "phase.end", run_id) else {
@@ -862,9 +972,9 @@ def cmd_close(args: argparse.Namespace) -> int:
862
972
 
863
973
  payload = {
864
974
  "ok": True,
865
- "code": "PHASE_CLOSED",
975
+ "code": close_code,
866
976
  "phase": args.phase,
867
- "status": args.status,
977
+ "status": close_status,
868
978
  "changeId": resolved["changeId"],
869
979
  "ledger": ledger_result,
870
980
  "testGuard": guard_result,
@@ -878,19 +988,56 @@ def cmd_close(args: argparse.Namespace) -> int:
878
988
  def cmd_classify(args: argparse.Namespace) -> int:
879
989
  as_json = bool(args.json)
880
990
  project = hc.resolve_main_project_root()
991
+ try:
992
+ workflow = _load_workflow_policy(project=project)
993
+ except (OSError, ValueError, hwp.PolicyValidationError) as exc:
994
+ return emit_error("POLICY_LOAD_FAILED", str(exc), as_json=as_json)
995
+
881
996
  resolved = hc.resolve_change(project, args.change)
882
- if not resolved.get("ok"):
883
- return emit_error(
884
- str(resolved.get("code", "RESOLVE_FAILED")),
885
- str(resolved.get("message", "change resolve failed")),
886
- as_json=as_json,
887
- extra={k: v for k, v in resolved.items() if k not in {"ok", "message"}},
997
+ change_dir: Path | None = None
998
+ change_id = str(args.change or "")
999
+ if resolved.get("ok"):
1000
+ change_dir = Path(resolved["changeDir"])
1001
+ change_id = str(resolved.get("changeId") or change_dir.name)
1002
+ if not change_dir.is_dir():
1003
+ change_dir = None
1004
+
1005
+ if change_dir is not None:
1006
+ payload = classify_risk(change_dir, args.stage, workflow=workflow)
1007
+ else:
1008
+ payload = classify_defaults(workflow, change_id=change_id or "unknown", stage=args.stage)
1009
+ if not resolved.get("ok"):
1010
+ payload["resolveCode"] = resolved.get("code")
1011
+
1012
+ tier_override = getattr(args, "tier_override", None)
1013
+ if tier_override:
1014
+ payload = apply_tier_override(
1015
+ payload,
1016
+ workflow,
1017
+ tier=str(tier_override),
1018
+ override_by=str(getattr(args, "override_by", None) or "user"),
888
1019
  )
889
- payload = classify_risk(Path(resolved["changeDir"]), args.stage)
1020
+ else:
1021
+ payload.setdefault("tierOverride", None)
1022
+
1023
+ classified_at = hc.now_iso()
1024
+ payload["classifiedAt"] = classified_at
1025
+
1026
+ if change_dir is not None and change_dir.is_dir():
1027
+ policy_doc = gate_policy_document(payload)
1028
+ policy_path = change_dir / "meta" / "gate-policy.json"
1029
+ _write_json(policy_path, policy_doc)
1030
+ payload["policyPersisted"] = True
1031
+ payload["policyPath"] = str(policy_path)
1032
+ else:
1033
+ payload["policyPersisted"] = False
1034
+ payload["warning"] = (
1035
+ "change directory does not exist; gate-policy.json not written"
1036
+ )
1037
+
890
1038
  emit(payload, as_json=as_json)
891
1039
  return 0
892
1040
 
893
-
894
1041
  def cmd_checkpoint(args: argparse.Namespace) -> int:
895
1042
  as_json = bool(args.json)
896
1043
  project = hc.resolve_main_project_root()
@@ -1012,6 +1159,12 @@ def build_parser() -> argparse.ArgumentParser:
1012
1159
  p_classify = sub.add_parser("classify", parents=[shared])
1013
1160
  p_classify.add_argument("--change", default=None)
1014
1161
  p_classify.add_argument("--stage", required=True, choices=["plan", "post-run"])
1162
+ p_classify.add_argument(
1163
+ "--tier-override",
1164
+ default=None,
1165
+ choices=["fast", "standard", "full"],
1166
+ )
1167
+ p_classify.add_argument("--override-by", default="user")
1015
1168
  p_classify.set_defaults(func=cmd_classify)
1016
1169
 
1017
1170
  p_checkpoint = sub.add_parser("checkpoint", parents=[shared])
@@ -816,6 +816,18 @@ def cmd_record(args: argparse.Namespace) -> int:
816
816
  "finishedAt": now_iso(),
817
817
  }
818
818
  )
819
+ metrics_raw = getattr(args, "metrics_json", None)
820
+ if metrics_raw is not None and str(metrics_raw).strip() != "":
821
+ try:
822
+ metrics_obj = json.loads(str(metrics_raw))
823
+ except json.JSONDecodeError:
824
+ return emit_error("invalid --metrics-json", as_json=as_json)
825
+ if not isinstance(metrics_obj, dict):
826
+ return emit_error("invalid --metrics-json", as_json=as_json)
827
+ entry["metrics"] = metrics_obj
828
+ elif "metrics" in entry:
829
+ # Fresh record without metrics must not keep stale metrics from prev.
830
+ entry.pop("metrics", None)
819
831
  if args.scope is not None and str(args.scope).strip():
820
832
  entry["scope"] = str(args.scope).strip()
821
833
  # No default scope: recording an incremental run as broad "module" scope
@@ -1014,6 +1026,11 @@ def build_parser() -> argparse.ArgumentParser:
1014
1026
  default=None,
1015
1027
  help="package: prior verifications reused when testsExecuted=false",
1016
1028
  )
1029
+ p_record.add_argument(
1030
+ "--metrics-json",
1031
+ default=None,
1032
+ help='structured counts, e.g. \'{"run":155,"failures":0}\' or \'{"total":3,"passed":3}\'',
1033
+ )
1017
1034
  p_record.set_defaults(func=cmd_record)
1018
1035
 
1019
1036
  p_diff = sub.add_parser(