@hunter-harness/workflow-harness 0.2.57 → 0.2.58

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 (182) 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 +1 -1
  7. package/harness/bundles/general/claude-code/harness-review/SKILL.md +5 -3
  8. package/harness/bundles/general/claude-code/harness-review/protocols.md +2 -1
  9. package/harness/bundles/general/claude-code/harness-run/SKILL.md +4 -4
  10. package/harness/bundles/general/claude-code/harness-submit/SKILL.md +1 -1
  11. package/harness/bundles/general/claude-code/harness-sync/SKILL.md +1 -1
  12. package/harness/bundles/general/claude-code/harness-test/SKILL.md +5 -3
  13. package/harness/bundles/general/claude-code/harness-test/reference.md +2 -0
  14. package/harness/bundles/general/claude-code/protocols/report-pipeline-protocol.md +2 -0
  15. package/harness/bundles/general/claude-code/scripts/harness_context.py +423 -77
  16. package/harness/bundles/general/claude-code/scripts/harness_events.py +102 -12
  17. package/harness/bundles/general/claude-code/scripts/harness_fixback.py +529 -1
  18. package/harness/bundles/general/claude-code/scripts/harness_gate.py +161 -6
  19. package/harness/bundles/general/claude-code/scripts/harness_ledger.py +81 -8
  20. package/harness/bundles/general/claude-code/scripts/harness_review.py +25 -2
  21. package/harness/bundles/general/claude-code/scripts/harness_test_runner.py +155 -1
  22. package/harness/bundles/general/codebuddy/.harness-build.json +1 -1
  23. package/harness/bundles/general/codebuddy/harness-archive/SKILL.md +1 -1
  24. package/harness/bundles/general/codebuddy/harness-codebase-map/SKILL.md +1 -1
  25. package/harness/bundles/general/codebuddy/harness-knowledge-ingest/SKILL.md +1 -1
  26. package/harness/bundles/general/codebuddy/harness-knowledge-query/SKILL.md +1 -1
  27. package/harness/bundles/general/codebuddy/harness-plan/SKILL.md +1 -1
  28. package/harness/bundles/general/codebuddy/harness-review/SKILL.md +5 -3
  29. package/harness/bundles/general/codebuddy/harness-review/protocols.md +2 -1
  30. package/harness/bundles/general/codebuddy/harness-run/SKILL.md +4 -4
  31. package/harness/bundles/general/codebuddy/harness-submit/SKILL.md +1 -1
  32. package/harness/bundles/general/codebuddy/harness-sync/SKILL.md +1 -1
  33. package/harness/bundles/general/codebuddy/harness-test/SKILL.md +5 -3
  34. package/harness/bundles/general/codebuddy/harness-test/reference.md +2 -0
  35. package/harness/bundles/general/codebuddy/protocols/report-pipeline-protocol.md +2 -0
  36. package/harness/bundles/general/codebuddy/scripts/harness_context.py +423 -77
  37. package/harness/bundles/general/codebuddy/scripts/harness_events.py +102 -12
  38. package/harness/bundles/general/codebuddy/scripts/harness_fixback.py +529 -1
  39. package/harness/bundles/general/codebuddy/scripts/harness_gate.py +161 -6
  40. package/harness/bundles/general/codebuddy/scripts/harness_ledger.py +81 -8
  41. package/harness/bundles/general/codebuddy/scripts/harness_review.py +25 -2
  42. package/harness/bundles/general/codebuddy/scripts/harness_test_runner.py +155 -1
  43. package/harness/bundles/general/codex/.harness-build.json +1 -1
  44. package/harness/bundles/general/codex/harness-archive/SKILL.md +1 -1
  45. package/harness/bundles/general/codex/harness-codebase-map/SKILL.md +1 -1
  46. package/harness/bundles/general/codex/harness-knowledge-ingest/SKILL.md +1 -1
  47. package/harness/bundles/general/codex/harness-knowledge-query/SKILL.md +1 -1
  48. package/harness/bundles/general/codex/harness-plan/SKILL.md +1 -1
  49. package/harness/bundles/general/codex/harness-review/SKILL.md +3 -1
  50. package/harness/bundles/general/codex/harness-review/protocols.md +2 -1
  51. package/harness/bundles/general/codex/harness-run/SKILL.md +4 -4
  52. package/harness/bundles/general/codex/harness-submit/SKILL.md +1 -1
  53. package/harness/bundles/general/codex/harness-sync/SKILL.md +1 -1
  54. package/harness/bundles/general/codex/harness-test/SKILL.md +5 -3
  55. package/harness/bundles/general/codex/harness-test/reference.md +2 -0
  56. package/harness/bundles/general/codex/protocols/report-pipeline-protocol.md +2 -0
  57. package/harness/bundles/general/codex/scripts/harness_context.py +423 -77
  58. package/harness/bundles/general/codex/scripts/harness_events.py +102 -12
  59. package/harness/bundles/general/codex/scripts/harness_fixback.py +529 -1
  60. package/harness/bundles/general/codex/scripts/harness_gate.py +161 -6
  61. package/harness/bundles/general/codex/scripts/harness_ledger.py +81 -8
  62. package/harness/bundles/general/codex/scripts/harness_review.py +25 -2
  63. package/harness/bundles/general/codex/scripts/harness_test_runner.py +155 -1
  64. package/harness/bundles/general/cursor/.harness-build.json +1 -1
  65. package/harness/bundles/general/cursor/harness-archive/SKILL.md +1 -1
  66. package/harness/bundles/general/cursor/harness-codebase-map/SKILL.md +1 -1
  67. package/harness/bundles/general/cursor/harness-knowledge-ingest/SKILL.md +1 -1
  68. package/harness/bundles/general/cursor/harness-knowledge-query/SKILL.md +1 -1
  69. package/harness/bundles/general/cursor/harness-plan/SKILL.md +1 -1
  70. package/harness/bundles/general/cursor/harness-review/SKILL.md +3 -1
  71. package/harness/bundles/general/cursor/harness-review/protocols.md +2 -1
  72. package/harness/bundles/general/cursor/harness-run/SKILL.md +4 -4
  73. package/harness/bundles/general/cursor/harness-submit/SKILL.md +1 -1
  74. package/harness/bundles/general/cursor/harness-sync/SKILL.md +1 -1
  75. package/harness/bundles/general/cursor/harness-test/SKILL.md +5 -3
  76. package/harness/bundles/general/cursor/harness-test/reference.md +2 -0
  77. package/harness/bundles/general/cursor/protocols/report-pipeline-protocol.md +2 -0
  78. package/harness/bundles/general/cursor/scripts/harness_context.py +423 -77
  79. package/harness/bundles/general/cursor/scripts/harness_events.py +102 -12
  80. package/harness/bundles/general/cursor/scripts/harness_fixback.py +529 -1
  81. package/harness/bundles/general/cursor/scripts/harness_gate.py +161 -6
  82. package/harness/bundles/general/cursor/scripts/harness_ledger.py +81 -8
  83. package/harness/bundles/general/cursor/scripts/harness_review.py +25 -2
  84. package/harness/bundles/general/cursor/scripts/harness_test_runner.py +155 -1
  85. package/harness/bundles/java/claude-code/.harness-build.json +1 -1
  86. package/harness/bundles/java/claude-code/harness-apidoc/SKILL.md +1 -1
  87. package/harness/bundles/java/claude-code/harness-archive/SKILL.md +1 -1
  88. package/harness/bundles/java/claude-code/harness-codebase-map/SKILL.md +1 -1
  89. package/harness/bundles/java/claude-code/harness-knowledge-ingest/SKILL.md +1 -1
  90. package/harness/bundles/java/claude-code/harness-knowledge-query/SKILL.md +1 -1
  91. package/harness/bundles/java/claude-code/harness-package/SKILL.md +1 -1
  92. package/harness/bundles/java/claude-code/harness-plan/SKILL.md +1 -1
  93. package/harness/bundles/java/claude-code/harness-review/SKILL.md +5 -3
  94. package/harness/bundles/java/claude-code/harness-review/protocols.md +2 -1
  95. package/harness/bundles/java/claude-code/harness-run/SKILL.md +4 -4
  96. package/harness/bundles/java/claude-code/harness-submit/SKILL.md +1 -1
  97. package/harness/bundles/java/claude-code/harness-sync/SKILL.md +1 -1
  98. package/harness/bundles/java/claude-code/harness-test/SKILL.md +5 -3
  99. package/harness/bundles/java/claude-code/protocols/report-pipeline-protocol.md +2 -0
  100. package/harness/bundles/java/claude-code/scripts/harness_context.py +423 -77
  101. package/harness/bundles/java/claude-code/scripts/harness_events.py +102 -12
  102. package/harness/bundles/java/claude-code/scripts/harness_fixback.py +529 -1
  103. package/harness/bundles/java/claude-code/scripts/harness_gate.py +161 -6
  104. package/harness/bundles/java/claude-code/scripts/harness_ledger.py +81 -8
  105. package/harness/bundles/java/claude-code/scripts/harness_review.py +25 -2
  106. package/harness/bundles/java/claude-code/scripts/harness_test_runner.py +155 -1
  107. package/harness/bundles/java/codebuddy/.harness-build.json +1 -1
  108. package/harness/bundles/java/codebuddy/harness-apidoc/SKILL.md +1 -1
  109. package/harness/bundles/java/codebuddy/harness-archive/SKILL.md +1 -1
  110. package/harness/bundles/java/codebuddy/harness-codebase-map/SKILL.md +1 -1
  111. package/harness/bundles/java/codebuddy/harness-knowledge-ingest/SKILL.md +1 -1
  112. package/harness/bundles/java/codebuddy/harness-knowledge-query/SKILL.md +1 -1
  113. package/harness/bundles/java/codebuddy/harness-package/SKILL.md +1 -1
  114. package/harness/bundles/java/codebuddy/harness-plan/SKILL.md +1 -1
  115. package/harness/bundles/java/codebuddy/harness-review/SKILL.md +5 -3
  116. package/harness/bundles/java/codebuddy/harness-review/protocols.md +2 -1
  117. package/harness/bundles/java/codebuddy/harness-run/SKILL.md +4 -4
  118. package/harness/bundles/java/codebuddy/harness-submit/SKILL.md +1 -1
  119. package/harness/bundles/java/codebuddy/harness-sync/SKILL.md +1 -1
  120. package/harness/bundles/java/codebuddy/harness-test/SKILL.md +5 -3
  121. package/harness/bundles/java/codebuddy/protocols/report-pipeline-protocol.md +2 -0
  122. package/harness/bundles/java/codebuddy/scripts/harness_context.py +423 -77
  123. package/harness/bundles/java/codebuddy/scripts/harness_events.py +102 -12
  124. package/harness/bundles/java/codebuddy/scripts/harness_fixback.py +529 -1
  125. package/harness/bundles/java/codebuddy/scripts/harness_gate.py +161 -6
  126. package/harness/bundles/java/codebuddy/scripts/harness_ledger.py +81 -8
  127. package/harness/bundles/java/codebuddy/scripts/harness_review.py +25 -2
  128. package/harness/bundles/java/codebuddy/scripts/harness_test_runner.py +155 -1
  129. package/harness/bundles/java/codex/.harness-build.json +1 -1
  130. package/harness/bundles/java/codex/harness-apidoc/SKILL.md +1 -1
  131. package/harness/bundles/java/codex/harness-archive/SKILL.md +1 -1
  132. package/harness/bundles/java/codex/harness-codebase-map/SKILL.md +1 -1
  133. package/harness/bundles/java/codex/harness-knowledge-ingest/SKILL.md +1 -1
  134. package/harness/bundles/java/codex/harness-knowledge-query/SKILL.md +1 -1
  135. package/harness/bundles/java/codex/harness-package/SKILL.md +1 -1
  136. package/harness/bundles/java/codex/harness-plan/SKILL.md +1 -1
  137. package/harness/bundles/java/codex/harness-review/SKILL.md +3 -1
  138. package/harness/bundles/java/codex/harness-review/protocols.md +2 -1
  139. package/harness/bundles/java/codex/harness-run/SKILL.md +4 -4
  140. package/harness/bundles/java/codex/harness-submit/SKILL.md +1 -1
  141. package/harness/bundles/java/codex/harness-sync/SKILL.md +1 -1
  142. package/harness/bundles/java/codex/harness-test/SKILL.md +5 -3
  143. package/harness/bundles/java/codex/protocols/report-pipeline-protocol.md +2 -0
  144. package/harness/bundles/java/codex/scripts/harness_context.py +423 -77
  145. package/harness/bundles/java/codex/scripts/harness_events.py +102 -12
  146. package/harness/bundles/java/codex/scripts/harness_fixback.py +529 -1
  147. package/harness/bundles/java/codex/scripts/harness_gate.py +161 -6
  148. package/harness/bundles/java/codex/scripts/harness_ledger.py +81 -8
  149. package/harness/bundles/java/codex/scripts/harness_review.py +25 -2
  150. package/harness/bundles/java/codex/scripts/harness_test_runner.py +155 -1
  151. package/harness/bundles/java/cursor/.harness-build.json +1 -1
  152. package/harness/bundles/java/cursor/harness-apidoc/SKILL.md +1 -1
  153. package/harness/bundles/java/cursor/harness-archive/SKILL.md +1 -1
  154. package/harness/bundles/java/cursor/harness-codebase-map/SKILL.md +1 -1
  155. package/harness/bundles/java/cursor/harness-knowledge-ingest/SKILL.md +1 -1
  156. package/harness/bundles/java/cursor/harness-knowledge-query/SKILL.md +1 -1
  157. package/harness/bundles/java/cursor/harness-package/SKILL.md +1 -1
  158. package/harness/bundles/java/cursor/harness-plan/SKILL.md +1 -1
  159. package/harness/bundles/java/cursor/harness-review/SKILL.md +3 -1
  160. package/harness/bundles/java/cursor/harness-review/protocols.md +2 -1
  161. package/harness/bundles/java/cursor/harness-run/SKILL.md +4 -4
  162. package/harness/bundles/java/cursor/harness-submit/SKILL.md +1 -1
  163. package/harness/bundles/java/cursor/harness-sync/SKILL.md +1 -1
  164. package/harness/bundles/java/cursor/harness-test/SKILL.md +5 -3
  165. package/harness/bundles/java/cursor/protocols/report-pipeline-protocol.md +2 -0
  166. package/harness/bundles/java/cursor/scripts/harness_context.py +423 -77
  167. package/harness/bundles/java/cursor/scripts/harness_events.py +102 -12
  168. package/harness/bundles/java/cursor/scripts/harness_fixback.py +529 -1
  169. package/harness/bundles/java/cursor/scripts/harness_gate.py +161 -6
  170. package/harness/bundles/java/cursor/scripts/harness_ledger.py +81 -8
  171. package/harness/bundles/java/cursor/scripts/harness_review.py +25 -2
  172. package/harness/bundles/java/cursor/scripts/harness_test_runner.py +155 -1
  173. package/harness/manifests/general/claude-code.json +23 -23
  174. package/harness/manifests/general/codebuddy.json +23 -23
  175. package/harness/manifests/general/codex.json +23 -23
  176. package/harness/manifests/general/cursor.json +23 -23
  177. package/harness/manifests/java/claude-code.json +24 -24
  178. package/harness/manifests/java/codebuddy.json +24 -24
  179. package/harness/manifests/java/codex.json +24 -24
  180. package/harness/manifests/java/cursor.json +24 -24
  181. package/hunter-workflow-family.json +5 -5
  182. package/package.json +1 -1
@@ -9,8 +9,10 @@ import hashlib
9
9
  import json
10
10
  import os
11
11
  import re
12
+ import subprocess
12
13
  import tempfile
13
14
  import sys
15
+ import time
14
16
  import uuid
15
17
  from pathlib import Path
16
18
  from typing import Any
@@ -21,6 +23,7 @@ if str(SCRIPTS_DIR) not in sys.path:
21
23
 
22
24
  from harness_paths import resolve_state_dir_for_contract
23
25
  import harness_events as he
26
+ import harness_review as hr
24
27
 
25
28
 
26
29
  SCHEMA_VERSION = 1
@@ -116,6 +119,477 @@ def _next_step(batch: dict[str, Any]) -> str:
116
119
  return "close"
117
120
 
118
121
 
122
+ def review_fixback_plan(change_dir: Path) -> dict[str, Any]:
123
+ """Read Review sidecars and return only actionable product-code fixes."""
124
+
125
+ review_root = _state_root(change_dir) / "reports" / "review"
126
+ findings_path = review_root / "review-findings.json"
127
+ dispositions_path = review_root / "fixback-dispositions.json"
128
+ if not findings_path.is_file() or not dispositions_path.is_file():
129
+ return {
130
+ "ok": False,
131
+ "code": "FIXBACK_REVIEW_OUTPUTS_REQUIRED",
132
+ "message": "评审结构化记录不完整,无法安全创建修复批次。",
133
+ "recoveryAction": "先补齐评审发现与处置记录,再重新执行修复",
134
+ }
135
+ try:
136
+ findings_doc = json.loads(findings_path.read_text(encoding="utf-8-sig"))
137
+ dispositions_doc = json.loads(
138
+ dispositions_path.read_text(encoding="utf-8-sig")
139
+ )
140
+ except (OSError, json.JSONDecodeError) as exc:
141
+ return {
142
+ "ok": False,
143
+ "code": "FIXBACK_REVIEW_OUTPUTS_INVALID",
144
+ "message": f"评审结构化记录无法读取:{exc}",
145
+ }
146
+ if not isinstance(findings_doc, dict) or not isinstance(dispositions_doc, dict):
147
+ return {
148
+ "ok": False,
149
+ "code": "FIXBACK_REVIEW_OUTPUTS_INVALID",
150
+ "message": "评审结构化记录格式无效。",
151
+ }
152
+ if findings_doc.get("runId") != dispositions_doc.get("runId"):
153
+ return {
154
+ "ok": False,
155
+ "code": "FIXBACK_REVIEW_RUN_MISMATCH",
156
+ "message": "评审发现与处置记录不属于同一轮评审。",
157
+ }
158
+ finding_problems = hr.validate_findings(findings_doc, require_ids=True)
159
+ known_ids = {
160
+ str(item.get("id"))
161
+ for item in findings_doc.get("findings", [])
162
+ if isinstance(item, dict) and item.get("id")
163
+ }
164
+ disposition_problems = hr.validate_dispositions(dispositions_doc, known_ids)
165
+ if finding_problems or disposition_problems:
166
+ return {
167
+ "ok": False,
168
+ "code": "FIXBACK_REVIEW_OUTPUTS_INVALID",
169
+ "message": "评审结构化记录不完整,无法安全创建修复批次。",
170
+ "problems": [*finding_problems, *disposition_problems],
171
+ "recoveryAction": "重新写入完整的评审发现与处置记录后重试",
172
+ }
173
+ dispositions = {
174
+ str(item.get("findingId")): str(item.get("disposition") or "UNKNOWN")
175
+ for item in dispositions_doc.get("dispositions", [])
176
+ if isinstance(item, dict) and item.get("findingId")
177
+ }
178
+ finding_ids = {
179
+ str(item.get("id"))
180
+ for item in findings_doc.get("findings", [])
181
+ if isinstance(item, dict) and item.get("id")
182
+ }
183
+ invalid_actions = sorted(
184
+ str(item.get("id") or f"index-{index}")
185
+ for index, item in enumerate(findings_doc.get("findings", []))
186
+ if isinstance(item, dict)
187
+ and item.get("fixbackAction") not in {"code", "manual", "workflow"}
188
+ )
189
+ missing_dispositions = sorted(finding_ids - set(dispositions))
190
+ if invalid_actions or missing_dispositions:
191
+ return {
192
+ "ok": False,
193
+ "code": "FIXBACK_REVIEW_OUTPUTS_INVALID",
194
+ "message": "评审项缺少明确的修复动作或处置状态。",
195
+ "invalidActions": invalid_actions,
196
+ "missingDispositions": missing_dispositions,
197
+ "recoveryAction": "补齐评审发现的 fixbackAction 与 disposition 后重试",
198
+ }
199
+ actionable: list[dict[str, Any]] = []
200
+ informational: list[dict[str, Any]] = []
201
+ for finding in findings_doc.get("findings", []):
202
+ if not isinstance(finding, dict) or not finding.get("id"):
203
+ continue
204
+ disposition = dispositions.get(str(finding["id"]), "UNKNOWN")
205
+ action = str(finding.get("fixbackAction"))
206
+ item = {
207
+ "issueId": str(finding["id"]),
208
+ "summary": str(finding.get("title") or "评审问题"),
209
+ "severity": str(finding.get("severity") or "YELLOW"),
210
+ "path": str(finding.get("path") or ""),
211
+ "line": int(finding.get("line") or 0),
212
+ "riskTags": sorted(
213
+ {
214
+ str(tag)
215
+ for tag in finding.get("riskTags", [])
216
+ if str(tag).strip()
217
+ }
218
+ ),
219
+ "disposition": disposition,
220
+ "fixbackAction": action,
221
+ }
222
+ if (
223
+ action == "code"
224
+ and item["severity"] in {"RED", "YELLOW"}
225
+ and disposition in {"OPEN", "UNKNOWN"}
226
+ ):
227
+ actionable.append(item)
228
+ else:
229
+ informational.append(item)
230
+ if not actionable:
231
+ return {
232
+ "ok": True,
233
+ "code": "FIXBACK_NOTHING_TO_APPLY",
234
+ "message": "本轮评审没有需要执行的代码修复。",
235
+ "reviewRunId": findings_doc.get("runId"),
236
+ "issues": [],
237
+ "informationalItems": informational,
238
+ "writesState": False,
239
+ }
240
+ return {
241
+ "ok": True,
242
+ "code": "FIXBACK_READY",
243
+ "message": f"已找到 {len(actionable)} 个需要处理的代码修复项。",
244
+ "reviewRunId": findings_doc.get("runId"),
245
+ "issues": actionable,
246
+ "informationalItems": informational,
247
+ "writesState": False,
248
+ }
249
+
250
+
251
+ def open_review_batch(
252
+ change_dir: Path,
253
+ *,
254
+ plan: dict[str, Any],
255
+ batch_id: str,
256
+ product_identity: str,
257
+ run_id: str,
258
+ attempt: int,
259
+ ) -> dict[str, Any]:
260
+ """Atomically create a populated batch from a validated Review plan."""
261
+
262
+ issues = plan.get("issues") if isinstance(plan, dict) else None
263
+ if plan.get("code") != "FIXBACK_READY" or not isinstance(issues, list) or not issues:
264
+ raise ValueError("FIXBACK_NOTHING_TO_APPLY")
265
+ if not product_identity.strip():
266
+ raise ValueError("FIXBACK_PRODUCT_IDENTITY_REQUIRED")
267
+ opened = [item for item in _all_batches(change_dir) if item.get("status") == "OPEN"]
268
+ if opened:
269
+ existing = sorted(opened, key=lambda item: str(item.get("openedAt") or ""))[0]
270
+ if existing.get("sourceReviewRunId") == plan.get("reviewRunId"):
271
+ result = dict(existing)
272
+ result.update(
273
+ {
274
+ "ok": True,
275
+ "code": "FIXBACK_RESUMED",
276
+ "resumed": True,
277
+ "runId": run_id,
278
+ "attempt": max(1, int(attempt)),
279
+ "nextStep": _next_step(existing),
280
+ }
281
+ )
282
+ return result
283
+ raise ValueError(
284
+ "FIXBACK_BATCH_ALREADY_OPEN: " + str(existing.get("batchId"))
285
+ )
286
+ path = _batch_path(change_dir, batch_id)
287
+ if path.exists():
288
+ raise ValueError(f"FIXBACK_BATCH_EXISTS: {batch_id}")
289
+ opened_at = now_iso()
290
+ issue_entries = [
291
+ {
292
+ "issueId": str(item["issueId"]),
293
+ "summary": str(item["summary"]),
294
+ "status": "OPEN",
295
+ "riskTags": list(item.get("riskTags") or []),
296
+ "severity": item.get("severity"),
297
+ "path": item.get("path"),
298
+ "line": item.get("line"),
299
+ "redEvidence": None,
300
+ "greenEvidence": None,
301
+ "changedFiles": [],
302
+ "resolvedAt": None,
303
+ }
304
+ for item in issues
305
+ ]
306
+ batch = {
307
+ "schemaVersion": SCHEMA_VERSION,
308
+ "batchId": batch_id,
309
+ "status": "OPEN",
310
+ "rootCause": "处理评审中明确要求的代码修复项",
311
+ "sourceReviewRunId": plan.get("reviewRunId"),
312
+ "openedAt": opened_at,
313
+ "updatedAt": opened_at,
314
+ "closedAt": None,
315
+ "baseProductIdentity": product_identity,
316
+ "finalProductIdentity": None,
317
+ "issues": issue_entries,
318
+ "changedFiles": [],
319
+ "requiredVerifications": _required_verifications(issue_entries),
320
+ "receipts": {"affected": None, "review": None},
321
+ "verificationRuns": {"affected": 0, "review": 0},
322
+ }
323
+ _write_json(path, batch)
324
+ session = {
325
+ "schemaVersion": 1,
326
+ "status": "ACTIVE",
327
+ "batchId": batch_id,
328
+ "runId": run_id,
329
+ "attempt": max(1, int(attempt)),
330
+ "nextStep": _next_step(batch),
331
+ "updatedAt": opened_at,
332
+ }
333
+ _write_json(_session_path(change_dir), session)
334
+ he.append_event(
335
+ change_dir,
336
+ phase="run",
337
+ type_="decision",
338
+ note=f"已从评审结果创建修复批次,共 {len(issue_entries)} 个代码修复项。",
339
+ run_id=run_id,
340
+ attempt=session["attempt"],
341
+ trigger="review-fixback",
342
+ from_phase="review",
343
+ )
344
+ return {
345
+ "ok": True,
346
+ "code": "FIXBACK_OPENED",
347
+ "resumed": False,
348
+ **batch,
349
+ **session,
350
+ }
351
+
352
+
353
+ def _default_context_prepare(**kwargs: Any) -> dict[str, Any]:
354
+ import harness_context as hctx
355
+
356
+ return hctx.prepare_context(
357
+ kwargs["project"],
358
+ phase="run",
359
+ executor=kwargs["executor"],
360
+ change=kwargs["change"],
361
+ trigger="review-fixback",
362
+ preparation_id=kwargs["preparation_id"],
363
+ )
364
+
365
+
366
+ def _default_context_begin(**kwargs: Any) -> dict[str, Any]:
367
+ import harness_context as hctx
368
+
369
+ return hctx.begin_transition(
370
+ kwargs["project"],
371
+ kwargs["change"],
372
+ phase="run",
373
+ executor=kwargs["executor"],
374
+ preparation_id=kwargs["preparation_id"],
375
+ )
376
+
377
+
378
+ def _default_context_cancel(**kwargs: Any) -> dict[str, Any]:
379
+ import harness_context as hctx
380
+
381
+ return hctx.cancel_prepared_context(
382
+ kwargs["project"],
383
+ kwargs["change"],
384
+ phase="run",
385
+ executor=kwargs["executor"],
386
+ preparation_id=kwargs["preparation_id"],
387
+ )
388
+
389
+
390
+ def _default_gate_begin(**kwargs: Any) -> dict[str, Any]:
391
+ command = [
392
+ sys.executable,
393
+ str(SCRIPTS_DIR / "harness_gate.py"),
394
+ "begin",
395
+ "--json",
396
+ "--phase",
397
+ "run",
398
+ "--change",
399
+ str(kwargs["change"]),
400
+ "--project",
401
+ str(kwargs["project"]),
402
+ "--skills-root",
403
+ str(kwargs["skills_root"]),
404
+ "--run-id",
405
+ str(kwargs["run_id"]),
406
+ "--note",
407
+ "开始处理评审中确认需要修改的代码问题。",
408
+ ]
409
+ for option, key in (
410
+ ("--executor-tool", "executor_tool"),
411
+ ("--executor-agent", "executor_agent"),
412
+ ("--executor-model", "executor_model"),
413
+ ):
414
+ value = kwargs.get(key)
415
+ if value:
416
+ command.extend([option, str(value)])
417
+ if kwargs.get("task") is not None:
418
+ command.extend(["--task", str(kwargs["task"])])
419
+ completed = subprocess.run(
420
+ command,
421
+ cwd=str(kwargs["project"]),
422
+ capture_output=True,
423
+ text=True,
424
+ encoding="utf-8",
425
+ errors="replace",
426
+ check=False,
427
+ )
428
+ raw = completed.stdout.strip() or completed.stderr.strip()
429
+ try:
430
+ payload = json.loads(raw)
431
+ except json.JSONDecodeError:
432
+ payload = {
433
+ "ok": False,
434
+ "code": "FIXBACK_GATE_COMMAND_FAILED",
435
+ "message": raw or "修复门禁未返回有效结果。",
436
+ }
437
+ if not isinstance(payload, dict):
438
+ payload = {
439
+ "ok": False,
440
+ "code": "FIXBACK_GATE_COMMAND_FAILED",
441
+ "message": "修复门禁返回了无效结果。",
442
+ }
443
+ payload.setdefault("ok", completed.returncode == 0)
444
+ return payload
445
+
446
+
447
+ def launch_review_fixback(
448
+ *,
449
+ project: Path,
450
+ change: str,
451
+ change_dir: Path,
452
+ executor: str,
453
+ skills_root: Path,
454
+ product_identity: str,
455
+ run_id: str | None = None,
456
+ attempt: int = 2,
457
+ batch_id: str | None = None,
458
+ task: int | None = None,
459
+ executor_tool: str | None = None,
460
+ executor_agent: str | None = None,
461
+ executor_model: str | None = None,
462
+ context_prepare: Any = None,
463
+ context_begin: Any = None,
464
+ gate_begin: Any = None,
465
+ context_cancel: Any = None,
466
+ ) -> dict[str, Any]:
467
+ """Plan, claim and begin Review Fixback through one fail-closed command."""
468
+
469
+ project = Path(project).resolve()
470
+ change_dir = Path(change_dir).resolve()
471
+ plan = review_fixback_plan(change_dir)
472
+ if not plan.get("ok") or plan.get("code") == "FIXBACK_NOTHING_TO_APPLY":
473
+ return plan
474
+ effective_run_id = run_id or "fixback-" + uuid.uuid4().hex
475
+ effective_attempt = max(2, int(attempt))
476
+ if batch_id is None:
477
+ source = f"{change}:{plan.get('reviewRunId') or 'review'}"
478
+ batch_id = "fb-" + hashlib.sha256(source.encode("utf-8")).hexdigest()[:16]
479
+ prepare_fn = context_prepare or _default_context_prepare
480
+ begin_fn = context_begin or _default_context_begin
481
+ gate_fn = gate_begin or _default_gate_begin
482
+ cancel_fn = context_cancel or _default_context_cancel
483
+ started_at = time.monotonic()
484
+ start_event = he.append_event(
485
+ change_dir,
486
+ phase="run",
487
+ type_="phase.prepare.start",
488
+ note="正在检查评审修复的前置条件。",
489
+ run_id=effective_run_id,
490
+ attempt=effective_attempt,
491
+ trigger="review-fixback",
492
+ from_phase="review",
493
+ executor_tool=executor_tool,
494
+ executor_agent=executor_agent,
495
+ executor_model=executor_model,
496
+ )
497
+ if not start_event.get("ok"):
498
+ return start_event
499
+ callback_args = {
500
+ "project": project,
501
+ "change": change,
502
+ "change_dir": change_dir,
503
+ "executor": executor,
504
+ "skills_root": Path(skills_root).resolve(),
505
+ "run_id": effective_run_id,
506
+ "attempt": effective_attempt,
507
+ "task": task,
508
+ "executor_tool": executor_tool,
509
+ "executor_agent": executor_agent,
510
+ "executor_model": executor_model,
511
+ "preparation_id": effective_run_id,
512
+ }
513
+
514
+ def blocked(result: dict[str, Any], *, cancel: bool) -> dict[str, Any]:
515
+ cancellation = cancel_fn(**callback_args) if cancel else None
516
+ duration = max(0, round((time.monotonic() - started_at) * 1000))
517
+ code = str(result.get("code") or "FIXBACK_PREPARATION_BLOCKED")
518
+ message = str(result.get("message") or result.get("error") or "前置条件未满足,修复未启动。")
519
+ he.append_event(
520
+ change_dir,
521
+ phase="run",
522
+ type_="phase.prepare.end",
523
+ status="BLOCKED",
524
+ code=code,
525
+ message=message,
526
+ run_id=effective_run_id,
527
+ attempt=effective_attempt,
528
+ trigger="review-fixback",
529
+ from_phase="review",
530
+ orchestration_active_ms=duration,
531
+ wall_clock_ms=duration,
532
+ executor_tool=executor_tool,
533
+ executor_agent=executor_agent,
534
+ executor_model=executor_model,
535
+ )
536
+ return {
537
+ "ok": False,
538
+ "code": code,
539
+ "message": message,
540
+ "recoveryAction": "处理提示的前置条件后重新执行 /harness-run --fixback",
541
+ "preparationDurationMs": duration,
542
+ "contextCancellation": cancellation,
543
+ }
544
+
545
+ prepared = prepare_fn(**callback_args)
546
+ if not prepared.get("ok"):
547
+ return blocked(prepared, cancel=False)
548
+ begun = begin_fn(**callback_args)
549
+ if not begun.get("ok"):
550
+ return blocked(begun, cancel=True)
551
+ gate = gate_fn(**callback_args)
552
+ if not gate.get("ok"):
553
+ return blocked(gate, cancel=True)
554
+ batch = open_review_batch(
555
+ change_dir,
556
+ plan=plan,
557
+ batch_id=batch_id,
558
+ product_identity=product_identity,
559
+ run_id=effective_run_id,
560
+ attempt=effective_attempt,
561
+ )
562
+ duration = max(0, round((time.monotonic() - started_at) * 1000))
563
+ he.append_event(
564
+ change_dir,
565
+ phase="run",
566
+ type_="phase.prepare.end",
567
+ status="STARTED",
568
+ message="前置条件已满足,修复编码已启动。",
569
+ run_id=effective_run_id,
570
+ attempt=effective_attempt,
571
+ trigger="review-fixback",
572
+ from_phase="review",
573
+ orchestration_active_ms=duration,
574
+ wall_clock_ms=duration,
575
+ executor_tool=executor_tool,
576
+ executor_agent=executor_agent,
577
+ executor_model=executor_model,
578
+ )
579
+ return {
580
+ "ok": True,
581
+ "code": "FIXBACK_STARTED",
582
+ "message": f"修复编码已启动,共 {len(batch.get('issues') or [])} 个代码修复项。",
583
+ "runId": effective_run_id,
584
+ "attempt": effective_attempt,
585
+ "preparationDurationMs": duration,
586
+ "context": prepared,
587
+ "transition": begun,
588
+ "gate": gate,
589
+ "batch": batch,
590
+ }
591
+
592
+
119
593
  def resume_batch(
120
594
  change_dir: Path,
121
595
  *,
@@ -823,8 +1297,10 @@ def _emit(value: dict[str, Any]) -> int:
823
1297
 
824
1298
  def build_parser() -> argparse.ArgumentParser:
825
1299
  parser = argparse.ArgumentParser(prog="harness_fixback.py")
1300
+ parser.add_argument("--json", action="store_true")
826
1301
  sub = parser.add_subparsers(dest="command", required=True)
827
1302
  resume = sub.add_parser("resume")
1303
+ resume.add_argument("--json", action="store_true")
828
1304
  resume.add_argument("--change-dir", required=True)
829
1305
  resume.add_argument("--batch-id")
830
1306
  resume.add_argument("--product-identity")
@@ -864,6 +1340,31 @@ def build_parser() -> argparse.ArgumentParser:
864
1340
  register = sub.add_parser("register-evidence")
865
1341
  register.add_argument("--change-dir", required=True)
866
1342
  register.add_argument("--evidence", required=True)
1343
+ review_plan = sub.add_parser("review-plan")
1344
+ review_plan.add_argument("--change-dir", required=True)
1345
+ review_plan.add_argument("--json", action="store_true")
1346
+ review_start = sub.add_parser("start-review")
1347
+ review_start.add_argument("--change-dir", required=True)
1348
+ review_start.add_argument("--batch-id", required=True)
1349
+ review_start.add_argument("--product-identity", required=True)
1350
+ review_start.add_argument("--run-id", required=True)
1351
+ review_start.add_argument("--attempt", type=int, required=True)
1352
+ review_start.add_argument("--json", action="store_true")
1353
+ launch_review = sub.add_parser("launch-review")
1354
+ launch_review.add_argument("--project", required=True)
1355
+ launch_review.add_argument("--change", required=True)
1356
+ launch_review.add_argument("--change-dir", required=True)
1357
+ launch_review.add_argument("--executor", required=True)
1358
+ launch_review.add_argument("--skills-root", required=True)
1359
+ launch_review.add_argument("--product-identity", required=True)
1360
+ launch_review.add_argument("--run-id")
1361
+ launch_review.add_argument("--attempt", type=int, default=2)
1362
+ launch_review.add_argument("--batch-id")
1363
+ launch_review.add_argument("--task", type=int)
1364
+ launch_review.add_argument("--executor-tool")
1365
+ launch_review.add_argument("--executor-agent")
1366
+ launch_review.add_argument("--executor-model")
1367
+ launch_review.add_argument("--json", action="store_true")
867
1368
  return parser
868
1369
 
869
1370
 
@@ -871,7 +1372,34 @@ def main(argv: list[str] | None = None) -> int:
871
1372
  args = build_parser().parse_args(argv)
872
1373
  change_dir = Path(args.change_dir)
873
1374
  try:
874
- if args.command == "resume":
1375
+ if args.command == "review-plan":
1376
+ value = review_fixback_plan(change_dir)
1377
+ elif args.command == "start-review":
1378
+ value = open_review_batch(
1379
+ change_dir,
1380
+ plan=review_fixback_plan(change_dir),
1381
+ batch_id=args.batch_id,
1382
+ product_identity=args.product_identity,
1383
+ run_id=args.run_id,
1384
+ attempt=args.attempt,
1385
+ )
1386
+ elif args.command == "launch-review":
1387
+ value = launch_review_fixback(
1388
+ project=Path(args.project),
1389
+ change=args.change,
1390
+ change_dir=change_dir,
1391
+ executor=args.executor,
1392
+ skills_root=Path(args.skills_root),
1393
+ product_identity=args.product_identity,
1394
+ run_id=args.run_id,
1395
+ attempt=args.attempt,
1396
+ batch_id=args.batch_id,
1397
+ task=args.task,
1398
+ executor_tool=args.executor_tool,
1399
+ executor_agent=args.executor_agent,
1400
+ executor_model=args.executor_model,
1401
+ )
1402
+ elif args.command == "resume":
875
1403
  value = resume_batch(
876
1404
  change_dir,
877
1405
  batch_id=args.batch_id,