@hunter-harness/workflow-harness 0.2.76 → 0.2.78

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 +5 -3
  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-pull/SKILL.md +1 -1
  8. package/harness/bundles/general/claude-code/harness-push/SKILL.md +1 -1
  9. package/harness/bundles/general/claude-code/harness-review/SKILL.md +1 -1
  10. package/harness/bundles/general/claude-code/harness-run/SKILL.md +1 -1
  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 +1 -1
  14. package/harness/bundles/general/claude-code/protocols/powershell-protocol.md +20 -0
  15. package/harness/bundles/general/claude-code/scripts/harness_archive.py +55 -0
  16. package/harness/bundles/general/claude-code/scripts/harness_check_gate.py +2 -0
  17. package/harness/bundles/general/claude-code/scripts/harness_context.py +4 -0
  18. package/harness/bundles/general/claude-code/scripts/harness_fixback.py +102 -3
  19. package/harness/bundles/general/claude-code/scripts/harness_knowledge_candidates.py +262 -0
  20. package/harness/bundles/general/claude-code/scripts/harness_ledger.py +4 -0
  21. package/harness/bundles/general/claude-code/scripts/harness_runtime.py +76 -2
  22. package/harness/bundles/general/codebuddy/.harness-build.json +1 -1
  23. package/harness/bundles/general/codebuddy/harness-archive/SKILL.md +5 -3
  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-pull/SKILL.md +1 -1
  29. package/harness/bundles/general/codebuddy/harness-push/SKILL.md +1 -1
  30. package/harness/bundles/general/codebuddy/harness-review/SKILL.md +1 -1
  31. package/harness/bundles/general/codebuddy/harness-run/SKILL.md +1 -1
  32. package/harness/bundles/general/codebuddy/harness-submit/SKILL.md +1 -1
  33. package/harness/bundles/general/codebuddy/harness-sync/SKILL.md +1 -1
  34. package/harness/bundles/general/codebuddy/harness-test/SKILL.md +1 -1
  35. package/harness/bundles/general/codebuddy/protocols/powershell-protocol.md +20 -0
  36. package/harness/bundles/general/codebuddy/scripts/harness_archive.py +55 -0
  37. package/harness/bundles/general/codebuddy/scripts/harness_check_gate.py +2 -0
  38. package/harness/bundles/general/codebuddy/scripts/harness_context.py +4 -0
  39. package/harness/bundles/general/codebuddy/scripts/harness_fixback.py +102 -3
  40. package/harness/bundles/general/codebuddy/scripts/harness_knowledge_candidates.py +262 -0
  41. package/harness/bundles/general/codebuddy/scripts/harness_ledger.py +4 -0
  42. package/harness/bundles/general/codebuddy/scripts/harness_runtime.py +76 -2
  43. package/harness/bundles/general/codex/.harness-build.json +1 -1
  44. package/harness/bundles/general/codex/harness-archive/SKILL.md +5 -3
  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-pull/SKILL.md +1 -1
  50. package/harness/bundles/general/codex/harness-push/SKILL.md +1 -1
  51. package/harness/bundles/general/codex/harness-review/SKILL.md +1 -1
  52. package/harness/bundles/general/codex/harness-run/SKILL.md +1 -1
  53. package/harness/bundles/general/codex/harness-submit/SKILL.md +1 -1
  54. package/harness/bundles/general/codex/harness-sync/SKILL.md +1 -1
  55. package/harness/bundles/general/codex/harness-test/SKILL.md +1 -1
  56. package/harness/bundles/general/codex/protocols/powershell-protocol.md +20 -0
  57. package/harness/bundles/general/codex/scripts/harness_archive.py +55 -0
  58. package/harness/bundles/general/codex/scripts/harness_check_gate.py +2 -0
  59. package/harness/bundles/general/codex/scripts/harness_context.py +4 -0
  60. package/harness/bundles/general/codex/scripts/harness_fixback.py +102 -3
  61. package/harness/bundles/general/codex/scripts/harness_knowledge_candidates.py +262 -0
  62. package/harness/bundles/general/codex/scripts/harness_ledger.py +4 -0
  63. package/harness/bundles/general/codex/scripts/harness_runtime.py +76 -2
  64. package/harness/bundles/general/cursor/.harness-build.json +1 -1
  65. package/harness/bundles/general/cursor/harness-archive/SKILL.md +5 -3
  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-pull/SKILL.md +1 -1
  71. package/harness/bundles/general/cursor/harness-push/SKILL.md +1 -1
  72. package/harness/bundles/general/cursor/harness-review/SKILL.md +1 -1
  73. package/harness/bundles/general/cursor/harness-run/SKILL.md +1 -1
  74. package/harness/bundles/general/cursor/harness-submit/SKILL.md +1 -1
  75. package/harness/bundles/general/cursor/harness-sync/SKILL.md +1 -1
  76. package/harness/bundles/general/cursor/harness-test/SKILL.md +1 -1
  77. package/harness/bundles/general/cursor/protocols/powershell-protocol.md +20 -0
  78. package/harness/bundles/general/cursor/scripts/harness_archive.py +55 -0
  79. package/harness/bundles/general/cursor/scripts/harness_check_gate.py +2 -0
  80. package/harness/bundles/general/cursor/scripts/harness_context.py +4 -0
  81. package/harness/bundles/general/cursor/scripts/harness_fixback.py +102 -3
  82. package/harness/bundles/general/cursor/scripts/harness_knowledge_candidates.py +262 -0
  83. package/harness/bundles/general/cursor/scripts/harness_ledger.py +4 -0
  84. package/harness/bundles/general/cursor/scripts/harness_runtime.py +76 -2
  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 +5 -3
  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-pull/SKILL.md +1 -1
  94. package/harness/bundles/java/claude-code/harness-push/SKILL.md +1 -1
  95. package/harness/bundles/java/claude-code/harness-review/SKILL.md +1 -1
  96. package/harness/bundles/java/claude-code/harness-run/SKILL.md +1 -1
  97. package/harness/bundles/java/claude-code/harness-submit/SKILL.md +1 -1
  98. package/harness/bundles/java/claude-code/harness-sync/SKILL.md +1 -1
  99. package/harness/bundles/java/claude-code/harness-test/SKILL.md +1 -1
  100. package/harness/bundles/java/claude-code/protocols/powershell-protocol.md +20 -0
  101. package/harness/bundles/java/claude-code/scripts/harness_archive.py +55 -0
  102. package/harness/bundles/java/claude-code/scripts/harness_check_gate.py +2 -0
  103. package/harness/bundles/java/claude-code/scripts/harness_context.py +4 -0
  104. package/harness/bundles/java/claude-code/scripts/harness_fixback.py +102 -3
  105. package/harness/bundles/java/claude-code/scripts/harness_knowledge_candidates.py +262 -0
  106. package/harness/bundles/java/claude-code/scripts/harness_ledger.py +4 -0
  107. package/harness/bundles/java/claude-code/scripts/harness_runtime.py +76 -2
  108. package/harness/bundles/java/codebuddy/.harness-build.json +1 -1
  109. package/harness/bundles/java/codebuddy/harness-apidoc/SKILL.md +1 -1
  110. package/harness/bundles/java/codebuddy/harness-archive/SKILL.md +5 -3
  111. package/harness/bundles/java/codebuddy/harness-codebase-map/SKILL.md +1 -1
  112. package/harness/bundles/java/codebuddy/harness-knowledge-ingest/SKILL.md +1 -1
  113. package/harness/bundles/java/codebuddy/harness-knowledge-query/SKILL.md +1 -1
  114. package/harness/bundles/java/codebuddy/harness-package/SKILL.md +1 -1
  115. package/harness/bundles/java/codebuddy/harness-plan/SKILL.md +1 -1
  116. package/harness/bundles/java/codebuddy/harness-pull/SKILL.md +1 -1
  117. package/harness/bundles/java/codebuddy/harness-push/SKILL.md +1 -1
  118. package/harness/bundles/java/codebuddy/harness-review/SKILL.md +1 -1
  119. package/harness/bundles/java/codebuddy/harness-run/SKILL.md +1 -1
  120. package/harness/bundles/java/codebuddy/harness-submit/SKILL.md +1 -1
  121. package/harness/bundles/java/codebuddy/harness-sync/SKILL.md +1 -1
  122. package/harness/bundles/java/codebuddy/harness-test/SKILL.md +1 -1
  123. package/harness/bundles/java/codebuddy/protocols/powershell-protocol.md +20 -0
  124. package/harness/bundles/java/codebuddy/scripts/harness_archive.py +55 -0
  125. package/harness/bundles/java/codebuddy/scripts/harness_check_gate.py +2 -0
  126. package/harness/bundles/java/codebuddy/scripts/harness_context.py +4 -0
  127. package/harness/bundles/java/codebuddy/scripts/harness_fixback.py +102 -3
  128. package/harness/bundles/java/codebuddy/scripts/harness_knowledge_candidates.py +262 -0
  129. package/harness/bundles/java/codebuddy/scripts/harness_ledger.py +4 -0
  130. package/harness/bundles/java/codebuddy/scripts/harness_runtime.py +76 -2
  131. package/harness/bundles/java/codex/.harness-build.json +1 -1
  132. package/harness/bundles/java/codex/harness-apidoc/SKILL.md +1 -1
  133. package/harness/bundles/java/codex/harness-archive/SKILL.md +5 -3
  134. package/harness/bundles/java/codex/harness-codebase-map/SKILL.md +1 -1
  135. package/harness/bundles/java/codex/harness-knowledge-ingest/SKILL.md +1 -1
  136. package/harness/bundles/java/codex/harness-knowledge-query/SKILL.md +1 -1
  137. package/harness/bundles/java/codex/harness-package/SKILL.md +1 -1
  138. package/harness/bundles/java/codex/harness-plan/SKILL.md +1 -1
  139. package/harness/bundles/java/codex/harness-pull/SKILL.md +1 -1
  140. package/harness/bundles/java/codex/harness-push/SKILL.md +1 -1
  141. package/harness/bundles/java/codex/harness-review/SKILL.md +1 -1
  142. package/harness/bundles/java/codex/harness-run/SKILL.md +1 -1
  143. package/harness/bundles/java/codex/harness-submit/SKILL.md +1 -1
  144. package/harness/bundles/java/codex/harness-sync/SKILL.md +1 -1
  145. package/harness/bundles/java/codex/harness-test/SKILL.md +1 -1
  146. package/harness/bundles/java/codex/protocols/powershell-protocol.md +20 -0
  147. package/harness/bundles/java/codex/scripts/harness_archive.py +55 -0
  148. package/harness/bundles/java/codex/scripts/harness_check_gate.py +2 -0
  149. package/harness/bundles/java/codex/scripts/harness_context.py +4 -0
  150. package/harness/bundles/java/codex/scripts/harness_fixback.py +102 -3
  151. package/harness/bundles/java/codex/scripts/harness_knowledge_candidates.py +262 -0
  152. package/harness/bundles/java/codex/scripts/harness_ledger.py +4 -0
  153. package/harness/bundles/java/codex/scripts/harness_runtime.py +76 -2
  154. package/harness/bundles/java/cursor/.harness-build.json +1 -1
  155. package/harness/bundles/java/cursor/harness-apidoc/SKILL.md +1 -1
  156. package/harness/bundles/java/cursor/harness-archive/SKILL.md +5 -3
  157. package/harness/bundles/java/cursor/harness-codebase-map/SKILL.md +1 -1
  158. package/harness/bundles/java/cursor/harness-knowledge-ingest/SKILL.md +1 -1
  159. package/harness/bundles/java/cursor/harness-knowledge-query/SKILL.md +1 -1
  160. package/harness/bundles/java/cursor/harness-package/SKILL.md +1 -1
  161. package/harness/bundles/java/cursor/harness-plan/SKILL.md +1 -1
  162. package/harness/bundles/java/cursor/harness-pull/SKILL.md +1 -1
  163. package/harness/bundles/java/cursor/harness-push/SKILL.md +1 -1
  164. package/harness/bundles/java/cursor/harness-review/SKILL.md +1 -1
  165. package/harness/bundles/java/cursor/harness-run/SKILL.md +1 -1
  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 +1 -1
  169. package/harness/bundles/java/cursor/protocols/powershell-protocol.md +20 -0
  170. package/harness/bundles/java/cursor/scripts/harness_archive.py +55 -0
  171. package/harness/bundles/java/cursor/scripts/harness_check_gate.py +2 -0
  172. package/harness/bundles/java/cursor/scripts/harness_context.py +4 -0
  173. package/harness/bundles/java/cursor/scripts/harness_fixback.py +102 -3
  174. package/harness/bundles/java/cursor/scripts/harness_knowledge_candidates.py +262 -0
  175. package/harness/bundles/java/cursor/scripts/harness_ledger.py +4 -0
  176. package/harness/bundles/java/cursor/scripts/harness_runtime.py +76 -2
  177. package/harness/manifests/general/claude-code.json +26 -22
  178. package/harness/manifests/general/codebuddy.json +26 -22
  179. package/harness/manifests/general/codex.json +26 -22
  180. package/harness/manifests/general/cursor.json +26 -22
  181. package/harness/manifests/java/claude-code.json +28 -24
  182. package/harness/manifests/java/codebuddy.json +28 -24
  183. package/harness/manifests/java/codex.json +28 -24
  184. package/harness/manifests/java/cursor.json +28 -24
  185. package/hunter-workflow-family.json +5 -5
  186. package/package.json +1 -1
@@ -91,6 +91,7 @@ import harness_events as he # noqa: E402
91
91
  import harness_events_sync as hes # noqa: E402
92
92
  import harness_efficiency as heff # noqa: E402
93
93
  import harness_gate as hgate # noqa: E402
94
+ import harness_knowledge_candidates as hkc # noqa: E402
94
95
  import harness_ledger as hl # noqa: E402
95
96
  import harness_paths as hp # noqa: E402
96
97
  import harness_phase as hphase # noqa: E402
@@ -4173,6 +4174,33 @@ def write_archive_meta(work_dir: Path, summary: dict[str, Any]) -> Path:
4173
4174
  return out
4174
4175
 
4175
4176
 
4177
+ def write_knowledge_candidates(work_dir: Path, summary: dict[str, Any]) -> Path:
4178
+ """Generate candidates/knowledge.json from summary-data (single ownership).
4179
+
4180
+ Mirrors write_archive_meta: derived from the same summary, written before the
4181
+ after-manifest so its bytes are covered. The archive directory name is the
4182
+ archive id, matching write_archive_meta's `archive-id` field.
4183
+ """
4184
+ archive_id = work_dir.name
4185
+ candidates = hkc.build_knowledge_candidates(
4186
+ summary,
4187
+ change_key=str(summary.get("changeName") or archive_id),
4188
+ archive_id=archive_id,
4189
+ # The archive schema version identifies the producing format; there is
4190
+ # no separate harness version constant to borrow here.
4191
+ producer_version=SCHEMA_VERSION,
4192
+ created_at=now_iso(),
4193
+ )
4194
+ out = work_dir / "candidates" / "knowledge.json"
4195
+ out.parent.mkdir(parents=True, exist_ok=True)
4196
+ out.write_text(
4197
+ hkc.render_knowledge_candidates_json(candidates),
4198
+ encoding="utf-8",
4199
+ newline="\n",
4200
+ )
4201
+ return out
4202
+
4203
+
4176
4204
 
4177
4205
 
4178
4206
  def _changed_files_from_git(
@@ -8269,6 +8297,21 @@ def cmd_finalize(
8269
8297
  warnings.append(f"archive-meta write failed: {exc}")
8270
8298
  payload["steps"]["archive_meta"] = {"ok": False, "error": str(exc)}
8271
8299
 
8300
+ # --- 8b. knowledge candidates (also before the after-manifest) ---
8301
+ # Soft-fail like archive-meta: an archive must never be rolled back because
8302
+ # knowledge extraction found nothing. An empty array is a valid outcome.
8303
+ try:
8304
+ summary = read_json(summary_path)
8305
+ candidates_path = write_knowledge_candidates(work_dir, summary)
8306
+ payload["steps"]["knowledge_candidates"] = {
8307
+ "ok": True,
8308
+ "path": str(candidates_path),
8309
+ "count": len(read_json(candidates_path)),
8310
+ }
8311
+ except Exception as exc: # noqa: BLE001 — candidates soft-fail
8312
+ warnings.append(f"knowledge candidates write failed: {exc}")
8313
+ payload["steps"]["knowledge_candidates"] = {"ok": False, "error": str(exc)}
8314
+
8272
8315
  # --- 9/10. final summary stats, then LAST manifest (IA-7) ---
8273
8316
  # Post-manifest rewrites of covered bytes are forbidden. We update the
8274
8317
  # summary first, regenerate after-manifest last, then verify on-disk hashes.
@@ -8885,6 +8928,12 @@ def _archive_core_file_specs(
8885
8928
  file_specs.append(
8886
8929
  (source, f"{folder}/{relative}", role, "text/markdown")
8887
8930
  )
8931
+ add_if_file(
8932
+ archive / "candidates" / "knowledge.json",
8933
+ "candidates/knowledge.json",
8934
+ "knowledge_candidates",
8935
+ "application/json",
8936
+ )
8888
8937
  add_if_file(
8889
8938
  archive / "meta" / "archive-meta.md",
8890
8939
  "archive-meta.md",
@@ -8913,6 +8962,8 @@ def _archive_source_identity(project_root: Path) -> dict[str, str | None]:
8913
8962
  ["git", *args],
8914
8963
  cwd=str(project_root),
8915
8964
  text=True,
8965
+ encoding="utf-8",
8966
+ errors="replace",
8916
8967
  stdout=subprocess.PIPE,
8917
8968
  stderr=subprocess.DEVNULL,
8918
8969
  timeout=10,
@@ -9449,6 +9500,10 @@ def auto_push_archive_core(
9449
9500
  command,
9450
9501
  cwd=str(project_root),
9451
9502
  text=True,
9503
+ # 不指定 encoding 时中文 Windows 按 cp936 解码 UTF-8,
9504
+ # CLI 的中文回执会被损坏,随后的 json.loads 也可能失败。
9505
+ encoding="utf-8",
9506
+ errors="replace",
9452
9507
  stdout=subprocess.PIPE,
9453
9508
  stderr=subprocess.PIPE,
9454
9509
  timeout=180,
@@ -36,6 +36,8 @@ def _git_output(args: list[str]) -> str | None:
36
36
  cwd=str(ROOT),
37
37
  capture_output=True,
38
38
  text=True,
39
+ encoding="utf-8",
40
+ errors="replace",
39
41
  timeout=5,
40
42
  )
41
43
  except (OSError, subprocess.SubprocessError):
@@ -184,6 +184,8 @@ def _head(project: Path) -> str | None:
184
184
  process = subprocess.run(
185
185
  ["git", "-C", str(project), "rev-parse", "--verify", "HEAD"],
186
186
  text=True,
187
+ encoding="utf-8",
188
+ errors="replace",
187
189
  stdout=subprocess.PIPE,
188
190
  stderr=subprocess.PIPE,
189
191
  check=False,
@@ -196,6 +198,8 @@ def _git_common_dir(project: Path) -> Path | None:
196
198
  process = subprocess.run(
197
199
  ["git", "-C", str(project), "rev-parse", "--git-common-dir"],
198
200
  text=True,
201
+ encoding="utf-8",
202
+ errors="replace",
199
203
  stdout=subprocess.PIPE,
200
204
  stderr=subprocess.PIPE,
201
205
  check=False,
@@ -587,6 +587,13 @@ def launch_review_fixback(
587
587
  "transition": begun,
588
588
  "gate": gate,
589
589
  "batch": batch,
590
+ # 证据要求随开批次一起交底:晚一步暴露,调用方就已经改完代码,
591
+ # 只能回退伪造 RED。
592
+ "evidenceContract": evidence_contract(
593
+ change_dir=str(change_dir),
594
+ batch_id=str(batch.get("batchId") or ""),
595
+ product_identity=str(batch.get("productIdentity") or ""),
596
+ ),
590
597
  }
591
598
 
592
599
 
@@ -802,12 +809,102 @@ def _resolve_evidence_path(change_dir: Path, raw_path: str) -> Path:
802
809
  )
803
810
  path = next((candidate for candidate in candidates if candidate.is_file()), None)
804
811
  if path is None:
805
- raise ValueError(f"FIXBACK_EVIDENCE_MISSING: {raw_path}")
812
+ raise ValueError(evidence_error_message("FIXBACK_EVIDENCE_MISSING", raw_path))
806
813
  if not _is_within(path, [contract_root, state_root, project_root]):
807
- raise ValueError(f"FIXBACK_EVIDENCE_OUTSIDE_PROJECT: {raw_path}")
814
+ raise ValueError(
815
+ evidence_error_message("FIXBACK_EVIDENCE_OUTSIDE_PROJECT", raw_path)
816
+ )
808
817
  return path
809
818
 
810
819
 
820
+ def evidence_contract(
821
+ *,
822
+ change_dir: str | None = None,
823
+ batch_id: str | None = None,
824
+ product_identity: str | None = None,
825
+ ) -> dict[str, Any]:
826
+ """开批次时就交出证据契约,而不是等 resolve-issue 报错才逼调用方逆向。
827
+
828
+ 契约本身没变过:每个 code 项需要一条修复前的 RED(会话 FAIL)与一条修复后的
829
+ GREEN(会话 OK),两者都必须是 run-start 产出的托管会话证据,且先注册后引用。
830
+ 问题在于它此前只写在实现里——调用方通常已经改完代码才发现 RED 要"修复前",
831
+ 于是把改动回退、跑一次假 RED、再改回来,凭空多出一轮返工与一段脏工作树。
832
+ """
833
+ change = change_dir or "<change-dir>"
834
+ batch = batch_id or "<batch-id>"
835
+ product = product_identity or "<product-identity>"
836
+ return {
837
+ "why": "RED 证明问题真实存在,GREEN 证明修复真的生效;缺任一条都无法关批次。",
838
+ "order": [
839
+ "1. 修复前:先用 run-start 跑一次能复现该问题的命令,得到 status=FAIL 的会话(RED)",
840
+ "2. 实施修复",
841
+ "3. 修复后:用同一条命令再跑一次 run-start,得到 status=OK 的会话(GREEN)",
842
+ "4. 两条证据各写一个 JSON 文件,分别 register-evidence 注册",
843
+ "5. resolve-issue 引用这两个已注册的证据文件",
844
+ ],
845
+ "antiPattern": (
846
+ "不要先改完再把修改回退来凑 RED:那样 RED 证明的只是回退后的状态,"
847
+ "不是原始缺陷,中途还会留下脏工作树。"
848
+ ),
849
+ "commands": {
850
+ "collectSession": (
851
+ "python harness_runtime.py run-start --state-root "
852
+ f"{change} --verification <名称> --working-directory . "
853
+ f"--product-identity {product} --json -- <复现/验证命令>"
854
+ ),
855
+ "awaitSession": (
856
+ "python harness_runtime.py run-status --state-root "
857
+ f"{change} --session-id <sessionId> --wait --json"
858
+ ),
859
+ "register": (
860
+ "python harness_fixback.py register-evidence --change-dir "
861
+ f"{change} --evidence <证据 JSON 路径>"
862
+ ),
863
+ "resolve": (
864
+ "python harness_fixback.py resolve-issue --change-dir "
865
+ f"{change} --batch-id {batch} --issue-id <issueId> "
866
+ "--red-evidence <red.json> --green-evidence <green.json>"
867
+ ),
868
+ },
869
+ "evidenceFile": {
870
+ "schemaVersion": 2,
871
+ "kind": ["red", "green"],
872
+ "status": {"red": "FAIL", "green": "OK"},
873
+ "provenance": {
874
+ "type": "managed-run-session",
875
+ "note": "指向 run-start 产出的 runtime/run-sessions/<sessionId>/session.json",
876
+ },
877
+ },
878
+ }
879
+
880
+
881
+ _EVIDENCE_ERROR_HINTS = {
882
+ "FIXBACK_EVIDENCE_MISSING": (
883
+ "路径未找到证据文件。它应当是 run-start 产出的托管会话证据 JSON"
884
+ "(schemaVersion=2,kind=red|green,provenance.type=managed-run-session);"
885
+ "先用 run-start 采集会话,再据此写证据文件。"
886
+ ),
887
+ "FIXBACK_EVIDENCE_UNREGISTERED": (
888
+ "证据文件存在但未注册,或注册后内容已变。"
889
+ "先执行 register-evidence 注册(内容改动后需重新注册),再引用。"
890
+ ),
891
+ "FIXBACK_EVIDENCE_INVALID": (
892
+ "证据文件不是合法的 schemaVersion=2 托管会话证据。"
893
+ "检查 kind / status / provenance 三个字段。"
894
+ ),
895
+ "FIXBACK_EVIDENCE_OUTSIDE_PROJECT": (
896
+ "证据路径落在项目之外。证据必须放在变更目录或项目内,便于随归档留痕。"
897
+ ),
898
+ }
899
+
900
+
901
+ def evidence_error_message(code: str, raw_path: str) -> str:
902
+ """把只回显路径的错误码,补成能直接照做的一句话。"""
903
+ hint = _EVIDENCE_ERROR_HINTS.get(code)
904
+ base = f"{code}: {raw_path}"
905
+ return base if hint is None else f"{base} — {hint}"
906
+
907
+
811
908
  def register_evidence(change_dir: Path, raw_path: str) -> dict[str, Any]:
812
909
  """Register evidence only after validating its authoritative provenance."""
813
910
  path = _resolve_evidence_path(change_dir, raw_path)
@@ -968,7 +1065,9 @@ def _evidence_record(
968
1065
  or ledger_record.get("productIdentity") != evidence_product
969
1066
  or ledger_record.get("passedGates") != sorted(set(passed_gates))
970
1067
  ):
971
- raise ValueError(f"FIXBACK_EVIDENCE_UNREGISTERED: {raw_path}")
1068
+ raise ValueError(
1069
+ evidence_error_message("FIXBACK_EVIDENCE_UNREGISTERED", raw_path)
1070
+ )
972
1071
  if product_identity is not None and evidence_product != product_identity:
973
1072
  raise ValueError(
974
1073
  "FIXBACK_EVIDENCE_IDENTITY_MISMATCH: "
@@ -0,0 +1,262 @@
1
+ #!/usr/bin/env python3
2
+ """Knowledge candidate generation from an archived change's summary-data.json.
3
+
4
+ The archive workflow has already filtered once: review findings were produced by
5
+ an independent reviewer and then adjudicated, and knownRisks are evidence-derived
6
+ facts. This module turns those two — and only those two — into KnowledgeCandidate
7
+ records for the archive package's ``candidates/knowledge.json``.
8
+
9
+ Mapping is fixed by docs/superpowers/specs/2026-08-18-three-views-data-flow-design.md
10
+ ("知识来源的选定")::
11
+
12
+ disposition = FIXED -> pitfall RED 0.95 / YELLOW 0.85
13
+ disposition = ACCEPTED_RISK | DEFERRED -> risk RED 0.95 / YELLOW 0.85
14
+ knownRisks[] -> risk 0.85
15
+ severity = OK | disposition = NOT_APPLICABLE -> dropped
16
+
17
+ Dispositions outside the adopted set (OPEN / UNKNOWN) are dropped too: an
18
+ unadjudicated finding is not yet knowledge. maintenanceNotes, finalStatusReasons
19
+ and manualActions are deliberately excluded — the spec evaluated each and found
20
+ them too noisy or empty to be worth persisting.
21
+
22
+ No LLM is involved. Every emitted field is copied or derived from a real
23
+ summary-data field, so the output is reproducible and free of invention.
24
+ """
25
+ from __future__ import annotations
26
+
27
+ import hashlib
28
+ import json
29
+ from typing import Any
30
+
31
+ SCHEMA_VERSION = 1
32
+ PRODUCER = "harness-archive"
33
+
34
+ # Only these severities carry knowledge; OK is explicitly dropped by the spec.
35
+ _SEVERITIES = {"RED", "YELLOW"}
36
+ # Adjudicated dispositions the spec adopts, mapped to the knowledge entry type.
37
+ _DISPOSITION_ENTRY_TYPES = {
38
+ "FIXED": "pitfall",
39
+ "ACCEPTED_RISK": "risk",
40
+ "DEFERRED": "risk",
41
+ }
42
+ _SEVERITY_CONFIDENCE = {"RED": 0.95, "YELLOW": 0.85}
43
+ _KNOWN_RISK_CONFIDENCE = 0.85
44
+
45
+ _MAX_KEYWORDS = 32
46
+ _MAX_KEYWORD_CHARS = 80
47
+ _MAX_BODY_CHARS = 20_000
48
+
49
+
50
+ def _text(value: Any) -> str:
51
+ return value.strip() if isinstance(value, str) else ""
52
+
53
+
54
+ def _digest(*parts: str) -> str:
55
+ return hashlib.sha256("\0".join(parts).encode("utf-8")).hexdigest()
56
+
57
+
58
+ def _candidate_id(change_key: str, kind: str, identity: str) -> str:
59
+ return f"kc_{_digest(change_key, kind, identity)[:32]}"
60
+
61
+
62
+ def _content_hash(entry_type: str, summary: str, body: str, keywords: list[str]) -> str:
63
+ canonical = json.dumps(
64
+ {"entry_type": entry_type, "summary": summary, "body": body, "keywords": keywords},
65
+ ensure_ascii=False,
66
+ sort_keys=True,
67
+ separators=(",", ":"),
68
+ )
69
+ return "sha256:" + hashlib.sha256(canonical.encode("utf-8")).hexdigest()
70
+
71
+
72
+ def _keywords(*values: str) -> list[str]:
73
+ """Deduplicate, preserve order, and honour the contract's bounds."""
74
+ seen: list[str] = []
75
+ for value in values:
76
+ keyword = _text(value)[:_MAX_KEYWORD_CHARS]
77
+ if keyword and keyword not in seen:
78
+ seen.append(keyword)
79
+ return seen[:_MAX_KEYWORDS]
80
+
81
+
82
+ def _path_segments(path: str) -> list[str]:
83
+ return [segment for segment in path.replace("\\", "/").split("/") if segment]
84
+
85
+
86
+ def _location(path: str, line: Any) -> str:
87
+ """``path:line`` when the line number is real, otherwise just the path."""
88
+ if not path:
89
+ return ""
90
+ if isinstance(line, bool) or not isinstance(line, int) or line < 1:
91
+ return path
92
+ return f"{path}:{line}"
93
+
94
+
95
+ def _finding_candidate(
96
+ finding: dict[str, Any],
97
+ *,
98
+ change_key: str,
99
+ archive_id: str,
100
+ producer_version: str,
101
+ created_at: str,
102
+ ) -> dict[str, Any] | None:
103
+ severity = _text(finding.get("severity"))
104
+ disposition = _text(finding.get("disposition"))
105
+ entry_type = _DISPOSITION_ENTRY_TYPES.get(disposition)
106
+ title = _text(finding.get("title"))
107
+ if severity not in _SEVERITIES or entry_type is None or not title:
108
+ return None
109
+
110
+ path = _text(finding.get("path"))
111
+ line = finding.get("line")
112
+ location = _location(path, line)
113
+ segments = _path_segments(path)
114
+ finding_id = _text(finding.get("id"))
115
+
116
+ body_lines = [title]
117
+ if location:
118
+ body_lines.append(f"位置:{location}")
119
+ body_lines.append(f"严重度:{severity}")
120
+ body_lines.append(f"裁决:{disposition}")
121
+ body = "\n".join(body_lines)[:_MAX_BODY_CHARS]
122
+
123
+ keywords = _keywords(
124
+ segments[-1] if segments else "",
125
+ segments[-2] if len(segments) >= 2 else "",
126
+ severity,
127
+ disposition,
128
+ )
129
+ source_ref = f"archive:{archive_id}#{finding_id}" if finding_id else f"archive:{archive_id}"
130
+ if path and location != path:
131
+ source_refs = [f"{path}#L{line}"]
132
+ elif path:
133
+ source_refs = [path]
134
+ else:
135
+ source_refs = [f"archive:{archive_id}"]
136
+
137
+ return {
138
+ "schema_version": SCHEMA_VERSION,
139
+ "candidate_id": _candidate_id(
140
+ change_key, "review", finding_id or f"{title}\0{path}\0{line}"
141
+ ),
142
+ "source_change_key": change_key,
143
+ "source_refs": source_refs,
144
+ "summary": title,
145
+ "reusability_scope": segments[0] if segments else "project",
146
+ "content_hash": _content_hash(entry_type, title, body, keywords),
147
+ "confidence": _SEVERITY_CONFIDENCE[severity],
148
+ "status": "pending",
149
+ "entry_type": entry_type,
150
+ "body": body,
151
+ "keywords": keywords,
152
+ "provenance": {
153
+ "source_kind": "review",
154
+ "source_ref": source_ref,
155
+ "producer": PRODUCER,
156
+ "producer_version": producer_version,
157
+ "created_at": created_at,
158
+ },
159
+ }
160
+
161
+
162
+ def _risk_candidate(
163
+ risk: dict[str, Any],
164
+ *,
165
+ change_key: str,
166
+ archive_id: str,
167
+ producer_version: str,
168
+ created_at: str,
169
+ ) -> dict[str, Any] | None:
170
+ message = _text(risk.get("message"))
171
+ if not message:
172
+ return None
173
+ phase = _text(risk.get("phase"))
174
+ severity = _text(risk.get("severity"))
175
+
176
+ body_lines = [message]
177
+ if phase:
178
+ body_lines.append(f"阶段:{phase}")
179
+ if severity:
180
+ body_lines.append(f"严重度:{severity}")
181
+ body = "\n".join(body_lines)[:_MAX_BODY_CHARS]
182
+ keywords = _keywords(phase, severity)
183
+
184
+ return {
185
+ "schema_version": SCHEMA_VERSION,
186
+ "candidate_id": _candidate_id(change_key, "known_risk", f"{phase}\0{message}"),
187
+ "source_change_key": change_key,
188
+ "source_refs": [f"archive:{archive_id}"],
189
+ "summary": message,
190
+ "reusability_scope": phase or "project",
191
+ "content_hash": _content_hash("risk", message, body, keywords),
192
+ "confidence": _KNOWN_RISK_CONFIDENCE,
193
+ "status": "pending",
194
+ "entry_type": "risk",
195
+ "body": body,
196
+ "keywords": keywords,
197
+ "provenance": {
198
+ "source_kind": "archive",
199
+ "source_ref": f"archive:{archive_id}",
200
+ "producer": PRODUCER,
201
+ "producer_version": producer_version,
202
+ "created_at": created_at,
203
+ },
204
+ }
205
+
206
+
207
+ def build_knowledge_candidates(
208
+ summary: dict[str, Any],
209
+ *,
210
+ change_key: str,
211
+ archive_id: str,
212
+ producer_version: str,
213
+ created_at: str,
214
+ ) -> list[dict[str, Any]]:
215
+ """Project reviewFindings + knownRisks into KnowledgeCandidate records.
216
+
217
+ Returns [] for missing or malformed input: an archive with nothing worth
218
+ persisting must still produce a valid (empty) candidates file.
219
+ """
220
+ if not isinstance(summary, dict):
221
+ return []
222
+ candidates: list[dict[str, Any]] = []
223
+ seen_ids: set[str] = set()
224
+
225
+ def collect(candidate: dict[str, Any] | None) -> None:
226
+ if candidate is None or candidate["candidate_id"] in seen_ids:
227
+ return
228
+ seen_ids.add(candidate["candidate_id"])
229
+ candidates.append(candidate)
230
+
231
+ findings = summary.get("reviewFindings")
232
+ if isinstance(findings, list):
233
+ for finding in findings:
234
+ if isinstance(finding, dict):
235
+ collect(_finding_candidate(
236
+ finding,
237
+ change_key=change_key,
238
+ archive_id=archive_id,
239
+ producer_version=producer_version,
240
+ created_at=created_at,
241
+ ))
242
+
243
+ risks = summary.get("knownRisks")
244
+ if isinstance(risks, list):
245
+ for risk in risks:
246
+ if isinstance(risk, dict):
247
+ collect(_risk_candidate(
248
+ risk,
249
+ change_key=change_key,
250
+ archive_id=archive_id,
251
+ producer_version=producer_version,
252
+ created_at=created_at,
253
+ ))
254
+
255
+ return candidates
256
+
257
+
258
+ def render_knowledge_candidates_json(candidates: list[dict[str, Any]]) -> str:
259
+ """Deterministic bytes for the archive package entry."""
260
+ return json.dumps(
261
+ candidates, ensure_ascii=False, sort_keys=True, separators=(",", ":")
262
+ ) + "\n"
@@ -444,6 +444,8 @@ def infer_execution_project_root(change_dir: Path) -> Path | None:
444
444
  process = subprocess.run(
445
445
  ["git", "-C", str(root), "rev-parse", "--path-format=absolute", "--git-common-dir"],
446
446
  text=True,
447
+ encoding="utf-8",
448
+ errors="replace",
447
449
  stdout=subprocess.PIPE,
448
450
  stderr=subprocess.PIPE,
449
451
  check=False,
@@ -461,6 +463,8 @@ def infer_execution_project_root(change_dir: Path) -> Path | None:
461
463
  process = subprocess.run(
462
464
  ["git", "-C", str(candidate), "rev-parse", "--show-toplevel"],
463
465
  text=True,
466
+ encoding="utf-8",
467
+ errors="replace",
464
468
  stdout=subprocess.PIPE,
465
469
  stderr=subprocess.PIPE,
466
470
  check=False,
@@ -1578,7 +1578,7 @@ def _run_session_worker_with_log(state_root: Path, session_id: str) -> int:
1578
1578
  return _run_session_worker(state_root, session_id)
1579
1579
 
1580
1580
 
1581
- def run_session_status(state_root: Path, session_id: str) -> dict[str, Any]:
1581
+ def _run_session_status_raw(state_root: Path, session_id: str) -> dict[str, Any]:
1582
1582
  from harness_service import is_pid_alive
1583
1583
 
1584
1584
  receipt = _load_run_receipt(state_root, session_id)
@@ -1651,6 +1651,65 @@ def run_session_status(state_root: Path, session_id: str) -> dict[str, Any]:
1651
1651
  return receipt
1652
1652
 
1653
1653
 
1654
+ # 终态判定此前只活在脚本内部的 RUN_TERMINAL_STATUSES 里,从不出现在返回体中。
1655
+ # 调用方于是只能按状态名猜——而 "INCOMPLETE" 读起来最像"还没结束",实际是终态。
1656
+ # 2026-08-18 的一次 fixback 执行里,一个启动即失败(LAUNCHER_FAILED)的会话
1657
+ # 被连等 20s、60s,纯属这个缺口造成的浪费。
1658
+ _TERMINAL_HINTS = {
1659
+ "LAUNCHER_FAILED": (
1660
+ "被测进程未能启动(testProcessStarted=false),不是超时;"
1661
+ "先核对 argv 的可执行文件在该工作目录下能否直接运行,再重跑 run-start。"
1662
+ ),
1663
+ "HEARTBEAT_LOST": "worker 心跳丢失,会话已判定结束;日志可能不完整。",
1664
+ "WORKER_IDENTITY_MISMATCH": "worker 身份校验不通过,会话已终止且未采信其结果。",
1665
+ "WORKER_EXITED_WITHOUT_FINAL_RECEIPT": "worker 未写最终回执即退出。",
1666
+ }
1667
+
1668
+
1669
+ def _annotate_run_status(receipt: dict[str, Any]) -> dict[str, Any]:
1670
+ """给回执补上可判定的终态标记与可行动线索。
1671
+
1672
+ FINALIZING 刻意判为非终态:结果虽已确定,但 worker 尚未退出,
1673
+ 调用方此时取读数会与清理竞争。
1674
+ """
1675
+ status = receipt.get("status")
1676
+ terminal = status in RUN_TERMINAL_STATUSES and status != "FINALIZING"
1677
+ annotated = dict(receipt)
1678
+ annotated["terminal"] = bool(terminal)
1679
+ hint = _TERMINAL_HINTS.get(str(receipt.get("reasonCode") or ""))
1680
+ if terminal and hint is not None:
1681
+ annotated["terminalHint"] = hint
1682
+ return annotated
1683
+
1684
+
1685
+ def run_session_status(state_root: Path, session_id: str) -> dict[str, Any]:
1686
+ return _annotate_run_status(_run_session_status_raw(state_root, session_id))
1687
+
1688
+
1689
+ def await_run_session(
1690
+ state_root: Path,
1691
+ session_id: str,
1692
+ *,
1693
+ timeout_seconds: float = 600.0,
1694
+ poll_seconds: float = 2.0,
1695
+ ) -> dict[str, Any]:
1696
+ """阻塞到会话进入终态,或超时后带 waitTimedOut 标记返回。
1697
+
1698
+ 没有这个入口时,调用方只能 `sleep <猜一个时长>` 再查一次;上述执行日志里
1699
+ 连猜了 5s / 20s / 60s / 100s 四轮。等待逻辑属于会话语义,应由这里承担。
1700
+ """
1701
+ deadline = time.monotonic() + max(float(timeout_seconds), 0.0)
1702
+ while True:
1703
+ current = run_session_status(state_root, session_id)
1704
+ if current.get("terminal") is True:
1705
+ current["waitTimedOut"] = False
1706
+ return current
1707
+ if time.monotonic() >= deadline:
1708
+ current["waitTimedOut"] = True
1709
+ return current
1710
+ time.sleep(max(float(poll_seconds), 0.01))
1711
+
1712
+
1654
1713
  def read_run_session_log(
1655
1714
  state_root: Path,
1656
1715
  session_id: str,
@@ -1974,6 +2033,13 @@ def build_parser() -> argparse.ArgumentParser:
1974
2033
  p_status = sub.add_parser("run-status", help="read a managed run receipt")
1975
2034
  p_status.add_argument("--state-root", required=True)
1976
2035
  p_status.add_argument("--session-id", required=True)
2036
+ p_status.add_argument(
2037
+ "--wait",
2038
+ action="store_true",
2039
+ help="阻塞到会话进入终态再返回,替代调用方自己 sleep 猜时长",
2040
+ )
2041
+ p_status.add_argument("--wait-timeout-seconds", type=float, default=600.0)
2042
+ p_status.add_argument("--poll-seconds", type=float, default=2.0)
1977
2043
  p_status.add_argument("--json", action="store_true")
1978
2044
  p_log = sub.add_parser("run-log", help="read an incremental log page")
1979
2045
  p_log.add_argument("--state-root", required=True)
@@ -2022,7 +2088,15 @@ def main(argv: list[str] | None = None) -> int:
2022
2088
  resource_locks=list(args.resource_lock),
2023
2089
  )
2024
2090
  elif args.command == "run-status":
2025
- result = run_session_status(Path(args.state_root), args.session_id)
2091
+ if getattr(args, "wait", False):
2092
+ result = await_run_session(
2093
+ Path(args.state_root),
2094
+ args.session_id,
2095
+ timeout_seconds=args.wait_timeout_seconds,
2096
+ poll_seconds=args.poll_seconds,
2097
+ )
2098
+ else:
2099
+ result = run_session_status(Path(args.state_root), args.session_id)
2026
2100
  elif args.command == "run-log":
2027
2101
  result = read_run_session_log(
2028
2102
  Path(args.state_root),
@@ -2,5 +2,5 @@
2
2
  "schemaVersion": 1,
3
3
  "overlay": "java",
4
4
  "agent": "codebuddy",
5
- "coreHash": "d8613ac610bcccc8"
5
+ "coreHash": "4e58f505d91f5969"
6
6
  }
@@ -3,7 +3,7 @@ name: harness-apidoc
3
3
  description: 从 Java 后端代码生成结构化前端 API 文档(Controller/VO/ErrorCode)。仅当用户显式调用
4
4
  /harness-apidoc 时使用,不得自动触发。
5
5
  ---
6
- <!-- generated by harness_deploy.py; core=d8613ac610bcccc8; overlay=java; agent=codebuddy; do not edit -->
6
+ <!-- generated by harness_deploy.py; core=4e58f505d91f5969; overlay=java; agent=codebuddy; do not edit -->
7
7
  # harness-apidoc — 前端接口文档(Java overlay)
8
8
 
9
9
  ## Purpose