@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
@@ -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": "claude-code",
5
- "coreHash": "d8613ac610bcccc8"
5
+ "coreHash": "4e58f505d91f5969"
6
6
  }
@@ -21,7 +21,7 @@ disallowed-tools:
21
21
  - Bash(awk *)
22
22
  - Bash(curl *)
23
23
  ---
24
- <!-- generated by harness_deploy.py; core=d8613ac610bcccc8; overlay=java; agent=claude-code; do not edit -->
24
+ <!-- generated by harness_deploy.py; core=4e58f505d91f5969; overlay=java; agent=claude-code; do not edit -->
25
25
  # harness-apidoc — 前端接口文档(Java overlay)
26
26
 
27
27
  ## Purpose
@@ -21,7 +21,7 @@ disallowed-tools:
21
21
  - Bash(awk *)
22
22
  - Bash(curl *)
23
23
  ---
24
- <!-- generated by harness_deploy.py; core=d8613ac610bcccc8; overlay=java; agent=claude-code; do not edit -->
24
+ <!-- generated by harness_deploy.py; core=4e58f505d91f5969; overlay=java; agent=claude-code; do not edit -->
25
25
  # harness-archive — 变更归档
26
26
 
27
27
  ## Purpose
@@ -221,11 +221,13 @@ git 命令通过 `powershell.exe -Command "..."` 执行;archive-meta.md 和 su
221
221
  | 上传 | 载什么 | 结果在哪 |
222
222
  |------|--------|---------|
223
223
  | `steps.archive_push` | 归档 ZIP(核心产物) | `archiveId`/`archiveStatus`/`uploadStatus`/`knowledgeStatus`,同时落盘 `state/local/archive-packages/<cn>.remote.json` |
224
- | `steps.managed_snapshot_push` | **归档交付物(每个变更目录的 `plans/`、`spec/`、`reports/`、`docs/`)、`.harness/codebase` 架构地图、`.harness/rules`、`.harness/project.yaml`、指令入口** | `ok`/`reasonCode`/`exitCode`/`cliCode`/`detail` |
224
+ | `steps.managed_snapshot_push` | **归档交付物(每个变更目录的 `plans/`、`spec/`、`docs/` 全部,`reports/` 只取 `final/` 定稿)、`.harness/codebase` 架构地图、`.harness/rules`、`.harness/project.yaml`、指令入口** | `ok`/`reasonCode`/`exitCode`/`cliCode`/`detail` |
225
225
 
226
226
  走 `harness-push`(remote-sync,产生分支快照)而非 legacy `push`(proposal 管道)。
227
227
  归档目录下的 `runtime/`、`meta/`、`evidence/`、`logs/`、`fixback/`、`.publication-staging/`
228
- 等过程文件不上传;交付物目录里的 `*.log`、`*.tmp`、凭证与 `.env*` 也仍被安全规则拦下。
228
+ 等过程文件不上传;`reports/review/` `reports/test/` 同样是过程产物,不上传——
229
+ 这条边界与归档 ZIP 的包内容边界一致。交付物目录里的 `*.log`、`*.tmp`、凭证与 `.env*`
230
+ 也仍被安全规则拦下。
229
231
 
230
232
  ```
231
233
  远端归档:arc_3ff325bb…(archiveStatus=durable,uploadStatus=ready,knowledgeStatus=ready,8 文件)
@@ -20,7 +20,7 @@ disallowed-tools:
20
20
  - Bash(awk *)
21
21
  - Bash(curl *)
22
22
  ---
23
- <!-- generated by harness_deploy.py; core=d8613ac610bcccc8; overlay=java; agent=claude-code; do not edit -->
23
+ <!-- generated by harness_deploy.py; core=4e58f505d91f5969; overlay=java; agent=claude-code; do not edit -->
24
24
  # harness-codebase-map — 代码库地图
25
25
 
26
26
  ## Purpose
@@ -8,7 +8,7 @@ disallowed-tools:
8
8
  - Bash(git *)
9
9
  - Bash(curl *)
10
10
  ---
11
- <!-- generated by harness_deploy.py; core=d8613ac610bcccc8; overlay=java; agent=claude-code; do not edit -->
11
+ <!-- generated by harness_deploy.py; core=4e58f505d91f5969; overlay=java; agent=claude-code; do not edit -->
12
12
  # harness-knowledge-ingest
13
13
 
14
14
  知识 ingest 完全由 Hunter Platform 负责。归档完成时,客户端生成一个确定性 ZIP;
@@ -8,7 +8,7 @@ disallowed-tools:
8
8
  - Bash(git *)
9
9
  - Bash(curl *)
10
10
  ---
11
- <!-- generated by harness_deploy.py; core=d8613ac610bcccc8; overlay=java; agent=claude-code; do not edit -->
11
+ <!-- generated by harness_deploy.py; core=4e58f505d91f5969; overlay=java; agent=claude-code; do not edit -->
12
12
  # harness-knowledge-query
13
13
 
14
14
  项目知识以 Hunter Platform 的服务端索引为唯一真源。客户端只提交查询并消费结果:
@@ -21,7 +21,7 @@ disallowed-tools:
21
21
  - Bash(awk *)
22
22
  - Bash(curl *)
23
23
  ---
24
- <!-- generated by harness_deploy.py; core=d8613ac610bcccc8; overlay=java; agent=claude-code; do not edit -->
24
+ <!-- generated by harness_deploy.py; core=4e58f505d91f5969; overlay=java; agent=claude-code; do not edit -->
25
25
  # harness-package — 增量模块打包(Java overlay)
26
26
 
27
27
  ## Purpose
@@ -21,7 +21,7 @@ disallowed-tools:
21
21
  - Bash(awk *)
22
22
  - Bash(curl *)
23
23
  ---
24
- <!-- generated by harness_deploy.py; core=d8613ac610bcccc8; overlay=java; agent=claude-code; do not edit -->
24
+ <!-- generated by harness_deploy.py; core=4e58f505d91f5969; overlay=java; agent=claude-code; do not edit -->
25
25
  # harness-plan — 需求规划
26
26
 
27
27
  ## Purpose
@@ -21,7 +21,7 @@ disallowed-tools:
21
21
  - Bash(awk *)
22
22
  - Bash(curl *)
23
23
  ---
24
- <!-- generated by harness_deploy.py; core=d8613ac610bcccc8; overlay=java; agent=claude-code; do not edit -->
24
+ <!-- generated by harness_deploy.py; core=4e58f505d91f5969; overlay=java; agent=claude-code; do not edit -->
25
25
  # harness-pull — 从 Hunter Platform 下拉与恢复
26
26
 
27
27
  ## Purpose
@@ -21,7 +21,7 @@ disallowed-tools:
21
21
  - Bash(awk *)
22
22
  - Bash(curl *)
23
23
  ---
24
- <!-- generated by harness_deploy.py; core=d8613ac610bcccc8; overlay=java; agent=claude-code; do not edit -->
24
+ <!-- generated by harness_deploy.py; core=4e58f505d91f5969; overlay=java; agent=claude-code; do not edit -->
25
25
  # harness-push — 上传到 Hunter Platform
26
26
 
27
27
  ## Purpose
@@ -22,7 +22,7 @@ disallowed-tools:
22
22
  - Bash(curl *)
23
23
  - Bash(codegraph *)
24
24
  ---
25
- <!-- generated by harness_deploy.py; core=d8613ac610bcccc8; overlay=java; agent=claude-code; do not edit -->
25
+ <!-- generated by harness_deploy.py; core=4e58f505d91f5969; overlay=java; agent=claude-code; do not edit -->
26
26
  # harness-review — 代码审查
27
27
 
28
28
  ## Purpose
@@ -22,7 +22,7 @@ disallowed-tools:
22
22
  - Bash(curl *)
23
23
  - Bash(codegraph *)
24
24
  ---
25
- <!-- generated by harness_deploy.py; core=d8613ac610bcccc8; overlay=java; agent=claude-code; do not edit -->
25
+ <!-- generated by harness_deploy.py; core=4e58f505d91f5969; overlay=java; agent=claude-code; do not edit -->
26
26
  # harness-run — 需求编码
27
27
 
28
28
  ## Purpose
@@ -21,7 +21,7 @@ disallowed-tools:
21
21
  - Bash(awk *)
22
22
  - Bash(curl *)
23
23
  ---
24
- <!-- generated by harness_deploy.py; core=d8613ac610bcccc8; overlay=java; agent=claude-code; do not edit -->
24
+ <!-- generated by harness_deploy.py; core=4e58f505d91f5969; overlay=java; agent=claude-code; do not edit -->
25
25
  # harness-submit — 最终提交(含 worktree 合并)
26
26
 
27
27
  ## Purpose
@@ -19,7 +19,7 @@ disallowed-tools:
19
19
  - Bash(awk *)
20
20
  - Bash(curl *)
21
21
  ---
22
- <!-- generated by harness_deploy.py; core=d8613ac610bcccc8; overlay=java; agent=claude-code; do not edit -->
22
+ <!-- generated by harness_deploy.py; core=4e58f505d91f5969; overlay=java; agent=claude-code; do not edit -->
23
23
  # harness-sync
24
24
 
25
25
  ## Purpose
@@ -23,7 +23,7 @@ disallowed-tools:
23
23
  - Bash(node *)
24
24
  - Bash(codegraph *)
25
25
  ---
26
- <!-- generated by harness_deploy.py; core=d8613ac610bcccc8; overlay=java; agent=claude-code; do not edit -->
26
+ <!-- generated by harness_deploy.py; core=4e58f505d91f5969; overlay=java; agent=claude-code; do not edit -->
27
27
  # harness-test — 测试执行
28
28
 
29
29
  ## Purpose
@@ -196,6 +196,26 @@ Windows 默认常为 Windows PowerShell **5.1**。下列特性在 5.1 不可用
196
196
  - 非 ASCII 路径(含中文)仓库:git/构建/文件操作一律 PowerShell;Bash 被 hook 拒绝(如 `Denied: non-ASCII path`)后**立即**改用 PowerShell 重发等价命令,禁止原样重试 Bash。
197
197
  - Maven `-D` 参数在 PowerShell 中加引号:`"-Dmaven.test.skip=true"`。
198
198
 
199
+ ### 捕获子进程中文输出:`$env:PYTHONUTF8=1` 不够
200
+
201
+ `PYTHONUTF8=1` 只让 **Python 按 UTF-8 输出**,不改变 **PowerShell 按什么编码读**。
202
+ 控制台代码页是 GBK(936) 时,PowerShell 会用 GBK 解码 UTF-8 字节,得到形如
203
+ `绫?javadoc 鏈�闅� 6 绾х簿绠€` 的乱码——内容已在捕获阶段损坏,落进报告、回执与
204
+ JSON 字段后无法还原。
205
+
206
+ 调用任何会输出中文的子进程前,**必须同时设置输出编码**:
207
+
208
+ ```powershell
209
+ [Console]::OutputEncoding = [Text.Encoding]::UTF8
210
+ $env:PYTHONUTF8 = 1
211
+ python harness/scripts/harness_fixback.py <子命令> --json
212
+ ```
213
+
214
+ - 两行缺一不可:只设 `PYTHONUTF8` 会乱码,只设 `OutputEncoding` 则 Python 可能仍按本地编码输出。
215
+ - 写脚本时把这两行放在脚本开头,不要依赖调用方记得加。
216
+ - 少用 `2>&1` 合并流:PowerShell 5.1 会把 stderr 包装成 `NativeCommandError`,
217
+ 与编码问题叠加后更难判断真实失败原因;需要 stderr 时分开捕获。
218
+
199
219
  ## 13. PS5/PS7 原生参数合同
200
220
 
201
221
  复杂 JSON、Docker Go template、here-string 内容及多层引号不得作为 native process 的内联参数跨 `Bash → powershell -Command → native executable` 传递。统一写 UTF-8 JSON 参数文件: