@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
@@ -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(
@@ -313,7 +313,23 @@ def _parse_scalar(val: str) -> Any:
313
313
  return val
314
314
 
315
315
 
316
- def cmd_check_agents(skills_root: Path, agent: str) -> dict[str, Any]:
316
+ def _resolve_agents_root(skills_root: Path, agents_root: Path | None) -> tuple[Path | None, str | None]:
317
+ """Resolve agent definitions without confusing a skills root with its sibling."""
318
+ if agents_root is not None:
319
+ return agents_root.resolve(), None
320
+ if skills_root.name == "skills" and skills_root.parent.name in (".claude", ".codebuddy"):
321
+ return (skills_root.parent / "agents").resolve(), None
322
+ if skills_root.name == "skills" and skills_root.parent.name in (".agents", ".cursor"):
323
+ return None, "CUSTOM_AGENTS_UNSUPPORTED"
324
+ return (skills_root / "agents").resolve(), None
325
+
326
+
327
+ def cmd_check_agents(
328
+ skills_root: Path,
329
+ agent: str,
330
+ *,
331
+ agents_root: Path | None = None,
332
+ ) -> dict[str, Any]:
317
333
  skills_root = skills_root.resolve()
318
334
  agent_name = agent.strip()
319
335
  if not agent_name:
@@ -325,9 +341,23 @@ def cmd_check_agents(skills_root: Path, agent: str) -> dict[str, Any]:
325
341
  "reason": "agent name is empty",
326
342
  }
327
343
 
344
+ resolved_agents_root, capability_error = _resolve_agents_root(skills_root, agents_root)
345
+ if capability_error is not None:
346
+ return {
347
+ "ok": True,
348
+ "action": "check-agents",
349
+ "agent": agent_name,
350
+ "usable": False,
351
+ "definitionValid": False,
352
+ "runtimeSupported": False,
353
+ "reasonCode": capability_error,
354
+ "reason": "current adapter runtime does not support custom agent definitions; use main-session fallback",
355
+ }
356
+
357
+ assert resolved_agents_root is not None
328
358
  # Accept bare name or with .md
329
359
  filename = agent_name if agent_name.endswith(".md") else f"{agent_name}.md"
330
- agent_path = skills_root / "agents" / filename
360
+ agent_path = resolved_agents_root / filename
331
361
 
332
362
  if not agent_path.is_file():
333
363
  return {
@@ -335,7 +365,11 @@ def cmd_check_agents(skills_root: Path, agent: str) -> dict[str, Any]:
335
365
  "action": "check-agents",
336
366
  "agent": agent_name,
337
367
  "path": str(agent_path),
368
+ "agentsRoot": str(resolved_agents_root),
338
369
  "usable": False,
370
+ "definitionValid": False,
371
+ "runtimeSupported": True,
372
+ "reasonCode": "AGENT_DEFINITION_NOT_FOUND",
339
373
  "reason": f"agent file not found: {agent_path}",
340
374
  }
341
375
 
@@ -390,7 +424,11 @@ def cmd_check_agents(skills_root: Path, agent: str) -> dict[str, Any]:
390
424
  "action": "check-agents",
391
425
  "agent": agent_name,
392
426
  "path": str(agent_path),
427
+ "agentsRoot": str(resolved_agents_root),
393
428
  "usable": True,
429
+ "definitionValid": True,
430
+ "runtimeSupported": True,
431
+ "reasonCode": "READY",
394
432
  "reason": "agent file exists; frontmatter parsed; tools declared",
395
433
  "name": name_field,
396
434
  "tools": tools,
@@ -431,6 +469,7 @@ def build_parser() -> argparse.ArgumentParser:
431
469
 
432
470
  p_agents = sub.add_parser("check-agents", help="Validate agent definition usability")
433
471
  p_agents.add_argument("--skills-root", required=True, type=Path)
472
+ p_agents.add_argument("--agents-root", type=Path)
434
473
  p_agents.add_argument("--agent", required=True)
435
474
  p_agents.add_argument("--json", action="store_true")
436
475
 
@@ -459,7 +498,11 @@ def main(argv: list[str] | None = None) -> int:
459
498
  )
460
499
  return emit_json(result, ok=bool(result.get("ok", False)))
461
500
  if args.command == "check-agents":
462
- result = cmd_check_agents(args.skills_root, args.agent)
501
+ result = cmd_check_agents(
502
+ args.skills_root,
503
+ args.agent,
504
+ agents_root=args.agents_root,
505
+ )
463
506
  return emit_json(result, ok=True)
464
507
 
465
508
  parser.error(f"unknown command: {args.command}")
@@ -223,8 +223,8 @@ def _java_commands(reactor_modules: list[str], pom_hash: str) -> dict[str, Any]:
223
223
  "basis": dict(basis),
224
224
  },
225
225
  "package": {
226
- "command": "mvn -f pom.xml package '-Dmaven.test.skip=true'",
227
- "argvTemplate": ["mvn", "-f", "pom.xml", "package", "-Dmaven.test.skip=true"],
226
+ "command": "mvn -f pom.xml clean package '-Dmaven.test.skip=true'",
227
+ "argvTemplate": ["mvn", "-f", "pom.xml", "clean", "package", "-Dmaven.test.skip=true"],
228
228
  "scope": "module",
229
229
  "inputs": _cmd_inputs(reactor_modules, pom=True, main=True, test=True),
230
230
  "coverage": "package",
@@ -12,9 +12,11 @@ Python 3.10+ stdlib only. UTF-8 without BOM. Windows path friendly.
12
12
  from __future__ import annotations
13
13
 
14
14
  import datetime as dt
15
+ import argparse
15
16
  import hashlib
16
17
  import json
17
18
  import os
19
+ import subprocess
18
20
  import sys
19
21
  from pathlib import Path
20
22
  from typing import Any
@@ -177,3 +179,149 @@ def refresh_segments(
177
179
  refreshed["segments"] = refreshed_segments
178
180
  refreshed["generatedAt"] = now_iso()
179
181
  return refreshed
182
+
183
+
184
+ def _git(project: Path, *args: str) -> str:
185
+ proc = subprocess.run(
186
+ ["git", "-C", str(project), *args],
187
+ capture_output=True,
188
+ text=True,
189
+ encoding="utf-8",
190
+ errors="replace",
191
+ check=False,
192
+ )
193
+ return proc.stdout.strip() if proc.returncode == 0 else ""
194
+
195
+
196
+ def _existing_files(paths: list[Path]) -> list[str]:
197
+ return sorted({str(path.resolve()) for path in paths if path.is_file()})
198
+
199
+
200
+ def discover_segment_files(
201
+ project: Path, change_dir: Path, *, base: str = "", head: str = "HEAD"
202
+ ) -> dict[str, list[str]]:
203
+ """Discover the compact inputs shared by plan/run/test/review/submit."""
204
+ project = project.resolve()
205
+ rules = [project / "AGENTS.md", project / "CLAUDE.md"]
206
+ for pattern in (
207
+ ".claude/rules/**/*",
208
+ ".cursor/rules/**/*",
209
+ ".codebuddy/rules/**/*",
210
+ ".codebuddy/.rules/**/*",
211
+ ):
212
+ rules.extend(project.glob(pattern))
213
+
214
+ profile = [
215
+ project / ".harness/config/build-profile.json",
216
+ project / ".harness/context-index.json",
217
+ project / "project.yaml",
218
+ project / "harness.json",
219
+ ]
220
+ codebase_map = list((project / ".harness/codebase/map").glob("**/*"))
221
+ knowledge = [
222
+ project / ".harness/knowledge/index.json",
223
+ project / ".harness/knowledge/status.json",
224
+ project / ".harness/knowledge/config.json",
225
+ ]
226
+ change_inputs: list[Path] = []
227
+ for folder in ("spec", "plans", "meta"):
228
+ change_inputs.extend((change_dir / folder).glob("**/*"))
229
+ change_inputs = [path for path in change_inputs if path != snapshot_path(change_dir)]
230
+
231
+ changed_names: list[str] = []
232
+ if base:
233
+ raw = _git(project, "diff", "--name-only", "--diff-filter=ACMRT", f"{base}..{head}")
234
+ changed_names = [line for line in raw.splitlines() if line.strip()]
235
+ code = [project / name for name in changed_names]
236
+ return {
237
+ "profile": _existing_files(profile),
238
+ "rules": _existing_files(rules),
239
+ "map": _existing_files(codebase_map),
240
+ "knowledge": _existing_files(knowledge),
241
+ "change": _existing_files(change_inputs),
242
+ "code": _existing_files(code),
243
+ }
244
+
245
+
246
+ def capture_current_state(
247
+ *,
248
+ project: Path,
249
+ change_dir: Path,
250
+ change_name: str,
251
+ worktree_root: Path,
252
+ base: str = "",
253
+ head: str = "HEAD",
254
+ ) -> tuple[dict[str, Any], list[str]]:
255
+ resolved_head = _git(project, "rev-parse", head) or head
256
+ resolved_base = _git(project, "rev-parse", base) if base else ""
257
+ files = discover_segment_files(project, change_dir, base=resolved_base, head=resolved_head)
258
+ previous = load_snapshot(change_dir)
259
+ fresh = capture_snapshot(
260
+ change_dir,
261
+ change_name=change_name,
262
+ project=project,
263
+ worktree_root=worktree_root,
264
+ base=resolved_base,
265
+ head=resolved_head,
266
+ segment_files=files,
267
+ )
268
+ changed: list[str] = []
269
+ if previous:
270
+ old_segments = previous.get("segments") or {}
271
+ for name, segment in fresh["segments"].items():
272
+ old = old_segments.get(name)
273
+ if isinstance(old, dict) and old.get("fingerprint") == segment.get("fingerprint"):
274
+ segment["capturedAt"] = old.get("capturedAt", segment["capturedAt"])
275
+ else:
276
+ changed.append(name)
277
+ else:
278
+ changed = list(fresh["segments"])
279
+ write_snapshot(change_dir, fresh)
280
+ return fresh, changed
281
+
282
+
283
+ def build_parser() -> argparse.ArgumentParser:
284
+ parser = argparse.ArgumentParser(description="Capture reusable Harness state")
285
+ sub = parser.add_subparsers(dest="command", required=True)
286
+ capture = sub.add_parser("capture", help="capture or refresh changed segments")
287
+ capture.add_argument("--project", default=".")
288
+ capture.add_argument("--change-dir", required=True)
289
+ capture.add_argument("--change-name")
290
+ capture.add_argument("--worktree-root")
291
+ capture.add_argument("--base", default="")
292
+ capture.add_argument("--head", default="HEAD")
293
+ capture.add_argument("--json", action="store_true")
294
+ return parser
295
+
296
+
297
+ def main(argv: list[str] | None = None) -> int:
298
+ args = build_parser().parse_args(argv)
299
+ project = Path(args.project).resolve()
300
+ change_dir = Path(args.change_dir).resolve()
301
+ name = args.change_name or change_dir.name
302
+ worktree = Path(args.worktree_root).resolve() if args.worktree_root else project
303
+ snapshot, changed = capture_current_state(
304
+ project=project,
305
+ change_dir=change_dir,
306
+ change_name=name,
307
+ worktree_root=worktree,
308
+ base=args.base,
309
+ head=args.head,
310
+ )
311
+ result = {
312
+ "ok": True,
313
+ "path": str(snapshot_path(change_dir)),
314
+ "changedSegments": changed,
315
+ "reusedSegments": sorted(set(snapshot["segments"]) - set(changed)),
316
+ "git": snapshot["git"],
317
+ }
318
+ if args.json:
319
+ print(json.dumps(result, ensure_ascii=False))
320
+ else:
321
+ print(f"state snapshot: {result['path']}")
322
+ print(f"changed: {', '.join(changed) if changed else 'none'}")
323
+ return 0
324
+
325
+
326
+ if __name__ == "__main__":
327
+ raise SystemExit(main())
@@ -2,5 +2,5 @@
2
2
  "schemaVersion": 1,
3
3
  "overlay": "java",
4
4
  "agent": "cursor",
5
- "coreHash": "fa41b1de952e802a"
5
+ "coreHash": "a7decc4da50d0b7f"
6
6
  }
@@ -2,7 +2,7 @@
2
2
  name: harness-apidoc
3
3
  description: 从 Java 后端代码生成结构化前端 API 文档(Controller/VO/ErrorCode)。使用场景:接口文档、前端接口文档、API 文档生成
4
4
  ---
5
- <!-- generated by harness_deploy.py; core=fa41b1de952e802a; overlay=java; agent=cursor; do not edit -->
5
+ <!-- generated by harness_deploy.py; core=a7decc4da50d0b7f; overlay=java; agent=cursor; do not edit -->
6
6
  # harness-apidoc — 前端接口文档(Java overlay)
7
7
 
8
8
  ## Purpose
@@ -24,6 +24,7 @@ description: 从 Java 后端代码生成结构化前端 API 文档(Controller/
24
24
  6. **implementation-detail** — 自适应执行参考;legacy 缺失 🟡WARN,不阻断
25
25
  7. **test-scenarios** — 测试真相源:`plans/<change>-test-scenarios.md`
26
26
  8. **禁止读取 `docs/superpowers/` 作为正式输入** — 旧草稿仅人工线索
27
+ 9. **每个 skill 边界只刷新一次状态快照** — 先运行 `python <skills-root>/scripts/harness_state.py capture --project . --change-dir ".harness/changes/<change-name>" --base <baseCommit> --json`;`changedSegments=[]` 时复用已有 profile/rules/map/knowledge/change/code 指纹,不再重复跑全量 `harness-sync`、知识重建或代码库扫描。只有对应 segment 变化或缺失时才定向刷新;快照不能替代源码读取和验证门禁。
27
28
 
28
29
  状态目录分层:新路径优先,旧路径兼容 → [[../protocols/state-layout-protocol.md|state-layout-protocol]]
29
30
 
@@ -57,7 +58,7 @@ description: 从 Java 后端代码生成结构化前端 API 文档(Controller/
57
58
 
58
59
  ## 执行日志
59
60
 
60
- `events.ndjson` 为唯一事实源(schema_version 2,`note` 承载人类可读摘要);`logs/execution-log.md` 由 `harness_events.py` 渲染,**禁止用 Write/Edit 直接维护**。直接修改的内容会在 `phase.end` 或 finalize 时被完整重建覆盖,属于数据丢失;需要保留的详情必须进入事件 `note`。结构 → [[../protocols/report-pipeline-protocol.md|report-pipeline-protocol]]
61
+ `events.ndjson` 为唯一事实源(schema_version 3,兼容读取 v1/v2;`note` 承载人类可读摘要);`logs/execution-log.md` 由 `harness_events.py` 渲染,**禁止用 Write/Edit 直接维护**。直接修改的内容会在 `phase.end` 或 finalize 时被完整重建覆盖,属于数据丢失;需要保留的详情必须进入事件 `note`。结构 → [[../protocols/report-pipeline-protocol.md|report-pipeline-protocol]]
61
62
 
62
63
  ```powershell
63
64
  python <skills-root>/scripts/harness_events.py append --change-dir ".harness/changes/<change-name>" --phase <phase> --type phase.start --note "<触发指令>"
@@ -66,3 +67,5 @@ python <skills-root>/scripts/harness_events.py append --change-dir ".harness/cha
66
67
  > **脚本接线**:`harness_events.py append`;`harness_archive.py finalize`;`harness_preflight.py check`;`harness_ledger.py can-reuse`;`harness_service.py ensure/stop`(须 `--files`/`serviceStart.inputFiles`)。JSON 输出按 D13 护栏解读。
67
68
 
68
69
  > **Task 4 §6.1 写入契约**:普通 `append` = 加锁 -> 追加一行 -> fsync -> 解锁,**不 load 历史、不渲染**(O(1),跨进程锁 `events.ndjson.lock`,UUID 用完整 `uuid4().hex` 无需去重扫描)。仅 `--type phase.end` append 在追加成功后渲染一次 `execution-log.md`;显式 `harness_events.py render` 随时从完整 events 重建;`harness_archive.py finalize` 在 collect 前强制 render 一次。高频 command append 期间 log 可能滞后,phase 边界保持最新。
70
+
71
+ 每次阶段结束必须写 `--status OK|WARN|FAIL|BLOCKED`;重试同一阶段时增加 `--attempt <n>`,不得覆盖或伪装成一次执行。跨工具继续执行时写 `--executor-tool <codex|claude-code|codebuddy|cursor>`,并在接棒事件写 `--handoff-from-tool` / `--handoff-reason`;也可由 `HUNTER_HARNESS_TOOL/AGENT/MODEL/RUN_ID` 环境变量统一注入。
@@ -3,7 +3,7 @@ name: harness-archive
3
3
  description: 归档所有变更产出(计划/测试报告/审查/SQL/API文档)到
4
4
  .harness/archive/,含归档元数据和可视化最终总结。使用场景:归档、完成归档、收尾、打包产出物
5
5
  ---
6
- <!-- generated by harness_deploy.py; core=fa41b1de952e802a; overlay=java; agent=cursor; do not edit -->
6
+ <!-- generated by harness_deploy.py; core=a7decc4da50d0b7f; overlay=java; agent=cursor; do not edit -->
7
7
  # harness-archive — 变更归档
8
8
 
9
9
  ## Purpose
@@ -49,6 +49,7 @@ description: 归档所有变更产出(计划/测试报告/审查/SQL/API文档
49
49
  6. **implementation-detail** — 自适应执行参考;legacy 缺失 🟡WARN,不阻断
50
50
  7. **test-scenarios** — 测试真相源:`plans/<change>-test-scenarios.md`
51
51
  8. **禁止读取 `docs/superpowers/` 作为正式输入** — 旧草稿仅人工线索
52
+ 9. **每个 skill 边界只刷新一次状态快照** — 先运行 `python <skills-root>/scripts/harness_state.py capture --project . --change-dir ".harness/changes/<change-name>" --base <baseCommit> --json`;`changedSegments=[]` 时复用已有 profile/rules/map/knowledge/change/code 指纹,不再重复跑全量 `harness-sync`、知识重建或代码库扫描。只有对应 segment 变化或缺失时才定向刷新;快照不能替代源码读取和验证门禁。
52
53
 
53
54
  状态目录分层:新路径优先,旧路径兼容 → [[../protocols/state-layout-protocol.md|state-layout-protocol]]
54
55
 
@@ -185,7 +186,7 @@ git 命令通过 `powershell.exe -Command "..."` 执行;archive-meta.md 和 fi
185
186
 
186
187
  ## 执行日志
187
188
 
188
- `events.ndjson` 为唯一事实源(schema_version 2,`note` 承载人类可读摘要);`logs/execution-log.md` 由 `harness_events.py` 渲染,**禁止用 Write/Edit 直接维护**。直接修改的内容会在 `phase.end` 或 finalize 时被完整重建覆盖,属于数据丢失;需要保留的详情必须进入事件 `note`。结构 → [[../protocols/report-pipeline-protocol.md|report-pipeline-protocol]]
189
+ `events.ndjson` 为唯一事实源(schema_version 3,兼容读取 v1/v2;`note` 承载人类可读摘要);`logs/execution-log.md` 由 `harness_events.py` 渲染,**禁止用 Write/Edit 直接维护**。直接修改的内容会在 `phase.end` 或 finalize 时被完整重建覆盖,属于数据丢失;需要保留的详情必须进入事件 `note`。结构 → [[../protocols/report-pipeline-protocol.md|report-pipeline-protocol]]
189
190
 
190
191
  ```powershell
191
192
  python <skills-root>/scripts/harness_events.py append --change-dir ".harness/changes/<change-name>" --phase <phase> --type phase.start --note "<触发指令>"
@@ -194,3 +195,5 @@ python <skills-root>/scripts/harness_events.py append --change-dir ".harness/cha
194
195
  > **脚本接线**:`harness_events.py append`;`harness_archive.py finalize`;`harness_preflight.py check`;`harness_ledger.py can-reuse`;`harness_service.py ensure/stop`(须 `--files`/`serviceStart.inputFiles`)。JSON 输出按 D13 护栏解读。
195
196
 
196
197
  > **Task 4 §6.1 写入契约**:普通 `append` = 加锁 -> 追加一行 -> fsync -> 解锁,**不 load 历史、不渲染**(O(1),跨进程锁 `events.ndjson.lock`,UUID 用完整 `uuid4().hex` 无需去重扫描)。仅 `--type phase.end` append 在追加成功后渲染一次 `execution-log.md`;显式 `harness_events.py render` 随时从完整 events 重建;`harness_archive.py finalize` 在 collect 前强制 render 一次。高频 command append 期间 log 可能滞后,phase 边界保持最新。
198
+
199
+ 每次阶段结束必须写 `--status OK|WARN|FAIL|BLOCKED`;重试同一阶段时增加 `--attempt <n>`,不得覆盖或伪装成一次执行。跨工具继续执行时写 `--executor-tool <codex|claude-code|codebuddy|cursor>`,并在接棒事件写 `--handoff-from-tool` / `--handoff-reason`;也可由 `HUNTER_HARNESS_TOOL/AGENT/MODEL/RUN_ID` 环境变量统一注入。
@@ -92,4 +92,4 @@ powershell.exe -NoProfile -Command "& '<node-path>' 'harness-skills/harness-arch
92
92
 
93
93
  ## 执行日志记录
94
94
 
95
- 归档只向 `events.ndjson` 追加事件(schema_version 2);`logs/execution-log.md` 由 `harness_events.py append` 自动渲染,禁止手工 Edit。事件类型与脚本用法见 SKILL.md `## 执行日志` 与 `../protocols/report-pipeline-protocol.md`。
95
+ 归档只向 `events.ndjson` 追加事件(schema_version 3,兼容读取 v1/v2);`logs/execution-log.md` 由 `harness_events.py append` 自动渲染,禁止手工 Edit。事件类型与脚本用法见 SKILL.md `## 执行日志` 与 `../protocols/report-pipeline-protocol.md`。