@hunter-harness/workflow-harness 0.2.5 → 0.2.6

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 (202) 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 +5 -2
  3. package/harness/bundles/general/claude-code/harness-archive/reference.md +1 -1
  4. package/harness/bundles/general/claude-code/harness-archive/templates/render-summary.mjs +78 -155
  5. package/harness/bundles/general/claude-code/harness-codebase-map/SKILL.md +4 -2
  6. package/harness/bundles/general/claude-code/harness-knowledge-ingest/SKILL.md +5 -3
  7. package/harness/bundles/general/claude-code/harness-knowledge-ingest/scripts/harness_knowledge.py +83 -6
  8. package/harness/bundles/general/claude-code/harness-knowledge-query/SKILL.md +4 -2
  9. package/harness/bundles/general/claude-code/harness-plan/SKILL.md +6 -3
  10. package/harness/bundles/general/claude-code/harness-review/SKILL.md +6 -3
  11. package/harness/bundles/general/claude-code/harness-run/SKILL.md +5 -2
  12. package/harness/bundles/general/claude-code/harness-run/reference.md +1 -1
  13. package/harness/bundles/general/claude-code/harness-submit/SKILL.md +5 -2
  14. package/harness/bundles/general/claude-code/harness-sync/SKILL.md +5 -3
  15. package/harness/bundles/general/claude-code/harness-test/SKILL.md +5 -2
  16. package/harness/bundles/general/claude-code/harness-test/reference.md +1 -1
  17. package/harness/bundles/general/claude-code/protocols/ledger-protocol.md +1 -1
  18. package/harness/bundles/general/claude-code/protocols/report-pipeline-protocol.md +11 -4
  19. package/harness/bundles/general/claude-code/scripts/harness_archive.py +190 -32
  20. package/harness/bundles/general/claude-code/scripts/harness_events.py +134 -40
  21. package/harness/bundles/general/claude-code/scripts/harness_preflight.py +46 -3
  22. package/harness/bundles/general/claude-code/scripts/harness_profile.py +2 -2
  23. package/harness/bundles/general/claude-code/scripts/harness_state.py +148 -0
  24. package/harness/bundles/general/codebuddy/.harness-build.json +1 -1
  25. package/harness/bundles/general/codebuddy/harness-archive/SKILL.md +5 -2
  26. package/harness/bundles/general/codebuddy/harness-archive/reference.md +1 -1
  27. package/harness/bundles/general/codebuddy/harness-archive/templates/render-summary.mjs +78 -155
  28. package/harness/bundles/general/codebuddy/harness-codebase-map/SKILL.md +4 -2
  29. package/harness/bundles/general/codebuddy/harness-knowledge-ingest/SKILL.md +5 -3
  30. package/harness/bundles/general/codebuddy/harness-knowledge-ingest/scripts/harness_knowledge.py +83 -6
  31. package/harness/bundles/general/codebuddy/harness-knowledge-query/SKILL.md +4 -2
  32. package/harness/bundles/general/codebuddy/harness-plan/SKILL.md +6 -3
  33. package/harness/bundles/general/codebuddy/harness-review/SKILL.md +6 -3
  34. package/harness/bundles/general/codebuddy/harness-run/SKILL.md +5 -2
  35. package/harness/bundles/general/codebuddy/harness-run/reference.md +1 -1
  36. package/harness/bundles/general/codebuddy/harness-submit/SKILL.md +5 -2
  37. package/harness/bundles/general/codebuddy/harness-sync/SKILL.md +5 -3
  38. package/harness/bundles/general/codebuddy/harness-test/SKILL.md +5 -2
  39. package/harness/bundles/general/codebuddy/harness-test/reference.md +1 -1
  40. package/harness/bundles/general/codebuddy/protocols/ledger-protocol.md +1 -1
  41. package/harness/bundles/general/codebuddy/protocols/report-pipeline-protocol.md +11 -4
  42. package/harness/bundles/general/codebuddy/scripts/harness_archive.py +190 -32
  43. package/harness/bundles/general/codebuddy/scripts/harness_events.py +134 -40
  44. package/harness/bundles/general/codebuddy/scripts/harness_preflight.py +46 -3
  45. package/harness/bundles/general/codebuddy/scripts/harness_profile.py +2 -2
  46. package/harness/bundles/general/codebuddy/scripts/harness_state.py +148 -0
  47. package/harness/bundles/general/codex/.harness-build.json +1 -1
  48. package/harness/bundles/general/codex/harness-archive/SKILL.md +5 -2
  49. package/harness/bundles/general/codex/harness-archive/reference.md +1 -1
  50. package/harness/bundles/general/codex/harness-archive/templates/render-summary.mjs +78 -155
  51. package/harness/bundles/general/codex/harness-codebase-map/SKILL.md +4 -2
  52. package/harness/bundles/general/codex/harness-knowledge-ingest/SKILL.md +5 -3
  53. package/harness/bundles/general/codex/harness-knowledge-ingest/scripts/harness_knowledge.py +83 -6
  54. package/harness/bundles/general/codex/harness-knowledge-query/SKILL.md +4 -2
  55. package/harness/bundles/general/codex/harness-plan/SKILL.md +6 -3
  56. package/harness/bundles/general/codex/harness-review/SKILL.md +5 -2
  57. package/harness/bundles/general/codex/harness-run/SKILL.md +5 -2
  58. package/harness/bundles/general/codex/harness-run/reference.md +1 -1
  59. package/harness/bundles/general/codex/harness-submit/SKILL.md +5 -2
  60. package/harness/bundles/general/codex/harness-sync/SKILL.md +5 -3
  61. package/harness/bundles/general/codex/harness-test/SKILL.md +5 -2
  62. package/harness/bundles/general/codex/harness-test/reference.md +1 -1
  63. package/harness/bundles/general/codex/protocols/ledger-protocol.md +1 -1
  64. package/harness/bundles/general/codex/protocols/report-pipeline-protocol.md +11 -4
  65. package/harness/bundles/general/codex/scripts/harness_archive.py +190 -32
  66. package/harness/bundles/general/codex/scripts/harness_events.py +134 -40
  67. package/harness/bundles/general/codex/scripts/harness_preflight.py +46 -3
  68. package/harness/bundles/general/codex/scripts/harness_profile.py +2 -2
  69. package/harness/bundles/general/codex/scripts/harness_state.py +148 -0
  70. package/harness/bundles/general/cursor/.harness-build.json +1 -1
  71. package/harness/bundles/general/cursor/harness-archive/SKILL.md +5 -2
  72. package/harness/bundles/general/cursor/harness-archive/reference.md +1 -1
  73. package/harness/bundles/general/cursor/harness-archive/templates/render-summary.mjs +78 -155
  74. package/harness/bundles/general/cursor/harness-codebase-map/SKILL.md +4 -2
  75. package/harness/bundles/general/cursor/harness-knowledge-ingest/SKILL.md +5 -3
  76. package/harness/bundles/general/cursor/harness-knowledge-ingest/scripts/harness_knowledge.py +83 -6
  77. package/harness/bundles/general/cursor/harness-knowledge-query/SKILL.md +4 -2
  78. package/harness/bundles/general/cursor/harness-plan/SKILL.md +6 -3
  79. package/harness/bundles/general/cursor/harness-review/SKILL.md +5 -2
  80. package/harness/bundles/general/cursor/harness-run/SKILL.md +5 -2
  81. package/harness/bundles/general/cursor/harness-run/reference.md +1 -1
  82. package/harness/bundles/general/cursor/harness-submit/SKILL.md +5 -2
  83. package/harness/bundles/general/cursor/harness-sync/SKILL.md +5 -3
  84. package/harness/bundles/general/cursor/harness-test/SKILL.md +5 -2
  85. package/harness/bundles/general/cursor/harness-test/reference.md +1 -1
  86. package/harness/bundles/general/cursor/protocols/ledger-protocol.md +1 -1
  87. package/harness/bundles/general/cursor/protocols/report-pipeline-protocol.md +11 -4
  88. package/harness/bundles/general/cursor/scripts/harness_archive.py +190 -32
  89. package/harness/bundles/general/cursor/scripts/harness_events.py +134 -40
  90. package/harness/bundles/general/cursor/scripts/harness_preflight.py +46 -3
  91. package/harness/bundles/general/cursor/scripts/harness_profile.py +2 -2
  92. package/harness/bundles/general/cursor/scripts/harness_state.py +148 -0
  93. package/harness/bundles/java/claude-code/.harness-build.json +1 -1
  94. package/harness/bundles/java/claude-code/harness-apidoc/SKILL.md +5 -2
  95. package/harness/bundles/java/claude-code/harness-archive/SKILL.md +5 -2
  96. package/harness/bundles/java/claude-code/harness-archive/reference.md +1 -1
  97. package/harness/bundles/java/claude-code/harness-archive/templates/render-summary.mjs +78 -155
  98. package/harness/bundles/java/claude-code/harness-codebase-map/SKILL.md +4 -2
  99. package/harness/bundles/java/claude-code/harness-knowledge-ingest/SKILL.md +5 -3
  100. package/harness/bundles/java/claude-code/harness-knowledge-ingest/scripts/harness_knowledge.py +83 -6
  101. package/harness/bundles/java/claude-code/harness-knowledge-query/SKILL.md +4 -2
  102. package/harness/bundles/java/claude-code/harness-package/SKILL.md +5 -2
  103. package/harness/bundles/java/claude-code/harness-plan/SKILL.md +6 -3
  104. package/harness/bundles/java/claude-code/harness-review/SKILL.md +6 -3
  105. package/harness/bundles/java/claude-code/harness-run/SKILL.md +5 -2
  106. package/harness/bundles/java/claude-code/harness-run/reference.md +1 -1
  107. package/harness/bundles/java/claude-code/harness-submit/SKILL.md +5 -2
  108. package/harness/bundles/java/claude-code/harness-sync/SKILL.md +5 -3
  109. package/harness/bundles/java/claude-code/harness-test/SKILL.md +5 -2
  110. package/harness/bundles/java/claude-code/harness-test/reference.md +1 -1
  111. package/harness/bundles/java/claude-code/protocols/ledger-protocol.md +1 -1
  112. package/harness/bundles/java/claude-code/protocols/report-pipeline-protocol.md +11 -4
  113. package/harness/bundles/java/claude-code/scripts/harness_archive.py +190 -32
  114. package/harness/bundles/java/claude-code/scripts/harness_events.py +134 -40
  115. package/harness/bundles/java/claude-code/scripts/harness_preflight.py +46 -3
  116. package/harness/bundles/java/claude-code/scripts/harness_profile.py +2 -2
  117. package/harness/bundles/java/claude-code/scripts/harness_state.py +148 -0
  118. package/harness/bundles/java/codebuddy/.harness-build.json +1 -1
  119. package/harness/bundles/java/codebuddy/harness-apidoc/SKILL.md +5 -2
  120. package/harness/bundles/java/codebuddy/harness-archive/SKILL.md +5 -2
  121. package/harness/bundles/java/codebuddy/harness-archive/reference.md +1 -1
  122. package/harness/bundles/java/codebuddy/harness-archive/templates/render-summary.mjs +78 -155
  123. package/harness/bundles/java/codebuddy/harness-codebase-map/SKILL.md +4 -2
  124. package/harness/bundles/java/codebuddy/harness-knowledge-ingest/SKILL.md +5 -3
  125. package/harness/bundles/java/codebuddy/harness-knowledge-ingest/scripts/harness_knowledge.py +83 -6
  126. package/harness/bundles/java/codebuddy/harness-knowledge-query/SKILL.md +4 -2
  127. package/harness/bundles/java/codebuddy/harness-package/SKILL.md +5 -2
  128. package/harness/bundles/java/codebuddy/harness-plan/SKILL.md +6 -3
  129. package/harness/bundles/java/codebuddy/harness-review/SKILL.md +6 -3
  130. package/harness/bundles/java/codebuddy/harness-run/SKILL.md +5 -2
  131. package/harness/bundles/java/codebuddy/harness-run/reference.md +1 -1
  132. package/harness/bundles/java/codebuddy/harness-submit/SKILL.md +5 -2
  133. package/harness/bundles/java/codebuddy/harness-sync/SKILL.md +5 -3
  134. package/harness/bundles/java/codebuddy/harness-test/SKILL.md +5 -2
  135. package/harness/bundles/java/codebuddy/harness-test/reference.md +1 -1
  136. package/harness/bundles/java/codebuddy/protocols/ledger-protocol.md +1 -1
  137. package/harness/bundles/java/codebuddy/protocols/report-pipeline-protocol.md +11 -4
  138. package/harness/bundles/java/codebuddy/scripts/harness_archive.py +190 -32
  139. package/harness/bundles/java/codebuddy/scripts/harness_events.py +134 -40
  140. package/harness/bundles/java/codebuddy/scripts/harness_preflight.py +46 -3
  141. package/harness/bundles/java/codebuddy/scripts/harness_profile.py +2 -2
  142. package/harness/bundles/java/codebuddy/scripts/harness_state.py +148 -0
  143. package/harness/bundles/java/codex/.harness-build.json +1 -1
  144. package/harness/bundles/java/codex/harness-apidoc/SKILL.md +5 -2
  145. package/harness/bundles/java/codex/harness-archive/SKILL.md +5 -2
  146. package/harness/bundles/java/codex/harness-archive/reference.md +1 -1
  147. package/harness/bundles/java/codex/harness-archive/templates/render-summary.mjs +78 -155
  148. package/harness/bundles/java/codex/harness-codebase-map/SKILL.md +4 -2
  149. package/harness/bundles/java/codex/harness-knowledge-ingest/SKILL.md +5 -3
  150. package/harness/bundles/java/codex/harness-knowledge-ingest/scripts/harness_knowledge.py +83 -6
  151. package/harness/bundles/java/codex/harness-knowledge-query/SKILL.md +4 -2
  152. package/harness/bundles/java/codex/harness-package/SKILL.md +5 -2
  153. package/harness/bundles/java/codex/harness-plan/SKILL.md +6 -3
  154. package/harness/bundles/java/codex/harness-review/SKILL.md +5 -2
  155. package/harness/bundles/java/codex/harness-run/SKILL.md +5 -2
  156. package/harness/bundles/java/codex/harness-run/reference.md +1 -1
  157. package/harness/bundles/java/codex/harness-submit/SKILL.md +5 -2
  158. package/harness/bundles/java/codex/harness-sync/SKILL.md +5 -3
  159. package/harness/bundles/java/codex/harness-test/SKILL.md +5 -2
  160. package/harness/bundles/java/codex/harness-test/reference.md +1 -1
  161. package/harness/bundles/java/codex/protocols/ledger-protocol.md +1 -1
  162. package/harness/bundles/java/codex/protocols/report-pipeline-protocol.md +11 -4
  163. package/harness/bundles/java/codex/scripts/harness_archive.py +190 -32
  164. package/harness/bundles/java/codex/scripts/harness_events.py +134 -40
  165. package/harness/bundles/java/codex/scripts/harness_preflight.py +46 -3
  166. package/harness/bundles/java/codex/scripts/harness_profile.py +2 -2
  167. package/harness/bundles/java/codex/scripts/harness_state.py +148 -0
  168. package/harness/bundles/java/cursor/.harness-build.json +1 -1
  169. package/harness/bundles/java/cursor/harness-apidoc/SKILL.md +5 -2
  170. package/harness/bundles/java/cursor/harness-archive/SKILL.md +5 -2
  171. package/harness/bundles/java/cursor/harness-archive/reference.md +1 -1
  172. package/harness/bundles/java/cursor/harness-archive/templates/render-summary.mjs +78 -155
  173. package/harness/bundles/java/cursor/harness-codebase-map/SKILL.md +4 -2
  174. package/harness/bundles/java/cursor/harness-knowledge-ingest/SKILL.md +5 -3
  175. package/harness/bundles/java/cursor/harness-knowledge-ingest/scripts/harness_knowledge.py +83 -6
  176. package/harness/bundles/java/cursor/harness-knowledge-query/SKILL.md +4 -2
  177. package/harness/bundles/java/cursor/harness-package/SKILL.md +5 -2
  178. package/harness/bundles/java/cursor/harness-plan/SKILL.md +6 -3
  179. package/harness/bundles/java/cursor/harness-review/SKILL.md +5 -2
  180. package/harness/bundles/java/cursor/harness-run/SKILL.md +5 -2
  181. package/harness/bundles/java/cursor/harness-run/reference.md +1 -1
  182. package/harness/bundles/java/cursor/harness-submit/SKILL.md +5 -2
  183. package/harness/bundles/java/cursor/harness-sync/SKILL.md +5 -3
  184. package/harness/bundles/java/cursor/harness-test/SKILL.md +5 -2
  185. package/harness/bundles/java/cursor/harness-test/reference.md +1 -1
  186. package/harness/bundles/java/cursor/protocols/ledger-protocol.md +1 -1
  187. package/harness/bundles/java/cursor/protocols/report-pipeline-protocol.md +11 -4
  188. package/harness/bundles/java/cursor/scripts/harness_archive.py +190 -32
  189. package/harness/bundles/java/cursor/scripts/harness_events.py +134 -40
  190. package/harness/bundles/java/cursor/scripts/harness_preflight.py +46 -3
  191. package/harness/bundles/java/cursor/scripts/harness_profile.py +2 -2
  192. package/harness/bundles/java/cursor/scripts/harness_state.py +148 -0
  193. package/harness/manifests/general/claude-code.json +24 -24
  194. package/harness/manifests/general/codebuddy.json +24 -24
  195. package/harness/manifests/general/codex.json +24 -24
  196. package/harness/manifests/general/cursor.json +24 -24
  197. package/harness/manifests/java/claude-code.json +26 -26
  198. package/harness/manifests/java/codebuddy.json +26 -26
  199. package/harness/manifests/java/codex.json +26 -26
  200. package/harness/manifests/java/cursor.json +26 -26
  201. package/hunter-workflow-family.json +1 -1
  202. package/package.json +1 -1
@@ -622,13 +622,16 @@ def _ledger_unit_tests(ledger: dict[str, Any] | None) -> dict[str, Any]:
622
622
  status = str(unit.get("status") or "").upper()
623
623
  evidence = unit.get("evidence") or {}
624
624
  if isinstance(evidence, dict):
625
- run = evidence.get("run", unit.get("run", 0))
625
+ run = evidence.get(
626
+ "run",
627
+ evidence.get("testsRun", unit.get("run", unit.get("testsRun", 0))),
628
+ )
626
629
  failures = evidence.get("failures", unit.get("failures", 0))
627
630
  errors = evidence.get("errors", unit.get("errors", 0))
628
631
  skipped = evidence.get("skipped", unit.get("skipped", 0))
629
632
  pass_rate = evidence.get("passRate") or unit.get("passRate")
630
633
  else:
631
- run = unit.get("run", 0)
634
+ run = unit.get("run", unit.get("testsRun", 0))
632
635
  failures = unit.get("failures", 0)
633
636
  errors = unit.get("errors", 0)
634
637
  skipped = unit.get("skipped", 0)
@@ -886,16 +889,21 @@ def _durations_from_event_phases(event_summary: dict[str, Any]) -> dict[str, Any
886
889
  minutes = round((dur or 0) / 60000, 2) if dur is not None else 0
887
890
  if dur:
888
891
  total_ms += int(dur)
889
- stages.append(
890
- {
891
- "stage": str(name),
892
- "skill": f"harness-{name}",
893
- "startedAt": info.get("started_at") or NOT_AVAILABLE,
894
- "endedAt": info.get("ended_at") or NOT_AVAILABLE,
895
- "minutes": minutes,
896
- "result": "OK",
897
- }
898
- )
892
+ attempts = info.get("attempts") if isinstance(info.get("attempts"), list) else []
893
+ result = str(info.get("status") or "UNKNOWN").upper()
894
+ if result in {"PASS", "PASSED", "SUCCESS"}:
895
+ result = "OK"
896
+ elif result in {"FAILED", "ERROR"}:
897
+ result = "FAIL"
898
+ stages.append({
899
+ "stage": str(name),
900
+ "skill": f"harness-{name}",
901
+ "startedAt": info.get("started_at") or NOT_AVAILABLE,
902
+ "endedAt": info.get("ended_at") or NOT_AVAILABLE,
903
+ "minutes": minutes,
904
+ "result": result,
905
+ "attempts": attempts,
906
+ })
899
907
  total_min = round(total_ms / 60000, 2)
900
908
  return {
901
909
  "totalLabel": f"约 {int(round(total_min))} 分",
@@ -917,11 +925,31 @@ def _stage_status_from_sources(
917
925
  "submit": "OK",
918
926
  "archive": "OK",
919
927
  }
928
+ for event in events:
929
+ if event.get("type") != "phase.end":
930
+ continue
931
+ phase = str(event.get("phase") or "").lower()
932
+ raw = str(event.get("status") or "").upper()
933
+ if phase not in status or not raw:
934
+ continue
935
+ if raw in {"PASS", "PASSED", "SUCCESS"}:
936
+ raw = "OK"
937
+ elif raw in {"FAILED", "ERROR"}:
938
+ raw = "FAIL"
939
+ elif raw in {"SKIP", "SKIPPED"}:
940
+ raw = "USER_SKIPPED"
941
+ status[phase] = raw
920
942
  # Issues in events can downgrade
921
943
  for e in events:
922
944
  if e.get("type") != "issue":
923
945
  continue
924
946
  sev = str(e.get("severity") or "").lower()
947
+ issue_text = " ".join(str(e.get(key) or "") for key in ("message", "note", "code")).lower()
948
+ if not sev:
949
+ if any(token in issue_text for token in ("fail", "error", "blocked", "失败", "阻塞")):
950
+ sev = "error"
951
+ elif any(token in issue_text for token in ("warn", "skip", "风险", "警告")):
952
+ sev = "warn"
925
953
  phase = str(e.get("phase") or "").lower()
926
954
  if phase in status and sev in {"error", "fail", "failed", "critical"}:
927
955
  status[phase] = "FAIL"
@@ -930,14 +958,19 @@ def _stage_status_from_sources(
930
958
 
931
959
  api = _ledger_api_tests(ledger)
932
960
  db = _ledger_db_compat(ledger)
933
- if api.get("status") == "USER_SKIPPED":
961
+ api_status = str(api.get("status") or "").upper()
962
+ if api_status in {"FAIL", "FAILED", "ERROR"} or int(api.get("failed") or 0) > 0:
963
+ status["test"] = "FAIL"
964
+ elif api_status in {"BLOCKED", "BLOCKED_BY_ENV", "BLOCKED_BY_DBA"}:
965
+ status["test"] = api_status
966
+ elif api_status == "USER_SKIPPED":
934
967
  status["test"] = "USER_SKIPPED"
935
968
  elif db == "BLOCKED_BY_DBA":
936
969
  status["test"] = "BLOCKED_BY_DBA"
937
970
  elif api.get("status") == "NOT_RUN" and not find_test_reports(change_dir):
938
971
  unit = _ledger_unit_tests(ledger)
939
972
  if unit.get("source") == "not-run" and unit.get("run", 0) == 0:
940
- status["test"] = "USER_SKIPPED"
973
+ status["test"] = "NOT_RUN"
941
974
 
942
975
  if not find_review_reports(change_dir):
943
976
  log_text = load_execution_log(change_dir)
@@ -954,8 +987,6 @@ def _compute_final_status(
954
987
  api = verification.get("apiTests") or {}
955
988
  db = str(verification.get("dbCompatibility") or "")
956
989
  api_status = str(api.get("status") or "")
957
- if api_status == "USER_SKIPPED" or db == "BLOCKED_BY_DBA":
958
- return "CONDITIONAL_OK"
959
990
  for v in stage_status.values():
960
991
  if v == "FAIL":
961
992
  return "FAIL"
@@ -964,8 +995,14 @@ def _compute_final_status(
964
995
  return "FAIL"
965
996
  if int(api.get("failed") or 0) > 0:
966
997
  return "FAIL"
998
+ conditional = {
999
+ "USER_SKIPPED", "BLOCKED", "BLOCKED_BY_ENV", "BLOCKED_BY_DBA",
1000
+ "NOT_RUN", "PARTIAL",
1001
+ }
1002
+ if api_status in conditional or db in conditional:
1003
+ return "CONDITIONAL_OK"
967
1004
  for v in stage_status.values():
968
- if v in {"WARN", "PARTIAL", "USER_SKIPPED", "BLOCKED_BY_DBA"}:
1005
+ if v in {"WARN", *conditional}:
969
1006
  return "WARN" if v == "WARN" else "CONDITIONAL_OK"
970
1007
  return "OK"
971
1008
 
@@ -984,6 +1021,8 @@ def _changed_files_from_git(
984
1021
  changed: list[dict[str, Any]] = []
985
1022
  if not base or not head:
986
1023
  return diff_stat, changed
1024
+ # The ledger bounds the complete task. Using only ``head^..head`` would
1025
+ # silently omit earlier checkpoint commits from the same change.
987
1026
  rng = f"{base}..{head}"
988
1027
  code, out, _ = git_run(project, "diff", "--numstat", rng)
989
1028
  if code != 0 or not out:
@@ -1040,6 +1079,97 @@ def _review_summary(change_dir: Path, existing: dict[str, Any] | None) -> dict[s
1040
1079
  return base
1041
1080
 
1042
1081
 
1082
+ def _final_commit_from_sources(
1083
+ ledger: dict[str, Any] | None,
1084
+ events: list[dict[str, Any]],
1085
+ existing: dict[str, Any] | None,
1086
+ project: Path,
1087
+ ) -> str:
1088
+ if ledger:
1089
+ for key in (
1090
+ "mergeFinalHash", "finalCommit", "finalHash", "changeCommit", "headCommit",
1091
+ ):
1092
+ value = str(ledger.get(key) or "").strip()
1093
+ if value:
1094
+ return value
1095
+ commit_pattern = re.compile(r"\b[0-9a-f]{7,40}\b", re.IGNORECASE)
1096
+ for event in reversed(events):
1097
+ if str(event.get("phase") or "").lower() not in {"submit", "merge", "archive"}:
1098
+ continue
1099
+ text = " ".join(str(event.get(key) or "") for key in ("note", "message", "command"))
1100
+ match = commit_pattern.search(text)
1101
+ if match:
1102
+ return match.group(0)
1103
+ if existing and existing.get("finalCommit"):
1104
+ return str(existing["finalCommit"])
1105
+ code, head, _ = git_run(project, "rev-parse", "HEAD")
1106
+ return head if code == 0 and head else ""
1107
+
1108
+
1109
+ def _business_goal_from_sources(change_dir: Path, events: list[dict[str, Any]]) -> str:
1110
+ plans_root = change_dir / "plans"
1111
+ primary = sorted(plans_root.glob("*-plan.md"))
1112
+ secondary = [
1113
+ path for path in sorted(plans_root.glob("*.md"))
1114
+ if path not in primary
1115
+ and "implementation-detail" not in path.name
1116
+ and "test-scenarios" not in path.name
1117
+ ]
1118
+ for plan in [*primary, *secondary]:
1119
+ try:
1120
+ text = plan.read_text(encoding="utf-8-sig")
1121
+ except OSError:
1122
+ continue
1123
+ body = re.sub(r"\A---\s*\n.*?\n---\s*\n", "", text, count=1, flags=re.DOTALL)
1124
+ goal = re.search(r"(?im)^\s*(?:goal|目标|业务目标|需求)\s*[::]\s*(.+)$", body)
1125
+ if goal:
1126
+ return goal.group(1).strip()
1127
+ scope = re.search(r"(?im)^\s*>?\s*(?:变更范围|目标)\s*[::]\s*(.+)$", body)
1128
+ if scope:
1129
+ return scope.group(1).strip()
1130
+ first_task = re.search(r"(?m)^\s*\|\s*1\s*\|\s*([^|]+?)\s*\|", body)
1131
+ if first_task:
1132
+ return first_task.group(1).strip()
1133
+ for line in body.splitlines():
1134
+ clean = line.strip().lstrip("#").strip()
1135
+ if clean and not clean.startswith(("---", ">", "|")) and len(clean) > 8:
1136
+ return clean
1137
+ for event in events:
1138
+ if event.get("type") == "decision":
1139
+ value = str(event.get("decision") or event.get("note") or "").strip()
1140
+ if value:
1141
+ return re.sub(r"^(?:需求收敛|目标)\s*[::]\s*", "", value)
1142
+ return ""
1143
+
1144
+
1145
+ def _timeline_from_events(event_summary: dict[str, Any], events: list[dict[str, Any]]) -> list[dict[str, Any]]:
1146
+ timeline: list[dict[str, Any]] = []
1147
+ for phase, info in (event_summary.get("phases") or {}).items():
1148
+ attempts = info.get("attempts") if isinstance(info.get("attempts"), list) else []
1149
+ for attempt in attempts:
1150
+ timeline.append({
1151
+ "phase": phase,
1152
+ "attempt": attempt.get("attempt"),
1153
+ "startedAt": attempt.get("started_at"),
1154
+ "endedAt": attempt.get("ended_at"),
1155
+ "durationMs": attempt.get("duration_ms"),
1156
+ "status": attempt.get("status") or "UNKNOWN",
1157
+ "executorTool": attempt.get("executor_tool"),
1158
+ "executorAgent": attempt.get("executor_agent"),
1159
+ "handoffFromTool": attempt.get("handoff_from_tool"),
1160
+ })
1161
+ for event in events:
1162
+ if event.get("type") not in {"decision", "issue"}:
1163
+ continue
1164
+ timeline.append({
1165
+ "phase": event.get("phase"),
1166
+ "timestamp": event.get("timestamp"),
1167
+ "type": event.get("type"),
1168
+ "summary": event.get("decision") or event.get("message") or event.get("note") or "",
1169
+ })
1170
+ return timeline
1171
+
1172
+
1043
1173
  def collect_summary_data(
1044
1174
  change_dir: Path,
1045
1175
  *,
@@ -1118,18 +1248,14 @@ def collect_summary_data(
1118
1248
  if existing and existing.get("businessGoal"):
1119
1249
  data["businessGoal"] = existing["businessGoal"]
1120
1250
  else:
1121
- data["businessGoal"] = NOT_AVAILABLE if for_replay else ""
1251
+ inferred_goal = _business_goal_from_sources(change_dir, events)
1252
+ data["businessGoal"] = inferred_goal or (NOT_AVAILABLE if for_replay else "")
1122
1253
 
1123
1254
  # commits
1124
1255
  project = find_project_root(change_dir)
1125
1256
  if not data.get("finalCommit") or str(data.get("finalCommit")).startswith("<"):
1126
- code, head, _ = git_run(project, "rev-parse", "HEAD")
1127
- if code == 0 and head:
1128
- data["finalCommit"] = head
1129
- elif existing and existing.get("finalCommit"):
1130
- data["finalCommit"] = existing["finalCommit"]
1131
- else:
1132
- data["finalCommit"] = NOT_AVAILABLE if for_replay else ""
1257
+ final_commit = _final_commit_from_sources(ledger, events, existing, project)
1258
+ data["finalCommit"] = final_commit or (NOT_AVAILABLE if for_replay else "")
1133
1259
 
1134
1260
  if not data.get("baseCommit") or str(data.get("baseCommit")).startswith("<"):
1135
1261
  if ledger and ledger.get("baseCommit"):
@@ -1235,18 +1361,43 @@ def collect_summary_data(
1235
1361
  if not isinstance(data.get("artifacts"), list):
1236
1362
  data["artifacts"] = []
1237
1363
  if not for_replay:
1238
- # Keep empty placeholder for build artifacts; event artifacts go to reportPipeline
1239
- pass
1364
+ data["artifacts"] = _artifacts_from_events(events)
1240
1365
 
1241
1366
  data["reviewSummary"] = _review_summary(change_dir, existing if for_replay else None)
1242
- data.setdefault("timeline", [])
1367
+ if not for_replay:
1368
+ data["timeline"] = _timeline_from_events(event_summary, events)
1369
+ else:
1370
+ data.setdefault("timeline", [])
1243
1371
  data.setdefault("uncommittedTestEvidence", [])
1244
1372
 
1245
- # Model-owned fields: leave empty placeholders on finalize; preserve on replay
1373
+ # Derive risks/actions from evidence. These fields are facts, not model prose.
1246
1374
  if not for_replay:
1247
- data["maintenanceNotes"] = []
1248
- data["knownRisks"] = []
1375
+ data["maintenanceNotes"] = [
1376
+ str(event.get("note") or event.get("message") or "")
1377
+ for event in events
1378
+ if event.get("type") == "decision" and (event.get("note") or event.get("message"))
1379
+ ]
1380
+ data["knownRisks"] = [
1381
+ {
1382
+ "phase": event.get("phase"),
1383
+ "severity": event.get("severity") or "unknown",
1384
+ "message": event.get("message") or event.get("note") or event.get("code") or "",
1385
+ }
1386
+ for event in events
1387
+ if event.get("type") == "issue"
1388
+ and not (
1389
+ event.get("phase") == "archive"
1390
+ and event.get("code") == "missing-command"
1391
+ )
1392
+ ]
1249
1393
  data["manualActions"] = []
1394
+ for name, value in (data.get("stageStatus") or {}).items():
1395
+ if value in {"BLOCKED", "BLOCKED_BY_ENV", "BLOCKED_BY_DBA", "NOT_RUN", "USER_SKIPPED"}:
1396
+ data["manualActions"].append({
1397
+ "stage": name,
1398
+ "status": value,
1399
+ "action": "补充或确认该阶段的真实验证证据",
1400
+ })
1250
1401
  else:
1251
1402
  data.setdefault("maintenanceNotes", [])
1252
1403
  data.setdefault("knownRisks", [])
@@ -1978,6 +2129,7 @@ def cmd_finalize(
1978
2129
  _restore_target,
1979
2130
  phase="archive",
1980
2131
  type_="phase.end",
2132
+ status="FAIL",
1981
2133
  note="finalize aborted; render failed; original preserved",
1982
2134
  )
1983
2135
  except OSError:
@@ -2117,6 +2269,7 @@ def cmd_finalize(
2117
2269
  _safe_append_restored,
2118
2270
  phase="archive",
2119
2271
  type_="phase.end",
2272
+ status="FAIL",
2120
2273
  note="finalize aborted; original preserved",
2121
2274
  )
2122
2275
  except OSError:
@@ -2168,7 +2321,12 @@ def cmd_finalize(
2168
2321
  if service_result.get("warning"):
2169
2322
  warnings.append(str(service_result["warning"]))
2170
2323
 
2171
- _safe_append(phase="archive", type_="phase.end", note="finalize complete")
2324
+ _safe_append(
2325
+ phase="archive",
2326
+ type_="phase.end",
2327
+ status="WARN" if warnings else "OK",
2328
+ note="finalize complete",
2329
+ )
2172
2330
 
2173
2331
  payload["ok"] = True
2174
2332
  payload["warnings"] = warnings
@@ -2,7 +2,7 @@
2
2
  """Harness events.ndjson writer and execution-log renderer (D2).
3
3
 
4
4
  Subcommands:
5
- append — append one schema_version 2 event, then auto-render execution-log.md
5
+ append — append one schema_version 3 event, then auto-render execution-log.md
6
6
  render — full re-render of logs/execution-log.md from events.ndjson
7
7
  summary — phase durations, event counts, and issue list (JSON)
8
8
 
@@ -42,7 +42,7 @@ EVENT_TYPES = frozenset(
42
42
  }
43
43
  )
44
44
 
45
- SCHEMA_VERSION = 2
45
+ SCHEMA_VERSION = 3
46
46
  HEADER_LINE = (
47
47
  "本文件由 harness_events.py 自动渲染,请勿手工编辑;事实源为 events.ndjson"
48
48
  )
@@ -62,6 +62,13 @@ OPTIONAL_FIELDS = (
62
62
  "message",
63
63
  "decision",
64
64
  "reason",
65
+ "run_id",
66
+ "attempt",
67
+ "executor_tool",
68
+ "executor_agent",
69
+ "executor_model",
70
+ "handoff_from_tool",
71
+ "handoff_reason",
65
72
  )
66
73
 
67
74
 
@@ -120,7 +127,7 @@ def duration_ms_between(start: Any, end: Any) -> int | None:
120
127
 
121
128
 
122
129
  def normalize_event(raw: dict[str, Any]) -> dict[str, Any]:
123
- """Normalize schema_version 1/2 events for rendering and summary."""
130
+ """Normalize schema_version 1/2/3 events for rendering and summary."""
124
131
  event = dict(raw)
125
132
  version = event.get("schema_version", 1)
126
133
  try:
@@ -273,6 +280,15 @@ def build_event(args: argparse.Namespace, existing: list[dict[str, Any]]) -> dic
273
280
  if value is None:
274
281
  continue
275
282
  event[field] = value
283
+ environment_defaults = {
284
+ "run_id": "HUNTER_HARNESS_RUN_ID",
285
+ "executor_tool": "HUNTER_HARNESS_TOOL",
286
+ "executor_agent": "HUNTER_HARNESS_AGENT",
287
+ "executor_model": "HUNTER_HARNESS_MODEL",
288
+ }
289
+ for field, env_name in environment_defaults.items():
290
+ if field not in event and os.environ.get(env_name):
291
+ event[field] = os.environ[env_name]
276
292
  if "note" not in event:
277
293
  event["note"] = ""
278
294
  return event
@@ -333,6 +349,46 @@ def group_events_by_phase(events: list[dict[str, Any]]) -> list[tuple[str, list[
333
349
  return [(phase, buckets[phase]) for phase in order]
334
350
 
335
351
 
352
+ def split_phase_attempts(phase_events: list[dict[str, Any]]) -> list[dict[str, Any]]:
353
+ """Split repeated starts into attempts and flag legacy events written after end."""
354
+ attempts: list[dict[str, Any]] = []
355
+ current: dict[str, Any] | None = None
356
+ next_attempt = 1
357
+ for event in phase_events:
358
+ if event.get("type") == "phase.start":
359
+ if current is not None:
360
+ current["warnings"].append("new phase.start before prior phase.end")
361
+ attempts.append(current)
362
+ raw_attempt = event.get("attempt")
363
+ attempt = raw_attempt if isinstance(raw_attempt, int) and raw_attempt > 0 else next_attempt
364
+ next_attempt = max(next_attempt, attempt + 1)
365
+ current = {"attempt": attempt, "events": [event], "warnings": []}
366
+ continue
367
+ if current is None:
368
+ if attempts:
369
+ prior_type = attempts[-1]["events"][-1].get("type")
370
+ attempts[-1]["events"].append(event)
371
+ if prior_type == "phase.end":
372
+ attempts[-1]["warnings"].append("event recorded after phase.end")
373
+ continue
374
+ raw_attempt = event.get("attempt")
375
+ attempt = raw_attempt if isinstance(raw_attempt, int) and raw_attempt > 0 else next_attempt
376
+ next_attempt = max(next_attempt, attempt + 1)
377
+ current = {
378
+ "attempt": attempt,
379
+ "events": [event],
380
+ "warnings": ["missing phase.start"],
381
+ }
382
+ else:
383
+ current["events"].append(event)
384
+ if event.get("type") == "phase.end":
385
+ attempts.append(current)
386
+ current = None
387
+ if current is not None:
388
+ attempts.append(current)
389
+ return attempts
390
+
391
+
336
392
  def phase_start_time(phase_events: list[dict[str, Any]]) -> str:
337
393
  for event in phase_events:
338
394
  if event.get("type") == "phase.start":
@@ -352,7 +408,9 @@ def phase_duration_ms(phase_events: list[dict[str, Any]]) -> int | None:
352
408
  elif etype == "phase.end":
353
409
  end_ts = event.get("timestamp")
354
410
  if start_ts and end_ts:
355
- return duration_ms_between(start_ts, end_ts)
411
+ stamps = [e.get("timestamp") for e in phase_events if e.get("timestamp")]
412
+ effective_end = stamps[-1] if stamps else end_ts
413
+ return duration_ms_between(start_ts, effective_end)
356
414
  # Fallback: first to last timestamp in the phase bucket.
357
415
  stamps = [e.get("timestamp") for e in phase_events if e.get("timestamp")]
358
416
  if len(stamps) >= 2:
@@ -400,7 +458,15 @@ def render_event_line(event: dict[str, Any]) -> list[str]:
400
458
  if etype == "phase.start":
401
459
  note = str(event.get("note") or "").strip()
402
460
  suffix = f" — {note}" if note else ""
403
- return [f"- phase.start @ {event.get('timestamp', '')}{suffix}"]
461
+ lines = [f"- phase.start @ {event.get('timestamp', '')}{suffix}"]
462
+ tool = str(event.get("executor_tool") or "").strip()
463
+ agent = str(event.get("executor_agent") or "").strip()
464
+ handoff = str(event.get("handoff_from_tool") or "").strip()
465
+ if handoff and tool:
466
+ lines.append(f"- 工具交接: {handoff} → {tool}")
467
+ elif tool:
468
+ lines.append(f"- 执行来源: {tool}" + (f" / {agent}" if agent else ""))
469
+ return lines
404
470
  if etype == "phase.end":
405
471
  note = str(event.get("note") or "").strip()
406
472
  suffix = f" — {note}" if note else ""
@@ -445,29 +511,34 @@ def render_execution_log(events: list[dict[str, Any]]) -> str:
445
511
  return "\n".join(lines)
446
512
 
447
513
  for phase, phase_events in group_events_by_phase(events):
448
- start = phase_start_time(phase_events) or "—"
449
- duration = phase_duration_ms(phase_events)
450
- lines.append(f"## {phase} — {start}")
451
- lines.append("")
452
- if duration is not None:
453
- lines.append(f"- 阶段耗时: {format_duration(duration)}")
454
- # Collapse consecutive commands into one table.
455
- buffer: list[dict[str, Any]] = []
456
-
457
- def flush_commands() -> None:
458
- nonlocal buffer
459
- if buffer:
460
- lines.extend(render_command_block(buffer))
461
- buffer = []
462
-
463
- for event in phase_events:
464
- if event.get("type") == "command":
465
- buffer.append(event)
466
- continue
514
+ attempts = split_phase_attempts(phase_events)
515
+ for attempt_record in attempts:
516
+ attempt_events = attempt_record["events"]
517
+ start = phase_start_time(attempt_events) or ""
518
+ duration = phase_duration_ms(attempt_events)
519
+ label = f"{phase}(尝试 {attempt_record['attempt']}" if len(attempts) > 1 else phase
520
+ lines.append(f"## {label} {start}")
521
+ lines.append("")
522
+ if duration is not None:
523
+ lines.append(f"- 阶段耗时: {format_duration(duration)}")
524
+ for warning in attempt_record["warnings"]:
525
+ lines.append(f"- 生命周期警告: {warning}")
526
+ buffer: list[dict[str, Any]] = []
527
+
528
+ def flush_commands() -> None:
529
+ nonlocal buffer
530
+ if buffer:
531
+ lines.extend(render_command_block(buffer))
532
+ buffer = []
533
+
534
+ for event in attempt_events:
535
+ if event.get("type") == "command":
536
+ buffer.append(event)
537
+ continue
538
+ flush_commands()
539
+ lines.extend(render_event_line(event))
467
540
  flush_commands()
468
- lines.extend(render_event_line(event))
469
- flush_commands()
470
- lines.append("")
541
+ lines.append("")
471
542
 
472
543
  return "\n".join(lines).rstrip() + "\n"
473
544
 
@@ -485,21 +556,37 @@ def build_summary(change_dir: Path, events: list[dict[str, Any]]) -> dict[str, A
485
556
  issues: list[dict[str, Any]] = []
486
557
 
487
558
  for phase, phase_events in group_events_by_phase(events):
488
- start_ts = None
489
- end_ts = None
490
- for event in phase_events:
491
- if event.get("type") == "phase.start" and start_ts is None:
492
- start_ts = event.get("timestamp")
493
- elif event.get("type") == "phase.end":
494
- end_ts = event.get("timestamp")
495
- duration = None
496
- if start_ts and end_ts:
497
- duration = duration_ms_between(start_ts, end_ts)
559
+ attempt_records: list[dict[str, Any]] = []
560
+ for record in split_phase_attempts(phase_events):
561
+ attempt_events = record["events"]
562
+ starts = [e for e in attempt_events if e.get("type") == "phase.start"]
563
+ ends = [e for e in attempt_events if e.get("type") == "phase.end"]
564
+ start_ts = starts[0].get("timestamp") if starts else None
565
+ end_ts = ends[-1].get("timestamp") if ends else None
566
+ provenance = starts[0] if starts else (attempt_events[0] if attempt_events else {})
567
+ attempt_records.append({
568
+ "attempt": record["attempt"],
569
+ "event_count": len(attempt_events),
570
+ "started_at": start_ts,
571
+ "ended_at": end_ts,
572
+ "duration_ms": phase_duration_ms(attempt_events),
573
+ "status": ends[-1].get("status") if ends else None,
574
+ "run_id": provenance.get("run_id"),
575
+ "executor_tool": provenance.get("executor_tool"),
576
+ "executor_agent": provenance.get("executor_agent"),
577
+ "handoff_from_tool": provenance.get("handoff_from_tool"),
578
+ "warnings": record["warnings"],
579
+ })
580
+ durations = [a["duration_ms"] for a in attempt_records if a["duration_ms"] is not None]
581
+ first = attempt_records[0] if attempt_records else {}
582
+ last = attempt_records[-1] if attempt_records else {}
498
583
  phases[phase] = {
499
584
  "event_count": len(phase_events),
500
- "started_at": start_ts,
501
- "ended_at": end_ts,
502
- "duration_ms": duration,
585
+ "started_at": first.get("started_at"),
586
+ "ended_at": last.get("ended_at"),
587
+ "duration_ms": sum(durations) if durations else None,
588
+ "status": last.get("status"),
589
+ "attempts": attempt_records,
503
590
  }
504
591
 
505
592
  for event in events:
@@ -649,6 +736,13 @@ def build_parser() -> argparse.ArgumentParser:
649
736
  p_append.add_argument("--message", default=None)
650
737
  p_append.add_argument("--decision", default=None)
651
738
  p_append.add_argument("--reason", default=None)
739
+ p_append.add_argument("--run-id", default=None)
740
+ p_append.add_argument("--attempt", type=int, default=None)
741
+ p_append.add_argument("--executor-tool", default=None)
742
+ p_append.add_argument("--executor-agent", default=None)
743
+ p_append.add_argument("--executor-model", default=None)
744
+ p_append.add_argument("--handoff-from-tool", default=None)
745
+ p_append.add_argument("--handoff-reason", default=None)
652
746
  p_append.set_defaults(func=cmd_append)
653
747
 
654
748
  p_render = sub.add_parser(