@hunter-harness/workflow-harness 0.2.54 → 0.2.55

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 (186) hide show
  1. package/harness/bundles/general/claude-code/.harness-build.json +1 -1
  2. package/harness/bundles/general/claude-code/harness-archive/SKILL.md +1 -1
  3. package/harness/bundles/general/claude-code/harness-codebase-map/SKILL.md +1 -1
  4. package/harness/bundles/general/claude-code/harness-knowledge-ingest/SKILL.md +1 -1
  5. package/harness/bundles/general/claude-code/harness-knowledge-query/SKILL.md +1 -1
  6. package/harness/bundles/general/claude-code/harness-plan/SKILL.md +2 -2
  7. package/harness/bundles/general/claude-code/harness-plan/checklist.md +2 -1
  8. package/harness/bundles/general/claude-code/harness-review/SKILL.md +6 -4
  9. package/harness/bundles/general/claude-code/harness-run/SKILL.md +4 -4
  10. package/harness/bundles/general/claude-code/harness-run/reference.md +2 -2
  11. package/harness/bundles/general/claude-code/harness-submit/SKILL.md +1 -1
  12. package/harness/bundles/general/claude-code/harness-sync/SKILL.md +1 -1
  13. package/harness/bundles/general/claude-code/harness-test/SKILL.md +5 -3
  14. package/harness/bundles/general/claude-code/harness-test/reference.md +1 -1
  15. package/harness/bundles/general/claude-code/scripts/harness_archive.py +46 -7
  16. package/harness/bundles/general/claude-code/scripts/harness_context.py +71 -22
  17. package/harness/bundles/general/claude-code/scripts/harness_events.py +40 -1
  18. package/harness/bundles/general/claude-code/scripts/harness_events_sync.py +93 -14
  19. package/harness/bundles/general/claude-code/scripts/harness_gate.py +165 -1
  20. package/harness/bundles/general/claude-code/scripts/harness_ledger.py +2 -0
  21. package/harness/bundles/general/claude-code/scripts/harness_plan_finalize.py +2 -0
  22. package/harness/bundles/general/claude-code/scripts/harness_test_guard.py +37 -44
  23. package/harness/bundles/general/codebuddy/.harness-build.json +1 -1
  24. package/harness/bundles/general/codebuddy/harness-archive/SKILL.md +1 -1
  25. package/harness/bundles/general/codebuddy/harness-codebase-map/SKILL.md +1 -1
  26. package/harness/bundles/general/codebuddy/harness-knowledge-ingest/SKILL.md +1 -1
  27. package/harness/bundles/general/codebuddy/harness-knowledge-query/SKILL.md +1 -1
  28. package/harness/bundles/general/codebuddy/harness-plan/SKILL.md +2 -2
  29. package/harness/bundles/general/codebuddy/harness-plan/checklist.md +2 -1
  30. package/harness/bundles/general/codebuddy/harness-review/SKILL.md +6 -4
  31. package/harness/bundles/general/codebuddy/harness-run/SKILL.md +4 -4
  32. package/harness/bundles/general/codebuddy/harness-run/reference.md +2 -2
  33. package/harness/bundles/general/codebuddy/harness-submit/SKILL.md +1 -1
  34. package/harness/bundles/general/codebuddy/harness-sync/SKILL.md +1 -1
  35. package/harness/bundles/general/codebuddy/harness-test/SKILL.md +5 -3
  36. package/harness/bundles/general/codebuddy/harness-test/reference.md +1 -1
  37. package/harness/bundles/general/codebuddy/scripts/harness_archive.py +46 -7
  38. package/harness/bundles/general/codebuddy/scripts/harness_context.py +71 -22
  39. package/harness/bundles/general/codebuddy/scripts/harness_events.py +40 -1
  40. package/harness/bundles/general/codebuddy/scripts/harness_events_sync.py +93 -14
  41. package/harness/bundles/general/codebuddy/scripts/harness_gate.py +165 -1
  42. package/harness/bundles/general/codebuddy/scripts/harness_ledger.py +2 -0
  43. package/harness/bundles/general/codebuddy/scripts/harness_plan_finalize.py +2 -0
  44. package/harness/bundles/general/codebuddy/scripts/harness_test_guard.py +37 -44
  45. package/harness/bundles/general/codex/.harness-build.json +1 -1
  46. package/harness/bundles/general/codex/harness-archive/SKILL.md +1 -1
  47. package/harness/bundles/general/codex/harness-codebase-map/SKILL.md +1 -1
  48. package/harness/bundles/general/codex/harness-knowledge-ingest/SKILL.md +1 -1
  49. package/harness/bundles/general/codex/harness-knowledge-query/SKILL.md +1 -1
  50. package/harness/bundles/general/codex/harness-plan/SKILL.md +2 -2
  51. package/harness/bundles/general/codex/harness-plan/checklist.md +2 -1
  52. package/harness/bundles/general/codex/harness-review/SKILL.md +3 -3
  53. package/harness/bundles/general/codex/harness-run/SKILL.md +4 -4
  54. package/harness/bundles/general/codex/harness-run/reference.md +2 -2
  55. package/harness/bundles/general/codex/harness-submit/SKILL.md +1 -1
  56. package/harness/bundles/general/codex/harness-sync/SKILL.md +1 -1
  57. package/harness/bundles/general/codex/harness-test/SKILL.md +5 -3
  58. package/harness/bundles/general/codex/harness-test/reference.md +1 -1
  59. package/harness/bundles/general/codex/scripts/harness_archive.py +46 -7
  60. package/harness/bundles/general/codex/scripts/harness_context.py +71 -22
  61. package/harness/bundles/general/codex/scripts/harness_events.py +40 -1
  62. package/harness/bundles/general/codex/scripts/harness_events_sync.py +93 -14
  63. package/harness/bundles/general/codex/scripts/harness_gate.py +165 -1
  64. package/harness/bundles/general/codex/scripts/harness_ledger.py +2 -0
  65. package/harness/bundles/general/codex/scripts/harness_plan_finalize.py +2 -0
  66. package/harness/bundles/general/codex/scripts/harness_test_guard.py +37 -44
  67. package/harness/bundles/general/cursor/.harness-build.json +1 -1
  68. package/harness/bundles/general/cursor/harness-archive/SKILL.md +1 -1
  69. package/harness/bundles/general/cursor/harness-codebase-map/SKILL.md +1 -1
  70. package/harness/bundles/general/cursor/harness-knowledge-ingest/SKILL.md +1 -1
  71. package/harness/bundles/general/cursor/harness-knowledge-query/SKILL.md +1 -1
  72. package/harness/bundles/general/cursor/harness-plan/SKILL.md +2 -2
  73. package/harness/bundles/general/cursor/harness-plan/checklist.md +2 -1
  74. package/harness/bundles/general/cursor/harness-review/SKILL.md +3 -3
  75. package/harness/bundles/general/cursor/harness-run/SKILL.md +4 -4
  76. package/harness/bundles/general/cursor/harness-run/reference.md +2 -2
  77. package/harness/bundles/general/cursor/harness-submit/SKILL.md +1 -1
  78. package/harness/bundles/general/cursor/harness-sync/SKILL.md +1 -1
  79. package/harness/bundles/general/cursor/harness-test/SKILL.md +5 -3
  80. package/harness/bundles/general/cursor/harness-test/reference.md +1 -1
  81. package/harness/bundles/general/cursor/scripts/harness_archive.py +46 -7
  82. package/harness/bundles/general/cursor/scripts/harness_context.py +71 -22
  83. package/harness/bundles/general/cursor/scripts/harness_events.py +40 -1
  84. package/harness/bundles/general/cursor/scripts/harness_events_sync.py +93 -14
  85. package/harness/bundles/general/cursor/scripts/harness_gate.py +165 -1
  86. package/harness/bundles/general/cursor/scripts/harness_ledger.py +2 -0
  87. package/harness/bundles/general/cursor/scripts/harness_plan_finalize.py +2 -0
  88. package/harness/bundles/general/cursor/scripts/harness_test_guard.py +37 -44
  89. package/harness/bundles/java/claude-code/.harness-build.json +1 -1
  90. package/harness/bundles/java/claude-code/harness-apidoc/SKILL.md +1 -1
  91. package/harness/bundles/java/claude-code/harness-archive/SKILL.md +1 -1
  92. package/harness/bundles/java/claude-code/harness-codebase-map/SKILL.md +1 -1
  93. package/harness/bundles/java/claude-code/harness-knowledge-ingest/SKILL.md +1 -1
  94. package/harness/bundles/java/claude-code/harness-knowledge-query/SKILL.md +1 -1
  95. package/harness/bundles/java/claude-code/harness-package/SKILL.md +1 -1
  96. package/harness/bundles/java/claude-code/harness-plan/SKILL.md +2 -2
  97. package/harness/bundles/java/claude-code/harness-plan/checklist.md +2 -1
  98. package/harness/bundles/java/claude-code/harness-review/SKILL.md +6 -4
  99. package/harness/bundles/java/claude-code/harness-run/SKILL.md +4 -4
  100. package/harness/bundles/java/claude-code/harness-submit/SKILL.md +1 -1
  101. package/harness/bundles/java/claude-code/harness-sync/SKILL.md +1 -1
  102. package/harness/bundles/java/claude-code/harness-test/SKILL.md +5 -3
  103. package/harness/bundles/java/claude-code/scripts/harness_archive.py +46 -7
  104. package/harness/bundles/java/claude-code/scripts/harness_context.py +71 -22
  105. package/harness/bundles/java/claude-code/scripts/harness_events.py +40 -1
  106. package/harness/bundles/java/claude-code/scripts/harness_events_sync.py +93 -14
  107. package/harness/bundles/java/claude-code/scripts/harness_gate.py +165 -1
  108. package/harness/bundles/java/claude-code/scripts/harness_ledger.py +2 -0
  109. package/harness/bundles/java/claude-code/scripts/harness_plan_finalize.py +2 -0
  110. package/harness/bundles/java/claude-code/scripts/harness_test_guard.py +37 -44
  111. package/harness/bundles/java/codebuddy/.harness-build.json +1 -1
  112. package/harness/bundles/java/codebuddy/harness-apidoc/SKILL.md +1 -1
  113. package/harness/bundles/java/codebuddy/harness-archive/SKILL.md +1 -1
  114. package/harness/bundles/java/codebuddy/harness-codebase-map/SKILL.md +1 -1
  115. package/harness/bundles/java/codebuddy/harness-knowledge-ingest/SKILL.md +1 -1
  116. package/harness/bundles/java/codebuddy/harness-knowledge-query/SKILL.md +1 -1
  117. package/harness/bundles/java/codebuddy/harness-package/SKILL.md +1 -1
  118. package/harness/bundles/java/codebuddy/harness-plan/SKILL.md +2 -2
  119. package/harness/bundles/java/codebuddy/harness-plan/checklist.md +2 -1
  120. package/harness/bundles/java/codebuddy/harness-review/SKILL.md +6 -4
  121. package/harness/bundles/java/codebuddy/harness-run/SKILL.md +4 -4
  122. package/harness/bundles/java/codebuddy/harness-submit/SKILL.md +1 -1
  123. package/harness/bundles/java/codebuddy/harness-sync/SKILL.md +1 -1
  124. package/harness/bundles/java/codebuddy/harness-test/SKILL.md +5 -3
  125. package/harness/bundles/java/codebuddy/scripts/harness_archive.py +46 -7
  126. package/harness/bundles/java/codebuddy/scripts/harness_context.py +71 -22
  127. package/harness/bundles/java/codebuddy/scripts/harness_events.py +40 -1
  128. package/harness/bundles/java/codebuddy/scripts/harness_events_sync.py +93 -14
  129. package/harness/bundles/java/codebuddy/scripts/harness_gate.py +165 -1
  130. package/harness/bundles/java/codebuddy/scripts/harness_ledger.py +2 -0
  131. package/harness/bundles/java/codebuddy/scripts/harness_plan_finalize.py +2 -0
  132. package/harness/bundles/java/codebuddy/scripts/harness_test_guard.py +37 -44
  133. package/harness/bundles/java/codex/.harness-build.json +1 -1
  134. package/harness/bundles/java/codex/harness-apidoc/SKILL.md +1 -1
  135. package/harness/bundles/java/codex/harness-archive/SKILL.md +1 -1
  136. package/harness/bundles/java/codex/harness-codebase-map/SKILL.md +1 -1
  137. package/harness/bundles/java/codex/harness-knowledge-ingest/SKILL.md +1 -1
  138. package/harness/bundles/java/codex/harness-knowledge-query/SKILL.md +1 -1
  139. package/harness/bundles/java/codex/harness-package/SKILL.md +1 -1
  140. package/harness/bundles/java/codex/harness-plan/SKILL.md +2 -2
  141. package/harness/bundles/java/codex/harness-plan/checklist.md +2 -1
  142. package/harness/bundles/java/codex/harness-review/SKILL.md +3 -3
  143. package/harness/bundles/java/codex/harness-run/SKILL.md +4 -4
  144. package/harness/bundles/java/codex/harness-submit/SKILL.md +1 -1
  145. package/harness/bundles/java/codex/harness-sync/SKILL.md +1 -1
  146. package/harness/bundles/java/codex/harness-test/SKILL.md +5 -3
  147. package/harness/bundles/java/codex/scripts/harness_archive.py +46 -7
  148. package/harness/bundles/java/codex/scripts/harness_context.py +71 -22
  149. package/harness/bundles/java/codex/scripts/harness_events.py +40 -1
  150. package/harness/bundles/java/codex/scripts/harness_events_sync.py +93 -14
  151. package/harness/bundles/java/codex/scripts/harness_gate.py +165 -1
  152. package/harness/bundles/java/codex/scripts/harness_ledger.py +2 -0
  153. package/harness/bundles/java/codex/scripts/harness_plan_finalize.py +2 -0
  154. package/harness/bundles/java/codex/scripts/harness_test_guard.py +37 -44
  155. package/harness/bundles/java/cursor/.harness-build.json +1 -1
  156. package/harness/bundles/java/cursor/harness-apidoc/SKILL.md +1 -1
  157. package/harness/bundles/java/cursor/harness-archive/SKILL.md +1 -1
  158. package/harness/bundles/java/cursor/harness-codebase-map/SKILL.md +1 -1
  159. package/harness/bundles/java/cursor/harness-knowledge-ingest/SKILL.md +1 -1
  160. package/harness/bundles/java/cursor/harness-knowledge-query/SKILL.md +1 -1
  161. package/harness/bundles/java/cursor/harness-package/SKILL.md +1 -1
  162. package/harness/bundles/java/cursor/harness-plan/SKILL.md +2 -2
  163. package/harness/bundles/java/cursor/harness-plan/checklist.md +2 -1
  164. package/harness/bundles/java/cursor/harness-review/SKILL.md +3 -3
  165. package/harness/bundles/java/cursor/harness-run/SKILL.md +4 -4
  166. package/harness/bundles/java/cursor/harness-submit/SKILL.md +1 -1
  167. package/harness/bundles/java/cursor/harness-sync/SKILL.md +1 -1
  168. package/harness/bundles/java/cursor/harness-test/SKILL.md +5 -3
  169. package/harness/bundles/java/cursor/scripts/harness_archive.py +46 -7
  170. package/harness/bundles/java/cursor/scripts/harness_context.py +71 -22
  171. package/harness/bundles/java/cursor/scripts/harness_events.py +40 -1
  172. package/harness/bundles/java/cursor/scripts/harness_events_sync.py +93 -14
  173. package/harness/bundles/java/cursor/scripts/harness_gate.py +165 -1
  174. package/harness/bundles/java/cursor/scripts/harness_ledger.py +2 -0
  175. package/harness/bundles/java/cursor/scripts/harness_plan_finalize.py +2 -0
  176. package/harness/bundles/java/cursor/scripts/harness_test_guard.py +37 -44
  177. package/harness/manifests/general/claude-code.json +24 -24
  178. package/harness/manifests/general/codebuddy.json +24 -24
  179. package/harness/manifests/general/codex.json +24 -24
  180. package/harness/manifests/general/cursor.json +24 -24
  181. package/harness/manifests/java/claude-code.json +24 -24
  182. package/harness/manifests/java/codebuddy.json +24 -24
  183. package/harness/manifests/java/codex.json +24 -24
  184. package/harness/manifests/java/cursor.json +24 -24
  185. package/hunter-workflow-family.json +5 -4
  186. package/package.json +1 -1
@@ -51,6 +51,15 @@ WHITELIST = {
51
51
  "duration_ms",
52
52
  "schemaVersion",
53
53
  "summary",
54
+ "executor_tool",
55
+ "executor_agent",
56
+ "executor_model",
57
+ "execution_mode",
58
+ "decision_reason_code",
59
+ "fallback_reason_code",
60
+ "trigger",
61
+ "from_phase",
62
+ "result_status",
54
63
  }
55
64
 
56
65
  MAX_BATCH_SIZE = 100
@@ -110,20 +119,43 @@ def _summary_text(value: Any) -> str:
110
119
  return text[:EVENT_SUMMARY_MAX_LENGTH].rstrip()
111
120
 
112
121
 
122
+ _MACHINE_DETAIL_RE = re.compile(
123
+ r"(?:\s*[;;,,]?\s*)"
124
+ r"(?:artifactsHash|artifactHash|receiptHash|content_sha256|contentSha256)"
125
+ r"\s*[:=]\s*(?:sha256:)?[0-9a-f]{32,64}",
126
+ re.IGNORECASE,
127
+ )
128
+
129
+
130
+ def _readable_summary_text(value: Any) -> str:
131
+ text = _summary_text(value)
132
+ if not text:
133
+ return ""
134
+ text = _MACHINE_DETAIL_RE.sub("", text)
135
+ text = re.sub(
136
+ r"\bfinalize\s+ok\b",
137
+ "规划产物已校验并发布",
138
+ text,
139
+ flags=re.IGNORECASE,
140
+ )
141
+ text = re.sub(r"\s*([;;,,])\s*([;;,,])", r"\1", text)
142
+ return _summary_text(text.strip(" ;;,,"))
143
+
144
+
113
145
  def build_event_summary(event: dict[str, Any]) -> str:
114
- explicit = _summary_text(event.get("summary"))
146
+ explicit = _readable_summary_text(event.get("summary"))
115
147
  if explicit:
116
148
  return explicit
117
149
 
118
150
  event_type = _summary_text(event.get("type"))
119
151
  phase = _summary_text(event.get("phase"))
120
152
  phase_label = PHASE_LABELS.get(phase, phase or "当前")
121
- note = _summary_text(event.get("note"))
122
- message = _summary_text(event.get("message"))
123
- decision = _summary_text(event.get("decision"))
124
- reason = _summary_text(event.get("reason"))
125
- name = _summary_text(event.get("name"))
126
- code = _summary_text(event.get("code"))
153
+ note = _readable_summary_text(event.get("note"))
154
+ message = _readable_summary_text(event.get("message"))
155
+ decision = _readable_summary_text(event.get("decision"))
156
+ reason = _readable_summary_text(event.get("reason"))
157
+ name = _readable_summary_text(event.get("name"))
158
+ code = _readable_summary_text(event.get("code"))
127
159
  status = _summary_text(event.get("status"))
128
160
 
129
161
  if event_type == "decision" and decision:
@@ -754,6 +786,39 @@ def _discover_changes(project_root: Path) -> list[Path]:
754
786
  return sorted(path for path in root.glob("*") if path.is_dir()) if root.is_dir() else []
755
787
 
756
788
 
789
+ def _change_has_open_phase(change_dir: Path) -> bool:
790
+ """Project the append-only stream into open/closed phase state."""
791
+ try:
792
+ path = Path(harness_paths.resolve_state_dir_for_contract(change_dir)) / "events.ndjson"
793
+ except (OSError, ValueError):
794
+ return False
795
+ if not path.is_file():
796
+ return False
797
+ open_phases: dict[str, bool] = {}
798
+ try:
799
+ for line in path.read_text(encoding="utf-8-sig").splitlines():
800
+ if not line.strip():
801
+ continue
802
+ event = json.loads(line)
803
+ if not isinstance(event, dict):
804
+ continue
805
+ phase = event.get("phase")
806
+ event_type = event.get("type")
807
+ if not isinstance(phase, str) or not phase:
808
+ continue
809
+ if event_type == "phase.start":
810
+ open_phases[phase] = True
811
+ elif event_type in {"phase.end", "phase.auto_sealed", "workflow.end", "run.end"}:
812
+ open_phases[phase] = False
813
+ except (OSError, json.JSONDecodeError):
814
+ return False
815
+ return any(open_phases.values())
816
+
817
+
818
+ def _project_has_open_phase(project_root: Path) -> bool:
819
+ return any(_change_has_open_phase(path) for path in _discover_changes(project_root))
820
+
821
+
757
822
  def _run_agent_session(project_root: Path) -> dict[str, Any]:
758
823
  attempts = 0
759
824
  uploaded = 0
@@ -766,14 +831,28 @@ def _run_agent_session(project_root: Path) -> dict[str, Any]:
766
831
  if all(item.get("ok") for item in results):
767
832
  time.sleep(0.6)
768
833
  if generation == _read_nudge_generation(project_root):
769
- return {
770
- "ok": True,
771
- "uploaded": uploaded,
772
- "quarantined": quarantined,
773
- "attempts": attempts + 1,
774
- }
834
+ if not _project_has_open_phase(project_root):
835
+ return {
836
+ "ok": True,
837
+ "uploaded": uploaded,
838
+ "quarantined": quarantined,
839
+ "attempts": attempts + 1,
840
+ }
841
+ # Keep a finite workflow lease alive while a phase is open.
842
+ # A phase.end nudge is observed on the next pass and releases
843
+ # the worker naturally; no terminal window is created.
844
+ time.sleep(10.0)
845
+ heartbeat_results = [
846
+ sync_change(project_root, path, heartbeat_only=True)
847
+ for path in _discover_changes(project_root)
848
+ ]
849
+ if all(item.get("ok") for item in heartbeat_results):
850
+ attempts = 0
851
+ continue
852
+ results = heartbeat_results
775
853
  attempts = 0
776
- continue
854
+ if generation != _read_nudge_generation(project_root):
855
+ continue
777
856
  time.sleep(min(16.0, float(2 ** attempts)))
778
857
  attempts += 1
779
858
  return {
@@ -30,6 +30,7 @@ if str(SCRIPTS_DIR) not in sys.path:
30
30
  sys.path.insert(0, str(SCRIPTS_DIR))
31
31
 
32
32
  import harness_change as hc # noqa: E402
33
+ import harness_context as hctx # noqa: E402
33
34
  import harness_events as he # noqa: E402
34
35
  import harness_events_sync as hes # noqa: E402
35
36
  import harness_ledger as hl # noqa: E402
@@ -1602,6 +1603,18 @@ def append_phase_event(
1602
1603
  auto_sealed: list[dict[str, Any]] = []
1603
1604
  with he.event_file_lock(lock_path):
1604
1605
  existing = he.load_events(events_file)
1606
+ phase_attempts = he.split_phase_attempts(
1607
+ [event for event in existing if event.get("phase") == phase]
1608
+ )
1609
+ latest_attempt = max(
1610
+ (
1611
+ int(item.get("attempt"))
1612
+ for item in phase_attempts
1613
+ if isinstance(item.get("attempt"), int)
1614
+ ),
1615
+ default=0,
1616
+ )
1617
+ event_attempt = latest_attempt + 1 if type_ == "phase.start" else latest_attempt or 1
1605
1618
  args = argparse.Namespace(
1606
1619
  phase=phase,
1607
1620
  type=type_,
@@ -1619,12 +1632,18 @@ def append_phase_event(
1619
1632
  decision=None,
1620
1633
  reason=None,
1621
1634
  run_id=run_id,
1622
- attempt=None,
1635
+ attempt=event_attempt,
1623
1636
  executor_tool=executor_tool,
1624
1637
  executor_agent=executor_agent,
1625
1638
  executor_model=executor_model,
1626
1639
  handoff_from_tool=None,
1627
1640
  handoff_reason=None,
1641
+ execution_mode=None,
1642
+ decision_reason_code=None,
1643
+ fallback_reason_code=None,
1644
+ trigger=("fixback" if "fixback" in note.lower() else None),
1645
+ from_phase=("review" if phase == "run" and "fixback" in note.lower() else None),
1646
+ result_status=None,
1628
1647
  )
1629
1648
  event = he.build_event(args, existing)
1630
1649
  if identity:
@@ -1655,6 +1674,7 @@ def append_phase_event(
1655
1674
  content = he.render_execution_log(events)
1656
1675
  log_path = he.write_execution_log(change_dir, content)
1657
1676
  rendered = True
1677
+ he.nudge_remote_sync(change_dir)
1658
1678
  return {
1659
1679
  "ok": True,
1660
1680
  "eventId": event.get("id"),
@@ -1702,6 +1722,80 @@ def _phase_event_exists(change_dir: Path, phase: str, type_: str, run_id: str) -
1702
1722
  )
1703
1723
 
1704
1724
 
1725
+ def _latest_phase_end(
1726
+ change_dir: Path,
1727
+ phase: str,
1728
+ run_id: str | None = None,
1729
+ ) -> dict[str, Any] | None:
1730
+ for event in reversed(he.load_events(he.events_path(change_dir))):
1731
+ if event.get("phase") != phase or event.get("type") != "phase.end":
1732
+ continue
1733
+ if run_id and event.get("run_id") != run_id:
1734
+ continue
1735
+ return event
1736
+ return None
1737
+
1738
+
1739
+ def _terminal_matches_context_session(
1740
+ project: Path,
1741
+ change_id: str,
1742
+ phase: str,
1743
+ terminal: dict[str, Any],
1744
+ ) -> bool:
1745
+ view = hctx.context_view(project, change_id)
1746
+ current = view.get("current") if isinstance(view, dict) else None
1747
+ if not isinstance(current, dict) or current.get("phase") != phase:
1748
+ return False
1749
+ prepared_at = he.parse_timestamp(current.get("preparedAt"))
1750
+ terminal_at = he.parse_timestamp(
1751
+ terminal.get("timestamp") or terminal.get("occurred_at")
1752
+ )
1753
+ return (
1754
+ prepared_at is not None
1755
+ and terminal_at is not None
1756
+ and terminal_at >= prepared_at
1757
+ )
1758
+
1759
+
1760
+ def _close_context_handoff(
1761
+ project: Path,
1762
+ change_id: str,
1763
+ args: argparse.Namespace,
1764
+ *,
1765
+ status: str,
1766
+ ) -> dict[str, Any] | None:
1767
+ to_phase = getattr(args, "to_phase", None)
1768
+ if not to_phase:
1769
+ return None
1770
+ try:
1771
+ return hctx.close_transition(
1772
+ project,
1773
+ change_id,
1774
+ from_phase=args.phase,
1775
+ to_phase=str(to_phase),
1776
+ executor=getattr(args, "executor", None),
1777
+ artifacts=list(getattr(args, "artifact", None) or []),
1778
+ status=status,
1779
+ )
1780
+ except Exception as exc: # noqa: BLE001 — phase close remains locally durable
1781
+ return {
1782
+ "ok": False,
1783
+ "code": "CONTEXT_HANDOFF_FAILED",
1784
+ "message": str(exc),
1785
+ }
1786
+
1787
+
1788
+ def _sync_after_phase_close(project: Path, change_dir: Path) -> dict[str, Any]:
1789
+ try:
1790
+ return hes.auto_events_sync(project, change_dir)
1791
+ except Exception as exc: # noqa: BLE001 — remote sync is recoverable
1792
+ return {
1793
+ "ok": False,
1794
+ "code": "PLATFORM_EVENTS_SYNC_WARN",
1795
+ "warning": f"events-sync hook failed: {exc}",
1796
+ }
1797
+
1798
+
1705
1799
  def _validate_scenario_coverage(change_dir: Path) -> dict[str, Any]:
1706
1800
  """C9: validate all ledger-required scenarios are covered by ledger entries.
1707
1801
 
@@ -2266,6 +2360,48 @@ def cmd_close(args: argparse.Namespace) -> int:
2266
2360
  explicit_run_id = args.run_id or os.environ.get("HUNTER_HARNESS_RUN_ID")
2267
2361
  current_lease = hc.inspect_lease(project, resolved["changeId"])
2268
2362
  if current_lease is None:
2363
+ terminal = _latest_phase_end(change_dir, args.phase, explicit_run_id)
2364
+ if (
2365
+ getattr(args, "to_phase", None)
2366
+ and terminal is not None
2367
+ and _terminal_matches_context_session(
2368
+ project, str(resolved["changeId"]), args.phase, terminal
2369
+ )
2370
+ ):
2371
+ terminal_status = str(terminal.get("status") or args.status)
2372
+ handoff = _close_context_handoff(
2373
+ project,
2374
+ str(resolved["changeId"]),
2375
+ args,
2376
+ status=terminal_status,
2377
+ )
2378
+ monitor = _sync_after_phase_close(project, change_dir)
2379
+ if not isinstance(handoff, dict) or not handoff.get("ok"):
2380
+ return emit_error(
2381
+ "PHASE_HANDOFF_PENDING",
2382
+ "phase gate is closed, but context handoff still needs attention",
2383
+ as_json=as_json,
2384
+ extra={
2385
+ "localCloseComplete": True,
2386
+ "retryable": True,
2387
+ "contextHandoff": handoff,
2388
+ "platformMonitor": monitor,
2389
+ },
2390
+ )
2391
+ emit(
2392
+ {
2393
+ "ok": True,
2394
+ "code": "PHASE_CLOSE_RESUMED",
2395
+ "phase": args.phase,
2396
+ "status": terminal_status,
2397
+ "changeId": resolved["changeId"],
2398
+ "localCloseComplete": True,
2399
+ "contextHandoff": handoff,
2400
+ "platformMonitor": monitor,
2401
+ },
2402
+ as_json=as_json,
2403
+ )
2404
+ return 0
2269
2405
  return emit_error("LEASE_ABSENT", "no active lease for phase close", as_json=as_json)
2270
2406
  run_id = explicit_run_id or str(current_lease.get("runId") or "")
2271
2407
  if str(current_lease.get("runId")) != run_id or str(current_lease.get("phase")) != args.phase:
@@ -2597,6 +2733,29 @@ def cmd_close(args: argparse.Namespace) -> int:
2597
2733
  capsule["closeStatus"] = close_status
2598
2734
  write_phase_capsule(change_dir, args.phase, run_id, capsule)
2599
2735
 
2736
+ context_handoff = _close_context_handoff(
2737
+ project,
2738
+ str(resolved["changeId"]),
2739
+ args,
2740
+ status=close_status,
2741
+ )
2742
+ platform_monitor = _sync_after_phase_close(project, change_dir)
2743
+ if isinstance(context_handoff, dict) and not context_handoff.get("ok"):
2744
+ return emit_error(
2745
+ "PHASE_HANDOFF_PENDING",
2746
+ "phase gate closed locally, but context handoff failed; retry the same close command",
2747
+ as_json=as_json,
2748
+ extra={
2749
+ "localCloseComplete": True,
2750
+ "retryable": True,
2751
+ "phase": args.phase,
2752
+ "status": close_status,
2753
+ "changeId": resolved["changeId"],
2754
+ "contextHandoff": context_handoff,
2755
+ "platformMonitor": platform_monitor,
2756
+ },
2757
+ )
2758
+
2600
2759
  payload = {
2601
2760
  "ok": True,
2602
2761
  "code": close_code,
@@ -2612,6 +2771,8 @@ def cmd_close(args: argparse.Namespace) -> int:
2612
2771
  "lease": release,
2613
2772
  "gateSeverityMode": severity_mode,
2614
2773
  "gateWarnings": gate_warnings,
2774
+ "contextHandoff": context_handoff,
2775
+ "platformMonitor": platform_monitor,
2615
2776
  }
2616
2777
  emit(payload, as_json=as_json)
2617
2778
  return 0
@@ -2806,6 +2967,9 @@ def build_parser() -> argparse.ArgumentParser:
2806
2967
  p_close.add_argument("--run-id", default=None)
2807
2968
  p_close.add_argument("--task", type=int, default=None)
2808
2969
  p_close.add_argument("--note", default="")
2970
+ p_close.add_argument("--to-phase", default=None)
2971
+ p_close.add_argument("--executor", default=None)
2972
+ p_close.add_argument("--artifact", action="append", default=[])
2809
2973
  p_close.set_defaults(func=cmd_close)
2810
2974
 
2811
2975
  p_classify = sub.add_parser("classify", parents=[shared])
@@ -1160,6 +1160,8 @@ _DYNAMIC_OWN_DIRS = ("events.ndjson", "logs", "evidence", "reports", "runtime",
1160
1160
 
1161
1161
  def _matches_ownership_path(rel: str, declared: Any) -> bool:
1162
1162
  scope = str(declared).replace("\\", "/").strip("/")
1163
+ if scope.endswith("/**"):
1164
+ scope = scope[:-3].rstrip("/")
1163
1165
  if not scope:
1164
1166
  return False
1165
1167
  normalized = rel.replace("\\", "/").strip("/")
@@ -1155,6 +1155,7 @@ def finalize_plan(
1155
1155
  "artifactsHash": validation["artifactsHash"],
1156
1156
  "files": validation["files"],
1157
1157
  "receiptPath": str(receipt_path),
1158
+ "artifactRef": "meta/plan-finalization.json",
1158
1159
  }
1159
1160
 
1160
1161
  lock_path.parent.mkdir(parents=True, exist_ok=True)
@@ -1253,6 +1254,7 @@ def finalize_plan(
1253
1254
  "artifactsHash": validation["artifactsHash"],
1254
1255
  "files": validation["files"],
1255
1256
  "receiptPath": str(receipt_path),
1257
+ "artifactRef": "meta/plan-finalization.json",
1256
1258
  "executionLogPath": str(harness_events.execution_log_path(change_dir)),
1257
1259
  }
1258
1260
  except OSError as exc:
@@ -1593,18 +1593,15 @@ def close(project: Path | str, change_dir: Path | str) -> dict[str, Any]:
1593
1593
  [],
1594
1594
  error=str(exc),
1595
1595
  )
1596
- if (
1596
+ profile_changed = bool(
1597
1597
  isinstance(snapshot.get("profileFingerprint"), str)
1598
1598
  and snapshot["profileFingerprint"] != current_profile_fingerprint
1599
- ):
1600
- return _result(False, action, "PROFILE_CHANGED", [])
1599
+ )
1601
1600
  baseline_commit = _snapshot_baseline_commit(
1602
1601
  project_root,
1603
1602
  change_root,
1604
1603
  snapshot,
1605
1604
  )
1606
- profile_changed = False
1607
-
1608
1605
  before_entries = snapshot.get("files")
1609
1606
  if not isinstance(before_entries, list):
1610
1607
  return _result(False, action, "SNAPSHOT_INVALID", [])
@@ -1682,11 +1679,21 @@ def close(project: Path | str, change_dir: Path | str) -> dict[str, Any]:
1682
1679
  return result
1683
1680
 
1684
1681
  recorded = [rel for rel, _ in touched]
1685
- reconciliation = _reconcile_close_manifest(
1686
- project_root,
1687
- change_root,
1688
- set(recorded),
1689
- baseline_commit,
1682
+ reconciliation = (
1683
+ {
1684
+ "ok": True,
1685
+ "code": "PROFILE_REBASED_MANIFEST_PRESERVED",
1686
+ "removedCurrentTouches": 0,
1687
+ "deletedEntries": 0,
1688
+ "preservedForeignEntries": 0,
1689
+ }
1690
+ if profile_changed
1691
+ else _reconcile_close_manifest(
1692
+ project_root,
1693
+ change_root,
1694
+ set(recorded),
1695
+ baseline_commit,
1696
+ )
1690
1697
  )
1691
1698
  if not reconciliation.get("ok"):
1692
1699
  return _result(
@@ -1696,40 +1703,26 @@ def close(project: Path | str, change_dir: Path | str) -> dict[str, Any]:
1696
1703
  list(reconciliation.get("files") or []),
1697
1704
  )
1698
1705
 
1699
- # Cross-check (retro §5.10): if the manifest has active entries for this
1700
- # change but close computed recordedCount=0, the snapshot/manifest/diff
1701
- # are inconsistent. Fail closed instead of silently returning success.
1702
- manifest_path = _manifest_path(change_root)
1703
- if manifest_path is not None and manifest_path.is_file():
1704
- try:
1705
- manifest = _read_json(manifest_path)
1706
- except (OSError, json.JSONDecodeError):
1707
- manifest = None
1708
- if isinstance(manifest, dict):
1709
- manifest_files = manifest.get("files")
1710
- if isinstance(manifest_files, list):
1711
- is_v2 = manifest.get("schemaVersion") == 2
1712
- change_id = change_root.name
1713
- active_entries = [
1714
- f for f in manifest_files
1715
- if isinstance(f, dict)
1716
- and f.get("reason") in ("tdd-created", "test-updated", "stale-test-repair")
1717
- and (
1718
- not is_v2
1719
- or f.get("introducedBy") == change_id
1720
- or change_id in (f.get("touchedBy") or [])
1721
- )
1722
- ]
1723
- if active_entries and not recorded:
1724
- return _result(
1725
- False,
1726
- action,
1727
- "MANIFEST_DIFF_HASH_MISMATCH",
1728
- [],
1729
- manifestEntries=len(active_entries),
1730
- recordedCount=0,
1731
- detail="manifest has active entries but close computed 0 recorded tests",
1732
- )
1706
+ # A manifest is change-wide and intentionally survives phase boundaries.
1707
+ # Therefore pre-existing entries with no new diff are consistent and must
1708
+ # not force a test-guard reset. When the build profile changes mid-phase,
1709
+ # retain the diff result and rebaseline the reusable snapshot atomically.
1710
+ if profile_changed:
1711
+ snapshot.update(
1712
+ {
1713
+ "profileFingerprint": current_profile_fingerprint,
1714
+ "capturedAt": time.strftime("%Y-%m-%dT%H:%M:%S"),
1715
+ "files": [
1716
+ {
1717
+ "path": rel,
1718
+ "sha256": digest,
1719
+ "ignored": _is_ignored(project_root, rel),
1720
+ }
1721
+ for rel, digest in sorted(current.items())
1722
+ ],
1723
+ }
1724
+ )
1725
+ _write_json(snapshot_path, snapshot)
1733
1726
 
1734
1727
  return _result(
1735
1728
  True,
@@ -2,5 +2,5 @@
2
2
  "schemaVersion": 1,
3
3
  "overlay": "none",
4
4
  "agent": "codebuddy",
5
- "coreHash": "9f66b2de1ca64c6e"
5
+ "coreHash": "e9b3973b8ba91be9"
6
6
  }
@@ -3,7 +3,7 @@ name: harness-archive
3
3
  description: 归档所有变更产出(计划/测试报告/审查/SQL/API文档)到 .harness/archive/,含归档元数据。仅当用户显式调用
4
4
  /harness-archive 时使用;不得被其他阶段 skill 自动接续触发。
5
5
  ---
6
- <!-- generated by harness_deploy.py; core=9f66b2de1ca64c6e; overlay=none; agent=codebuddy; do not edit -->
6
+ <!-- generated by harness_deploy.py; core=e9b3973b8ba91be9; overlay=none; agent=codebuddy; do not edit -->
7
7
  # harness-archive — 变更归档
8
8
 
9
9
  ## Purpose
@@ -3,7 +3,7 @@ name: harness-codebase-map
3
3
  description: 分析现有代码库并生成 .harness/codebase/map/
4
4
  下的结构化代码库地图。用于陌生项目理解、重大改造前梳理、harness-sync 提示 map 过期后的人工刷新。
5
5
  ---
6
- <!-- generated by harness_deploy.py; core=9f66b2de1ca64c6e; overlay=none; agent=codebuddy; do not edit -->
6
+ <!-- generated by harness_deploy.py; core=e9b3973b8ba91be9; overlay=none; agent=codebuddy; do not edit -->
7
7
  # harness-codebase-map — 代码库地图
8
8
 
9
9
  ## Purpose
@@ -2,7 +2,7 @@
2
2
  name: harness-knowledge-ingest
3
3
  description: 确认归档 ZIP 已上传并由 Hunter Platform 在服务端解包、校验和入库。客户端不再构建或维护本地知识索引。
4
4
  ---
5
- <!-- generated by harness_deploy.py; core=9f66b2de1ca64c6e; overlay=none; agent=codebuddy; do not edit -->
5
+ <!-- generated by harness_deploy.py; core=e9b3973b8ba91be9; overlay=none; agent=codebuddy; do not edit -->
6
6
  # harness-knowledge-ingest
7
7
 
8
8
  知识 ingest 完全由 Hunter Platform 负责。归档完成时,客户端生成一个确定性 ZIP;
@@ -2,7 +2,7 @@
2
2
  name: harness-knowledge-query
3
3
  description: 在规划、实现或排查前,通过 hunter-harness CLI 查询远端项目知识库。远端不可用时直接报告不可用,不建立本地索引或离线回退。
4
4
  ---
5
- <!-- generated by harness_deploy.py; core=9f66b2de1ca64c6e; overlay=none; agent=codebuddy; do not edit -->
5
+ <!-- generated by harness_deploy.py; core=e9b3973b8ba91be9; overlay=none; agent=codebuddy; do not edit -->
6
6
  # harness-knowledge-query
7
7
 
8
8
  项目知识以 Hunter Platform 的服务端索引为唯一真源。客户端只提交查询并消费结果:
@@ -3,7 +3,7 @@ name: harness-plan
3
3
  description: 将需求转化为设计文档+实施计划+完整测试场景表,必须在编码前完成。仅当用户显式调用 /harness-plan 或明确要求进入
4
4
  Harness 规划阶段时使用;不得因用户描述需求就自动触发。
5
5
  ---
6
- <!-- generated by harness_deploy.py; core=9f66b2de1ca64c6e; overlay=none; agent=codebuddy; do not edit -->
6
+ <!-- generated by harness_deploy.py; core=e9b3973b8ba91be9; overlay=none; agent=codebuddy; do not edit -->
7
7
  # harness-plan — 需求规划
8
8
 
9
9
  ## Purpose
@@ -74,7 +74,7 @@ description: 将需求转化为设计文档+实施计划+完整测试场景表
74
74
  | 4 | **设计审批包** blocking user confirmation;确认事件早于 approved 设计文档和 `meta/worktree.json` |
75
75
  | 5–6 | plan + implementation-detail + test-scenarios → `plans/` |
76
76
  | 7.5 | 仅 `--adversarial` 对抗评审 |
77
- | 8 | 在临时产物集上运行 `harness_plan_finalize.py finalize`,随后立即运行 `verify`;成功后执行 `harness_context.py close --from-phase plan --to-phase run --executor <tool> --artifact <plan-finalization> --json` append-only handoff receipt;原子发布、派生清单计数对账、完整生命周期、render → `checklist.md` |
77
+ | 8 | 在临时产物集上运行 `harness_plan_finalize.py finalize`,随后立即运行 `verify`;成功后把 finalizer 返回的绝对 `receiptPath` 原样传给 `harness_context.py close --from-phase plan --to-phase run --executor <tool> --artifact "<receiptPath>" --json`(也可使用返回的稳定 `artifactRef=meta/plan-finalization.json`),写 append-only handoff receipt;不得手写占位路径;原子发布、派生清单计数对账、完整生命周期、render → `checklist.md` |
78
78
 
79
79
  change-name 范围变更 → 提示重命名或记 🟡WARN(→ `reference.md`)
80
80
 
@@ -211,13 +211,14 @@ source: harness-plan
211
211
 
212
212
  - [ ] 所有待发布产物先写入 staging,不直接覆盖正式 change 目录
213
213
  - [ ] 执行 `harness_plan_finalize.py finalize --change-dir ... --staging-dir ... --change ... --run-id <plan-run-id> --attempt <attempt> --json`;身份必须与本次 `phase.start` 完全相同
214
- - [ ] finalizer 返回 `ok=true` `artifactsHash`;重复执行返回 `idempotent=true`
214
+ - [ ] finalizer 返回 `ok=true`、`artifactsHash`、绝对 `receiptPath` 与稳定 `artifactRef=meta/plan-finalization.json`;重复执行返回 `idempotent=true`
215
215
  - [ ] 紧接着执行 `harness_plan_finalize.py verify --change-dir ... --json`
216
216
  - [ ] verify 返回 `phaseStartCount=1`、`phaseEndCount=1`、`phaseEndStatus=OK`、`receiptConsistent=true`
217
217
  - [ ] 收据 `files` 完整包含 design / plan / implementation-detail / test-scenarios / gate-policy / worktree 六项标准产物;不得省略、重复或经 symlink/junction/reparse point 引用
218
218
  - [ ] verify 的 `taskCount` 等于全部任务表行数,`scenarioCount` 等于 Markdown 中全部唯一场景 ID 数;任一为 0 或不一致即 ❌FAIL
219
219
  - [ ] finalizer 失败时正式目录无半发布产物、无成功 `phase.end`、无伪造 execution log
220
220
  - [ ] 禁止在 finalizer 之前手工追加成功 `phase.end`
221
+ - [ ] context close 的 `--artifact` 只传 finalizer 原样返回的 `receiptPath` 或 `artifactRef`,不得猜测、拼接或使用 `<plan-finalization>` 占位文本
221
222
 
222
223
  | 文件 | 必须存在 | 检查结果 |
223
224
  |------|:---:|:---:|
@@ -4,7 +4,7 @@ description: 6维度代码审查(架构/安全/规范/兼容/测试/性能)
4
4
  .harness/context-index.json)和测试场景表,在隔离上下文运行。仅当用户显式调用 /harness-review 时使用;不得在
5
5
  test 结束后自动接续执行。
6
6
  ---
7
- <!-- generated by harness_deploy.py; core=9f66b2de1ca64c6e; overlay=none; agent=codebuddy; do not edit -->
7
+ <!-- generated by harness_deploy.py; core=e9b3973b8ba91be9; overlay=none; agent=codebuddy; do not edit -->
8
8
  # harness-review — 代码审查
9
9
 
10
10
  ## Purpose
@@ -45,14 +45,16 @@ description: 6维度代码审查(架构/安全/规范/兼容/测试/性能)
45
45
 
46
46
  ## Workflow
47
47
 
48
- 先用 `harness_context.py prepare --phase review --executor <tool> [--change <id>] --json` 与 `harness_context.py begin --phase review --change <id> --executor <tool> --json` 解析唯一 active change 并校验 test→review receipt;多个 active change 未显式选择时返回 `ACTIVE_CHANGE_AMBIGUOUS`,禁止再按 Glob/mtime 猜测。阶段门禁使用 `harness_gate.py begin --phase review --change <id> --task <n> --skills-root <skills-root> --executor-tool <tool> --json` `harness_gate.py close --phase review --change <id> --status <OK|WARN|FAIL> --run-id <begin-run-id> --json`;之后通过 `harness_context.py close --from-phase review --to-phase submit|run --executor <tool> --json` 交接或 fixback。不得手工追加 `phase.start/phase.end`;任一 close 失败不得宣称审查完成。
48
+ 先用 `harness_context.py prepare --phase review --executor <tool> [--change <id>] --json` 与 `harness_context.py begin --phase review --change <id> --executor <tool> --json` 解析唯一 active change 并校验 test→review receipt;多个 active change 未显式选择时返回 `ACTIVE_CHANGE_AMBIGUOUS`,禁止再按 Glob/mtime 猜测。阶段门禁使用 `harness_gate.py begin --phase review --change <id> --task <n> --skills-root <skills-root> --executor-tool <tool> --json`;结束时只执行一次 `harness_gate.py close --phase review --change <id> --status <OK|WARN|FAIL> --run-id <begin-run-id> --to-phase submit|run --executor <tool> --json`,由 gate 完成阶段事件、租约、上下文交接与远端补传。不得手工追加 `phase.start/phase.end` 或再次调用 context close;任一 close 失败不得宣称审查完成。
49
49
 
50
50
  0. **启动准备** — 确定变更名(Glob `.harness/changes/*/plans/*-plan.md`,排除 `.harness/archive/*/`,读 frontmatter 提取 change-name);**append `phase.start` 事件**(不得等审查完成才补)
51
51
  1. **读取 worktree 状态(门禁检查)** — 读 `.harness/changes/<change-name>/meta/worktree.json`:`requested=true` 但 worktree 不存在 → 停止并提示先修复 `harness-run`,不得静默回主目录(否则 git diff 为空);`requested=true` 且 worktree 已创建 → spawned agent 用该 worktree 路径执行 `git diff`(确保审查 worktree 变更而非主目录);`requested=false` → 审查主目录变更
52
52
  <!-- @section-id review.delegate -->
53
- ### 2. 审查执行(风险分级、inline 优先)
53
+ ### 2. 审查执行(隔离 reviewer 优先)
54
54
 
55
- 默认由主会话按同一 6 维度检查清单审查。仅发布候选,或变更命中权限/安全、支付、数据迁移、并发幂等、公共契约、跨模块核心路径时,才考虑隔离 reviewer;需要固定 `harness-reviewer` 时执行一次 `python <skills-root>/scripts/harness_preflight.py check-agents --skills-root <skills-root> --agent harness-reviewer --json`。只有 `executionMode=delegated` 才委派;`executionMode=inline` 静默由主会话审查,不显示缺失告警。定义损坏、spawn 失败、空返回或只有元数据 → 记录一次 issue 后直接主会话审查,`fallbackPolicy=inline-no-retry`。
55
+ review 默认优先在独立上下文执行,避免主会话已形成的实现结论影响审查。阶段开始后只做一次能力判定:固定 reviewer 可用时执行 `python <skills-root>/scripts/harness_preflight.py check-agents --skills-root <skills-root> --agent harness-reviewer --json`;宿主提供通用隔离任务能力时可直接委派一次只读审查。委派只读取 diff、规则和测试证据,主会话负责最终核验与落盘。
56
+
57
+ 无隔离能力、定义损坏、spawn 失败、空返回或只有元数据时,不重试,立即由主会话按同一 6 维度检查清单完成审查。无论是否委派,都必须追加一条 `decision` 事件:`executor_agent=harness-reviewer` 表示已委派;`decision` 写执行方式,`reason` 写选择或回退原因,稳定原因码使用 `REVIEW_DELEGATED`、`REVIEW_INLINE_UNAVAILABLE`、`REVIEW_INLINE_SPAWN_FAILED` 或 `REVIEW_INLINE_INVALID_RESULT`。正常 inline 是可接受结果,不显示成流程故障;`fallbackPolicy=inline-no-retry`。
56
58
 
57
59
  3. **持久化报告与事实 sidecar(强制,主会话)** — Agent 返回后主会话 Write 到 `reports/review/review-report-*.md`,并把同一批发现写成临时 JSON 后调用 `python <skills-root>/scripts/harness_review.py write-findings --change-dir <change-dir> --input <findings.json>`。权威计数来自 `reports/review/review-findings.json`,不是 Markdown。任一写入缺失 → 🟡WARN,不得宣称 review 完成。
58
60
  4. **生成修复反馈(原生协议)** — 若报告存在 RED/YELLOW 问题,执行 `protocols.md` 的 `review-fixback-protocol`,将问题转化为结构化 fixback 清单并落盘到 `.harness/changes/<change-name>/reports/review/fixback-YYYYMMDD-HHmm.md`;随后把 disposition 临时 JSON 交给 `python <skills-root>/scripts/harness_review.py write-dispositions --change-dir <change-dir> --input <dispositions.json>`,权威状态写入 `reports/review/fixback-dispositions.json`。若无 RED/YELLOW,也必须写空 findings sidecar,并记录 `review-fixback-protocol: skipped(no findings)`。不调用 Superpowers `receiving-code-review`,也不记录外部 skill 降级。