@hunter-harness/workflow-harness 0.2.37 → 0.2.39

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 (194) 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 +10 -7
  3. package/harness/bundles/general/claude-code/harness-archive/templates/render-summary.mjs +24 -4
  4. package/harness/bundles/general/claude-code/harness-codebase-map/SKILL.md +1 -1
  5. package/harness/bundles/general/claude-code/harness-knowledge-ingest/SKILL.md +1 -1
  6. package/harness/bundles/general/claude-code/harness-knowledge-ingest/scripts/harness_knowledge.py +283 -28
  7. package/harness/bundles/general/claude-code/harness-knowledge-query/SKILL.md +1 -1
  8. package/harness/bundles/general/claude-code/harness-plan/SKILL.md +1 -1
  9. package/harness/bundles/general/claude-code/harness-review/SKILL.md +2 -2
  10. package/harness/bundles/general/claude-code/harness-review/reference.md +20 -5
  11. package/harness/bundles/general/claude-code/harness-run/SKILL.md +1 -1
  12. package/harness/bundles/general/claude-code/harness-submit/SKILL.md +1 -1
  13. package/harness/bundles/general/claude-code/harness-sync/SKILL.md +1 -1
  14. package/harness/bundles/general/claude-code/harness-test/SKILL.md +1 -1
  15. package/harness/bundles/general/claude-code/scripts/harness_archive.py +446 -20
  16. package/harness/bundles/general/claude-code/scripts/harness_events.py +273 -21
  17. package/harness/bundles/general/claude-code/scripts/harness_gate.py +56 -32
  18. package/harness/bundles/general/claude-code/scripts/harness_integration.py +197 -3
  19. package/harness/bundles/general/claude-code/scripts/harness_ledger.py +126 -2
  20. package/harness/bundles/general/claude-code/scripts/harness_report_model.py +28 -6
  21. package/harness/bundles/general/claude-code/scripts/harness_review.py +118 -17
  22. package/harness/bundles/general/claude-code/scripts/harness_service.py +219 -0
  23. package/harness/bundles/general/codebuddy/.harness-build.json +1 -1
  24. package/harness/bundles/general/codebuddy/harness-archive/SKILL.md +10 -7
  25. package/harness/bundles/general/codebuddy/harness-archive/templates/render-summary.mjs +24 -4
  26. package/harness/bundles/general/codebuddy/harness-codebase-map/SKILL.md +1 -1
  27. package/harness/bundles/general/codebuddy/harness-knowledge-ingest/SKILL.md +1 -1
  28. package/harness/bundles/general/codebuddy/harness-knowledge-ingest/scripts/harness_knowledge.py +283 -28
  29. package/harness/bundles/general/codebuddy/harness-knowledge-query/SKILL.md +1 -1
  30. package/harness/bundles/general/codebuddy/harness-plan/SKILL.md +1 -1
  31. package/harness/bundles/general/codebuddy/harness-review/SKILL.md +2 -2
  32. package/harness/bundles/general/codebuddy/harness-review/reference.md +20 -5
  33. package/harness/bundles/general/codebuddy/harness-run/SKILL.md +1 -1
  34. package/harness/bundles/general/codebuddy/harness-submit/SKILL.md +1 -1
  35. package/harness/bundles/general/codebuddy/harness-sync/SKILL.md +1 -1
  36. package/harness/bundles/general/codebuddy/harness-test/SKILL.md +1 -1
  37. package/harness/bundles/general/codebuddy/scripts/harness_archive.py +446 -20
  38. package/harness/bundles/general/codebuddy/scripts/harness_events.py +273 -21
  39. package/harness/bundles/general/codebuddy/scripts/harness_gate.py +56 -32
  40. package/harness/bundles/general/codebuddy/scripts/harness_integration.py +197 -3
  41. package/harness/bundles/general/codebuddy/scripts/harness_ledger.py +126 -2
  42. package/harness/bundles/general/codebuddy/scripts/harness_report_model.py +28 -6
  43. package/harness/bundles/general/codebuddy/scripts/harness_review.py +118 -17
  44. package/harness/bundles/general/codebuddy/scripts/harness_service.py +219 -0
  45. package/harness/bundles/general/codex/.harness-build.json +1 -1
  46. package/harness/bundles/general/codex/harness-archive/SKILL.md +10 -7
  47. package/harness/bundles/general/codex/harness-archive/templates/render-summary.mjs +24 -4
  48. package/harness/bundles/general/codex/harness-codebase-map/SKILL.md +1 -1
  49. package/harness/bundles/general/codex/harness-knowledge-ingest/SKILL.md +1 -1
  50. package/harness/bundles/general/codex/harness-knowledge-ingest/scripts/harness_knowledge.py +283 -28
  51. package/harness/bundles/general/codex/harness-knowledge-query/SKILL.md +1 -1
  52. package/harness/bundles/general/codex/harness-plan/SKILL.md +1 -1
  53. package/harness/bundles/general/codex/harness-review/SKILL.md +2 -2
  54. package/harness/bundles/general/codex/harness-review/reference.md +20 -5
  55. package/harness/bundles/general/codex/harness-run/SKILL.md +1 -1
  56. package/harness/bundles/general/codex/harness-submit/SKILL.md +1 -1
  57. package/harness/bundles/general/codex/harness-sync/SKILL.md +1 -1
  58. package/harness/bundles/general/codex/harness-test/SKILL.md +1 -1
  59. package/harness/bundles/general/codex/scripts/harness_archive.py +446 -20
  60. package/harness/bundles/general/codex/scripts/harness_events.py +273 -21
  61. package/harness/bundles/general/codex/scripts/harness_gate.py +56 -32
  62. package/harness/bundles/general/codex/scripts/harness_integration.py +197 -3
  63. package/harness/bundles/general/codex/scripts/harness_ledger.py +126 -2
  64. package/harness/bundles/general/codex/scripts/harness_report_model.py +28 -6
  65. package/harness/bundles/general/codex/scripts/harness_review.py +118 -17
  66. package/harness/bundles/general/codex/scripts/harness_service.py +219 -0
  67. package/harness/bundles/general/cursor/.harness-build.json +1 -1
  68. package/harness/bundles/general/cursor/harness-archive/SKILL.md +10 -7
  69. package/harness/bundles/general/cursor/harness-archive/templates/render-summary.mjs +24 -4
  70. package/harness/bundles/general/cursor/harness-codebase-map/SKILL.md +1 -1
  71. package/harness/bundles/general/cursor/harness-knowledge-ingest/SKILL.md +1 -1
  72. package/harness/bundles/general/cursor/harness-knowledge-ingest/scripts/harness_knowledge.py +283 -28
  73. package/harness/bundles/general/cursor/harness-knowledge-query/SKILL.md +1 -1
  74. package/harness/bundles/general/cursor/harness-plan/SKILL.md +1 -1
  75. package/harness/bundles/general/cursor/harness-review/SKILL.md +2 -2
  76. package/harness/bundles/general/cursor/harness-review/reference.md +20 -5
  77. package/harness/bundles/general/cursor/harness-run/SKILL.md +1 -1
  78. package/harness/bundles/general/cursor/harness-submit/SKILL.md +1 -1
  79. package/harness/bundles/general/cursor/harness-sync/SKILL.md +1 -1
  80. package/harness/bundles/general/cursor/harness-test/SKILL.md +1 -1
  81. package/harness/bundles/general/cursor/scripts/harness_archive.py +446 -20
  82. package/harness/bundles/general/cursor/scripts/harness_events.py +273 -21
  83. package/harness/bundles/general/cursor/scripts/harness_gate.py +56 -32
  84. package/harness/bundles/general/cursor/scripts/harness_integration.py +197 -3
  85. package/harness/bundles/general/cursor/scripts/harness_ledger.py +126 -2
  86. package/harness/bundles/general/cursor/scripts/harness_report_model.py +28 -6
  87. package/harness/bundles/general/cursor/scripts/harness_review.py +118 -17
  88. package/harness/bundles/general/cursor/scripts/harness_service.py +219 -0
  89. package/harness/bundles/java/claude-code/.harness-build.json +1 -1
  90. package/harness/bundles/java/claude-code/harness-apidoc/SKILL.md +1 -1
  91. package/harness/bundles/java/claude-code/harness-archive/SKILL.md +10 -7
  92. package/harness/bundles/java/claude-code/harness-archive/templates/render-summary.mjs +24 -4
  93. package/harness/bundles/java/claude-code/harness-codebase-map/SKILL.md +1 -1
  94. package/harness/bundles/java/claude-code/harness-knowledge-ingest/SKILL.md +1 -1
  95. package/harness/bundles/java/claude-code/harness-knowledge-ingest/scripts/harness_knowledge.py +283 -28
  96. package/harness/bundles/java/claude-code/harness-knowledge-query/SKILL.md +1 -1
  97. package/harness/bundles/java/claude-code/harness-package/SKILL.md +1 -1
  98. package/harness/bundles/java/claude-code/harness-plan/SKILL.md +1 -1
  99. package/harness/bundles/java/claude-code/harness-review/SKILL.md +2 -2
  100. package/harness/bundles/java/claude-code/harness-review/reference.md +20 -5
  101. package/harness/bundles/java/claude-code/harness-run/SKILL.md +1 -1
  102. package/harness/bundles/java/claude-code/harness-submit/SKILL.md +1 -1
  103. package/harness/bundles/java/claude-code/harness-sync/SKILL.md +1 -1
  104. package/harness/bundles/java/claude-code/harness-test/SKILL.md +1 -1
  105. package/harness/bundles/java/claude-code/scripts/harness_archive.py +446 -20
  106. package/harness/bundles/java/claude-code/scripts/harness_events.py +273 -21
  107. package/harness/bundles/java/claude-code/scripts/harness_gate.py +56 -32
  108. package/harness/bundles/java/claude-code/scripts/harness_integration.py +197 -3
  109. package/harness/bundles/java/claude-code/scripts/harness_ledger.py +126 -2
  110. package/harness/bundles/java/claude-code/scripts/harness_report_model.py +28 -6
  111. package/harness/bundles/java/claude-code/scripts/harness_review.py +118 -17
  112. package/harness/bundles/java/claude-code/scripts/harness_service.py +219 -0
  113. package/harness/bundles/java/codebuddy/.harness-build.json +1 -1
  114. package/harness/bundles/java/codebuddy/harness-apidoc/SKILL.md +1 -1
  115. package/harness/bundles/java/codebuddy/harness-archive/SKILL.md +10 -7
  116. package/harness/bundles/java/codebuddy/harness-archive/templates/render-summary.mjs +24 -4
  117. package/harness/bundles/java/codebuddy/harness-codebase-map/SKILL.md +1 -1
  118. package/harness/bundles/java/codebuddy/harness-knowledge-ingest/SKILL.md +1 -1
  119. package/harness/bundles/java/codebuddy/harness-knowledge-ingest/scripts/harness_knowledge.py +283 -28
  120. package/harness/bundles/java/codebuddy/harness-knowledge-query/SKILL.md +1 -1
  121. package/harness/bundles/java/codebuddy/harness-package/SKILL.md +1 -1
  122. package/harness/bundles/java/codebuddy/harness-plan/SKILL.md +1 -1
  123. package/harness/bundles/java/codebuddy/harness-review/SKILL.md +2 -2
  124. package/harness/bundles/java/codebuddy/harness-review/reference.md +20 -5
  125. package/harness/bundles/java/codebuddy/harness-run/SKILL.md +1 -1
  126. package/harness/bundles/java/codebuddy/harness-submit/SKILL.md +1 -1
  127. package/harness/bundles/java/codebuddy/harness-sync/SKILL.md +1 -1
  128. package/harness/bundles/java/codebuddy/harness-test/SKILL.md +1 -1
  129. package/harness/bundles/java/codebuddy/scripts/harness_archive.py +446 -20
  130. package/harness/bundles/java/codebuddy/scripts/harness_events.py +273 -21
  131. package/harness/bundles/java/codebuddy/scripts/harness_gate.py +56 -32
  132. package/harness/bundles/java/codebuddy/scripts/harness_integration.py +197 -3
  133. package/harness/bundles/java/codebuddy/scripts/harness_ledger.py +126 -2
  134. package/harness/bundles/java/codebuddy/scripts/harness_report_model.py +28 -6
  135. package/harness/bundles/java/codebuddy/scripts/harness_review.py +118 -17
  136. package/harness/bundles/java/codebuddy/scripts/harness_service.py +219 -0
  137. package/harness/bundles/java/codex/.harness-build.json +1 -1
  138. package/harness/bundles/java/codex/harness-apidoc/SKILL.md +1 -1
  139. package/harness/bundles/java/codex/harness-archive/SKILL.md +10 -7
  140. package/harness/bundles/java/codex/harness-archive/templates/render-summary.mjs +24 -4
  141. package/harness/bundles/java/codex/harness-codebase-map/SKILL.md +1 -1
  142. package/harness/bundles/java/codex/harness-knowledge-ingest/SKILL.md +1 -1
  143. package/harness/bundles/java/codex/harness-knowledge-ingest/scripts/harness_knowledge.py +283 -28
  144. package/harness/bundles/java/codex/harness-knowledge-query/SKILL.md +1 -1
  145. package/harness/bundles/java/codex/harness-package/SKILL.md +1 -1
  146. package/harness/bundles/java/codex/harness-plan/SKILL.md +1 -1
  147. package/harness/bundles/java/codex/harness-review/SKILL.md +2 -2
  148. package/harness/bundles/java/codex/harness-review/reference.md +20 -5
  149. package/harness/bundles/java/codex/harness-run/SKILL.md +1 -1
  150. package/harness/bundles/java/codex/harness-submit/SKILL.md +1 -1
  151. package/harness/bundles/java/codex/harness-sync/SKILL.md +1 -1
  152. package/harness/bundles/java/codex/harness-test/SKILL.md +1 -1
  153. package/harness/bundles/java/codex/scripts/harness_archive.py +446 -20
  154. package/harness/bundles/java/codex/scripts/harness_events.py +273 -21
  155. package/harness/bundles/java/codex/scripts/harness_gate.py +56 -32
  156. package/harness/bundles/java/codex/scripts/harness_integration.py +197 -3
  157. package/harness/bundles/java/codex/scripts/harness_ledger.py +126 -2
  158. package/harness/bundles/java/codex/scripts/harness_report_model.py +28 -6
  159. package/harness/bundles/java/codex/scripts/harness_review.py +118 -17
  160. package/harness/bundles/java/codex/scripts/harness_service.py +219 -0
  161. package/harness/bundles/java/cursor/.harness-build.json +1 -1
  162. package/harness/bundles/java/cursor/harness-apidoc/SKILL.md +1 -1
  163. package/harness/bundles/java/cursor/harness-archive/SKILL.md +10 -7
  164. package/harness/bundles/java/cursor/harness-archive/templates/render-summary.mjs +24 -4
  165. package/harness/bundles/java/cursor/harness-codebase-map/SKILL.md +1 -1
  166. package/harness/bundles/java/cursor/harness-knowledge-ingest/SKILL.md +1 -1
  167. package/harness/bundles/java/cursor/harness-knowledge-ingest/scripts/harness_knowledge.py +283 -28
  168. package/harness/bundles/java/cursor/harness-knowledge-query/SKILL.md +1 -1
  169. package/harness/bundles/java/cursor/harness-package/SKILL.md +1 -1
  170. package/harness/bundles/java/cursor/harness-plan/SKILL.md +1 -1
  171. package/harness/bundles/java/cursor/harness-review/SKILL.md +2 -2
  172. package/harness/bundles/java/cursor/harness-review/reference.md +20 -5
  173. package/harness/bundles/java/cursor/harness-run/SKILL.md +1 -1
  174. package/harness/bundles/java/cursor/harness-submit/SKILL.md +1 -1
  175. package/harness/bundles/java/cursor/harness-sync/SKILL.md +1 -1
  176. package/harness/bundles/java/cursor/harness-test/SKILL.md +1 -1
  177. package/harness/bundles/java/cursor/scripts/harness_archive.py +446 -20
  178. package/harness/bundles/java/cursor/scripts/harness_events.py +273 -21
  179. package/harness/bundles/java/cursor/scripts/harness_gate.py +56 -32
  180. package/harness/bundles/java/cursor/scripts/harness_integration.py +197 -3
  181. package/harness/bundles/java/cursor/scripts/harness_ledger.py +126 -2
  182. package/harness/bundles/java/cursor/scripts/harness_report_model.py +28 -6
  183. package/harness/bundles/java/cursor/scripts/harness_review.py +118 -17
  184. package/harness/bundles/java/cursor/scripts/harness_service.py +219 -0
  185. package/harness/manifests/general/claude-code.json +24 -24
  186. package/harness/manifests/general/codebuddy.json +24 -24
  187. package/harness/manifests/general/codex.json +24 -24
  188. package/harness/manifests/general/cursor.json +24 -24
  189. package/harness/manifests/java/claude-code.json +26 -26
  190. package/harness/manifests/java/codebuddy.json +26 -26
  191. package/harness/manifests/java/codex.json +26 -26
  192. package/harness/manifests/java/cursor.json +26 -26
  193. package/hunter-workflow-family.json +4 -4
  194. package/package.json +1 -1
@@ -2,5 +2,5 @@
2
2
  "schemaVersion": 1,
3
3
  "overlay": "none",
4
4
  "agent": "claude-code",
5
- "coreHash": "af97a6533498c853"
5
+ "coreHash": "6a2c743a1f8ceb0b"
6
6
  }
@@ -20,7 +20,7 @@ disallowed-tools:
20
20
  - Bash(awk *)
21
21
  - Bash(curl *)
22
22
  ---
23
- <!-- generated by harness_deploy.py; core=af97a6533498c853; overlay=none; agent=claude-code; do not edit -->
23
+ <!-- generated by harness_deploy.py; core=6a2c743a1f8ceb0b; overlay=none; agent=claude-code; do not edit -->
24
24
  # harness-archive — 变更归档
25
25
 
26
26
  ## Purpose
@@ -43,7 +43,7 @@ disallowed-tools:
43
43
  自动调用边界:
44
44
 
45
45
  - 归档涉及移动/删除原变更目录,支持被其他 skill 调用,但调用前必须确保前置条件已满足;也支持用户显式 `/harness-archive` 调用。
46
- - 归档前确认是强制阻断检查点,用户拒绝 终止流程,不执行任何操作。
46
+ - 默认归档前确认是阻断检查点。若 `harness_archive.py auto-gate` 返回 `ARCHIVE_AUTO_GATE_SATISFIED`(Submit 或 Merge 已终态,archive-boundary snapshot 存在且全部归档门禁满足),后续 Archive 可无 AskQuestion 自动运行;其他情况仍须确认。
47
47
 
48
48
  ## 前置条件
49
49
 
@@ -106,9 +106,12 @@ disallowed-tools:
106
106
  - **Read `checklist.md`** — 归档前检查项
107
107
  - 发现多个未归档变更 → 让用户选择或终止
108
108
 
109
- ### Phase 2:确认归档 ⚠️ 强制阻断
109
+ ### Phase 2:确认归档 / 自动门禁
110
110
 
111
- blocking user confirmation 让用户确认归档操作。**用户拒绝 终止流程,不执行任何操作。**
111
+ 先运行 `harness_archive.py auto-gate --change-dir ".harness/changes/<change-name>" --intent <intent> --json`。
112
+
113
+ - `autoArchiveAllowed=true`:记录 gate receipt,直接进入 Phase 3;无需 AskQuestion。
114
+ - 否则:blocking user confirmation 让用户确认归档操作。**用户拒绝 → 终止流程,不执行任何操作。**
112
115
 
113
116
  - **Read `reference.md`** — 确认对话框的内容格式
114
117
 
@@ -143,9 +146,9 @@ blocking user confirmation 让用户确认归档操作。**用户拒绝 → 终
143
146
 
144
147
  扫描排除 `.harness/archive/`;多个未归档变更 → 让用户选择或终止,不批量归档。
145
148
 
146
- ### 二、归档前确认是强制阻断检查点
149
+ ### 二、归档前确认或自动门禁
147
150
 
148
- blocking user confirmation 确认;用户拒绝 → 终止,不执行任何操作。
151
+ `auto-gate` 只有在 Submit/Merge 终态、`meta/state-snapshot.json` 含 archive-boundary base、且 `status.archivable=true` 时才允许无确认执行。未满足时 blocking user confirmation;用户拒绝 → 终止,不执行任何操作。
149
152
 
150
153
  ### 三、文件移动只用内置工具或 PowerShell
151
154
 
@@ -218,7 +221,7 @@ git 命令通过 `powershell.exe -Command "..."` 执行;archive-meta.md 和 fi
218
221
 
219
222
  ## 交互白名单
220
223
 
221
- **仅允许**归档确认(Phase 2 blocking user confirmation);拒绝 → 终止,不执行任何移动。
224
+ 仅当 `auto-gate` 未满足时,允许归档确认(Phase 2 blocking user confirmation);拒绝 → 终止,不执行任何移动。
222
225
 
223
226
  ## 执行日志
224
227
 
@@ -69,7 +69,7 @@ const risks = list(current.findings).length
69
69
  const statusTone = (raw) => {
70
70
  const status = String(raw || "UNKNOWN").toUpperCase();
71
71
  if (/FAIL|ERROR|BLOCKED/.test(status)) return "danger";
72
- if (/WARN|CONDITIONAL|PARTIAL|NOT_RUN|UNKNOWN|SKIP/.test(status)) return "warning";
72
+ if (/WARN|CONDITIONAL|PARTIAL|NOT_RUN|UNKNOWN|SKIP|EVIDENCE_MISSING/.test(status)) return "warning";
73
73
  if (/ADVISORY|REUSED|NOT_APPLICABLE/.test(status)) return "neutral";
74
74
  return "success";
75
75
  };
@@ -77,7 +77,7 @@ const STATUS_LABELS = {
77
77
  OK: "通过", PASS: "通过", PASSED: "通过", CONDITIONAL_OK: "有条件通过",
78
78
  WARN: "警告", ADVISORY: "建议", FAIL: "失败", FAILED: "失败", ERROR: "错误",
79
79
  BLOCKED: "阻塞", NOT_RUN: "未运行", SKIPPED: "已跳过",
80
- NOT_APPLICABLE: "不适用", UNKNOWN: "未知"
80
+ NOT_APPLICABLE: "不适用", UNKNOWN: "未知", EVIDENCE_MISSING: "证据缺失"
81
81
  };
82
82
  const pill = (raw) => {
83
83
  const status = String(raw || "UNKNOWN").toUpperCase();
@@ -115,7 +115,17 @@ const verificationStatus = (value) => {
115
115
  };
116
116
  const verificationNote = (key, raw) => {
117
117
  const value = record(raw);
118
- if (key === "unitTests") return `${value.passRate || `${value.run ?? 0} 项`} · 失败 ${value.failures ?? 0}`;
118
+ if (key === "unitTests") {
119
+ const unique = numeric(value.uniqueTestCount);
120
+ const uniqueLabel = unique === null ? "唯一用例未记录" : `唯一 ${unique} 项`;
121
+ return `${value.passRate || `${value.run ?? 0} 次执行`} · ${uniqueLabel} · 重跑 ${value.rerunCount ?? 0}`;
122
+ }
123
+ if (key === "dbCompatibility") {
124
+ const evidence = record(verification.dbCompatibilityEvidence);
125
+ return evidence.status === "EVIDENCE_MISSING"
126
+ ? "缺少可验证的类型化账本/收据"
127
+ : evidence.reason || evidence.evidenceHash || "类型化兼容性证据";
128
+ }
119
129
  if (key === "apiTests") return `${value.passRate || `${value.passed ?? 0}/${value.total ?? 0}`} · 阻塞 ${value.blocked ?? 0}`;
120
130
  if (key === "browserE2E") return `${value.passed ?? 0}/${value.total ?? 0} 通过 · ${value.failed ?? 0} 失败 · 重试 ${value.retries ?? 0}`;
121
131
  if (value.checks !== undefined) return `${value.checks} 项检查`;
@@ -135,6 +145,8 @@ const groups = [
135
145
  const statuses = values.map(verificationStatus);
136
146
  const groupStatus = statuses.some((status) => /FAIL|ERROR|BLOCKED/.test(String(status)))
137
147
  ? "FAIL"
148
+ : statuses.some((status) => /EVIDENCE_MISSING/.test(String(status)))
149
+ ? "EVIDENCE_MISSING"
138
150
  : statuses.some((status) => /WARN|CONDITIONAL|PARTIAL/.test(String(status)))
139
151
  ? "WARN"
140
152
  : statuses.length && statuses.every((status) => /NOT_APPLICABLE/.test(String(status)))
@@ -147,6 +159,13 @@ const groups = [
147
159
  });
148
160
  const passedGroups = groups.filter((group) => group.status === "OK").length;
149
161
  const productCommit = identity.productCommit || data.productCommit || data.finalCommit;
162
+ const identityChain = [
163
+ ["检查点", identity.checkpointCommit || data.checkpointCommit],
164
+ ["产品", productCommit],
165
+ ["功能尖端", identity.featureTip || data.featureTip],
166
+ ["合并", identity.mergeCommit || data.mergeCommit || identity.featureMergeHash],
167
+ ["发布尖端", identity.releaseTip || data.releaseTip || identity.releaseTipHash]
168
+ ].filter(([, value]) => value).map(([label, value]) => `${label} ${shortHash(value)}`).join(" → ") || "N/A";
150
169
  const wallClock = timing.workflowWallClockMs ?? record(data.durations).totalMs;
151
170
 
152
171
  const groupHtml = groups.map((group) => `
@@ -201,7 +220,7 @@ const html = `<!doctype html>
201
220
  ${recordOnly ? '<div class="record-only">归档意图:仅记录 · 未请求发布</div>' : ""}
202
221
  </section>
203
222
  <section class="metrics">
204
- <article class="metric"><small>产品提交</small><strong><code title="${esc(productCommit || "N/A")}">${esc(shortHash(productCommit))}</code></strong></article>
223
+ <article class="metric"><small>产品提交链</small><strong><code title="${esc(identityChain)}">${esc(identityChain)}</code></strong></article>
205
224
  <article class="metric"><small>验证概览</small><strong>${passedGroups}/${groups.length} 组通过</strong><small>按后端 / Geo / 前端 / 浏览器 / API</small></article>
206
225
  <article class="metric"><small>风险与动作</small><strong>${risks.length} / ${actions.length}</strong><small>当前风险 / 人工动作</small></article>
207
226
  <article class="metric"><small>全流程耗时</small><strong>${esc(duration(wallClock))}</strong><small>活动 ${esc(duration(timing.stageActiveExecutionMs))}</small></article>
@@ -234,6 +253,7 @@ const html = `<!doctype html>
234
253
  <details><summary>命令证据(${commands.length})</summary><div class="table-wrap"><table><thead><tr><th>阶段</th><th>命令</th><th>结果</th></tr></thead><tbody>${commandHtml}</tbody></table></div></details>
235
254
  <details><summary>技术元数据</summary><div><dl>
236
255
  <dt>产品提交</dt><dd><code>${esc(productCommit || "N/A")}</code></dd>
256
+ <dt>检查点 → 产品 → 功能尖端 → 合并 → 发布尖端</dt><dd><code>${esc(identityChain)}</code></dd>
237
257
  <dt>产品树哈希</dt><dd><code>${esc(identity.productTreeHash || data.productTreeHash || "N/A")}</code></dd>
238
258
  <dt>环境哈希</dt><dd><code>${esc(identity.environmentHash || data.environmentHash || "N/A")}</code></dd>
239
259
  <dt>基线提交</dt><dd><code>${esc(identity.baseCommit || data.baseCommit || "N/A")}</code></dd>
@@ -20,7 +20,7 @@ disallowed-tools:
20
20
  - Bash(awk *)
21
21
  - Bash(curl *)
22
22
  ---
23
- <!-- generated by harness_deploy.py; core=af97a6533498c853; overlay=none; agent=claude-code; do not edit -->
23
+ <!-- generated by harness_deploy.py; core=6a2c743a1f8ceb0b; overlay=none; agent=claude-code; do not edit -->
24
24
  # harness-codebase-map — 代码库地图
25
25
 
26
26
  ## Purpose
@@ -20,7 +20,7 @@ disallowed-tools:
20
20
  - Bash(awk *)
21
21
  - Bash(curl *)
22
22
  ---
23
- <!-- generated by harness_deploy.py; core=af97a6533498c853; overlay=none; agent=claude-code; do not edit -->
23
+ <!-- generated by harness_deploy.py; core=6a2c743a1f8ceb0b; overlay=none; agent=claude-code; do not edit -->
24
24
  # harness-knowledge-ingest
25
25
 
26
26
  从 `.harness/archive/**/reports/final/summary-data.json` 抽取项目知识,生成并维护本地 `.harness/knowledge/` 索引。
@@ -2654,6 +2654,55 @@ def build_snapshot(project: Path) -> KnowledgeSnapshot:
2654
2654
  )
2655
2655
 
2656
2656
 
2657
+ def archive_delta(
2658
+ archive_records: list[dict[str, Any]],
2659
+ previous_index: dict[str, Any] | None,
2660
+ ) -> dict[str, set[str]]:
2661
+ """Classify archive summaries without treating the whole corpus as dirty."""
2662
+ previous_items = (
2663
+ previous_index.get("archives", {}).get("items", [])
2664
+ if isinstance(previous_index, dict)
2665
+ else []
2666
+ )
2667
+ previous = {
2668
+ str(item.get("summaryData")): str(item.get("summarySha256") or "")
2669
+ for item in previous_items
2670
+ if isinstance(item, dict) and item.get("summaryData")
2671
+ }
2672
+ current = {
2673
+ str(item.get("summaryData")): str(item.get("summarySha256") or "")
2674
+ for item in archive_records
2675
+ if item.get("summaryData")
2676
+ }
2677
+ added = set(current) - set(previous)
2678
+ changed = {
2679
+ path for path in set(current) & set(previous)
2680
+ if current[path] != previous[path]
2681
+ }
2682
+ return {
2683
+ "added": added,
2684
+ "changed": changed,
2685
+ "removed": set(previous) - set(current),
2686
+ "dirty": added | changed,
2687
+ }
2688
+
2689
+
2690
+ def restore_unchanged_entry_state(
2691
+ entry: dict[str, Any],
2692
+ previous_entries: dict[str, dict[str, Any]],
2693
+ *,
2694
+ is_dirty: bool,
2695
+ ) -> None:
2696
+ """Keep historical lifecycle decisions out of a warm ingest hot path."""
2697
+ if is_dirty:
2698
+ return
2699
+ previous = previous_entries.get(str(entry.get("id") or ""))
2700
+ if previous is None:
2701
+ return
2702
+ entry["status"] = previous.get("status", entry.get("status"))
2703
+ entry["lifecycle"] = json_clone(previous.get("lifecycle", entry.get("lifecycle", {})))
2704
+
2705
+
2657
2706
  def build_index(
2658
2707
  project: Path,
2659
2708
  incremental: bool = True,
@@ -2684,19 +2733,21 @@ def build_index(
2684
2733
  ensure_knowledge_dirs(knowledge)
2685
2734
  reporter.emit("snapshot", len(summary_paths), len(summary_paths), force=True)
2686
2735
 
2736
+ old_index: dict[str, Any] | None = None
2737
+ index_path = knowledge / "index.json"
2738
+ if index_path.exists():
2739
+ try:
2740
+ old_index = read_json(index_path)
2741
+ except (OSError, json.JSONDecodeError):
2742
+ old_index = None
2743
+ delta = archive_delta(archive_records, old_index)
2744
+
2687
2745
  # No-op fast path: inputs (archive checksums + config + schema) are unchanged.
2688
2746
  # Write nothing — entries, sqlite, index and views all stay byte-identical,
2689
2747
  # so a repeated ingest is a true no-op (design §3.5, cluster 6, UT-025).
2690
2748
  # Also require index.sqlite to exist so a query never sees a stale index.json
2691
2749
  # pointing at a missing sqlite (API-009 single ensure-current).
2692
2750
  if incremental:
2693
- old_index: dict[str, Any] | None = None
2694
- index_path = knowledge / "index.json"
2695
- if index_path.exists():
2696
- try:
2697
- old_index = read_json(index_path)
2698
- except (OSError, json.JSONDecodeError):
2699
- old_index = None
2700
2751
  if (
2701
2752
  isinstance(old_index, dict)
2702
2753
  and old_index.get("inputsHash") == inputs_hash
@@ -2716,6 +2767,10 @@ def build_index(
2716
2767
  "sqliteRebuild": 0,
2717
2768
  "sqliteUpsert": 0,
2718
2769
  "sqliteDelete": 0,
2770
+ "archivesAdded": 0,
2771
+ "archivesChanged": 0,
2772
+ "archivesRemoved": 0,
2773
+ "archivesDelta": 0,
2719
2774
  }
2720
2775
  )
2721
2776
  result = dict(old_index)
@@ -2751,7 +2806,17 @@ def build_index(
2751
2806
  "sqliteDelete": 0,
2752
2807
  "summaryHashesReused": 0,
2753
2808
  "summaryHashesComputed": 0,
2809
+ "archivesAdded": len(delta["added"]),
2810
+ "archivesChanged": len(delta["changed"]),
2811
+ "archivesRemoved": len(delta["removed"]),
2812
+ "archivesDelta": len(delta["dirty"]) + len(delta["removed"]),
2813
+ }
2814
+ previous_entries = {
2815
+ str(entry.get("id")): entry
2816
+ for _, entry in load_entry_files(knowledge)
2817
+ if entry.get("id")
2754
2818
  }
2819
+ dirty_entry_ids: set[str] = set()
2755
2820
 
2756
2821
  for summary_index, summary_path in enumerate(summary_paths, start=1):
2757
2822
  summary_rel = rel_to_project(project, summary_path)
@@ -2790,6 +2855,15 @@ def build_index(
2790
2855
  archive_entries = rebind_cached_archive_entries(
2791
2856
  project, summary_path, summary_hash, archive_entries
2792
2857
  )
2858
+ is_dirty = summary_rel in delta["dirty"] or not incremental
2859
+ for entry in archive_entries:
2860
+ restore_unchanged_entry_state(
2861
+ entry,
2862
+ previous_entries,
2863
+ is_dirty=is_dirty,
2864
+ )
2865
+ if is_dirty and entry.get("id"):
2866
+ dirty_entry_ids.add(str(entry["id"]))
2793
2867
  # API-006/RET-40: entries from archives failing the publication
2794
2868
  # gate stay quarantined (candidate only, never active/promoted).
2795
2869
  archive_rel = rel_to_project(project, archive_dir_from_summary(summary_path))
@@ -2833,9 +2907,24 @@ def build_index(
2833
2907
  seen.add(fp)
2834
2908
  deduped.append(entry)
2835
2909
 
2836
- ingest_mode.update(apply_batch_git_freshness(project, deduped))
2910
+ freshness_entries = [
2911
+ entry
2912
+ for entry in deduped
2913
+ if (
2914
+ str(entry.get("id") or "") in dirty_entry_ids
2915
+ or (
2916
+ entry.get("status") in {"active", "candidate"}
2917
+ and str(entry.get("id") or "") not in previous_entries
2918
+ )
2919
+ )
2920
+ ]
2921
+ ingest_mode.update(apply_batch_git_freshness(project, freshness_entries))
2837
2922
  reporter.emit("near-deduplicate", 0, len(deduped), force=True)
2838
- near_dedupe = dedupe_near_duplicates(deduped, progress=reporter)
2923
+ near_dedupe = dedupe_near_duplicates(
2924
+ deduped,
2925
+ progress=reporter,
2926
+ dirty_entry_ids=dirty_entry_ids if incremental else None,
2927
+ )
2839
2928
  ingest_mode["nearDuplicatesMerged"] = near_dedupe["merged"]
2840
2929
  ingest_mode["similarityCandidates"] = near_dedupe["similarityCandidates"]
2841
2930
  ingest_mode["exactSimilarityComparisons"] = near_dedupe["exactSimilarityComparisons"]
@@ -2920,7 +3009,22 @@ def build_index(
2920
3009
  )
2921
3010
 
2922
3011
  reporter.emit("sqlite", 0, len(indexed_entries), force=True)
2923
- sqlite_stats = write_sqlite(knowledge / "index.sqlite", indexed_entries)
3012
+ sqlite_dirty_entries = [
3013
+ entry
3014
+ for entry in indexed_entries
3015
+ if (
3016
+ str(entry.get("id") or "") in dirty_entry_ids
3017
+ or (
3018
+ entry.get("status") in {"active", "candidate"}
3019
+ and str(entry.get("id") or "") not in previous_entries
3020
+ )
3021
+ )
3022
+ ]
3023
+ sqlite_stats = write_sqlite(
3024
+ knowledge / "index.sqlite",
3025
+ indexed_entries,
3026
+ dirty_entries=sqlite_dirty_entries if incremental else None,
3027
+ )
2924
3028
  ingest_mode.update(sqlite_stats)
2925
3029
  ingest_mode["sqliteRowsTouched"] = (
2926
3030
  sqlite_stats.get("sqliteUpsert", 0) + sqlite_stats.get("sqliteDelete", 0)
@@ -3288,13 +3392,19 @@ def assert_persistence_invariants(
3288
3392
  )
3289
3393
 
3290
3394
 
3291
- def write_sqlite(path: Path, entries: list[dict[str, Any]]) -> dict[str, Any]:
3395
+ def write_sqlite(
3396
+ path: Path,
3397
+ entries: list[dict[str, Any]],
3398
+ *,
3399
+ dirty_entries: list[dict[str, Any]] | None = None,
3400
+ ) -> dict[str, Any]:
3292
3401
  """Persist entries to SQLite using a transactional dirty-set.
3293
3402
 
3294
- Reads existing rows, then upserts only changed entries and deletes removed
3295
- ones within a single transaction. When the schema version changes or the
3296
- table is missing, performs a full rebuild. If nothing changed the file is
3297
- not touched (cluster 6, design §3.5 true incremental).
3403
+ The normal path compares existing row payloads. Incremental callers may
3404
+ provide the known dirty entries instead, avoiding a full JSON digest of
3405
+ unchanged historical rows. Both paths delete rows absent from the current
3406
+ closure in one transaction. When the schema version changes or the table
3407
+ is missing, performs a full rebuild.
3298
3408
  """
3299
3409
  path.parent.mkdir(parents=True, exist_ok=True)
3300
3410
  stats: dict[str, Any] = {
@@ -3315,18 +3425,52 @@ def write_sqlite(path: Path, entries: list[dict[str, Any]]) -> dict[str, Any]:
3315
3425
  need_rebuild = (not table_exists) or existing_version != SQLITE_SCHEMA_VERSION
3316
3426
 
3317
3427
  existing_map: dict[str, str] = {}
3318
- if not need_rebuild:
3428
+ existing_status: dict[str, str] = {}
3429
+ dirty_mode = dirty_entries is not None and not need_rebuild
3430
+ if not need_rebuild and not dirty_mode:
3319
3431
  rows = con.execute("select id, entry_json from entries").fetchall()
3320
3432
  existing_map = {str(row[0]): str(row[1]) for row in rows}
3433
+ elif dirty_mode:
3434
+ rows = con.execute("select id, status from entries").fetchall()
3435
+ existing_map = {str(row[0]): "" for row in rows}
3436
+ existing_status = {str(row[0]): str(row[1] or "") for row in rows}
3321
3437
 
3322
3438
  new_map: dict[str, str] = {}
3323
- for entry in entries:
3439
+ entries_to_serialize = list(dirty_entries or []) if dirty_mode else list(entries)
3440
+ if dirty_mode:
3441
+ # HH-KNOW-20260730-002: archive-delta dirty sets miss lifecycle-only
3442
+ # transitions (promote/demote/conflict/supersede) on unchanged
3443
+ # archives. Reconcile those by status without digesting every row.
3444
+ seen_serialize_ids = {
3445
+ str(entry.get("id") or "")
3446
+ for entry in entries_to_serialize
3447
+ if isinstance(entry, dict) and entry.get("id")
3448
+ }
3449
+ for entry in entries:
3450
+ if not isinstance(entry, dict) or not entry.get("id"):
3451
+ continue
3452
+ eid = str(entry["id"])
3453
+ if eid in seen_serialize_ids:
3454
+ continue
3455
+ if existing_status.get(eid) != str(entry.get("status") or ""):
3456
+ entries_to_serialize.append(entry)
3457
+ seen_serialize_ids.add(eid)
3458
+ for entry in entries_to_serialize:
3324
3459
  if isinstance(entry, dict) and entry.get("id"):
3325
3460
  new_map[str(entry["id"])] = json.dumps(entry, ensure_ascii=False)
3326
3461
 
3327
- to_upsert = [eid for eid in new_map if new_map[eid] != existing_map.get(eid)]
3328
- to_delete = [eid for eid in existing_map if eid not in new_map]
3329
- stats["sqliteUnchanged"] = len(new_map) - len(to_upsert)
3462
+ current_ids = {
3463
+ str(entry["id"])
3464
+ for entry in entries
3465
+ if isinstance(entry, dict) and entry.get("id")
3466
+ }
3467
+ to_upsert = (
3468
+ list(new_map)
3469
+ if dirty_mode
3470
+ else [eid for eid in new_map if new_map[eid] != existing_map.get(eid)]
3471
+ )
3472
+ to_delete = [eid for eid in existing_map if eid not in current_ids]
3473
+ stats["sqliteUnchanged"] = len(entries) - len(to_upsert)
3330
3474
 
3331
3475
  if not need_rebuild and not to_upsert and not to_delete:
3332
3476
  # nothing to do; leave the file (and its mtime) untouched
@@ -3372,6 +3516,11 @@ def write_sqlite(path: Path, entries: list[dict[str, Any]]) -> dict[str, Any]:
3372
3516
  con.execute(f"pragma user_version = {SQLITE_SCHEMA_VERSION}")
3373
3517
  stats["sqliteRebuild"] = 1
3374
3518
  # full rebuild => every entry must be (re)inserted
3519
+ new_map = {
3520
+ str(entry["id"]): json.dumps(entry, ensure_ascii=False)
3521
+ for entry in entries
3522
+ if isinstance(entry, dict) and entry.get("id")
3523
+ }
3375
3524
  to_upsert = list(new_map.keys())
3376
3525
  to_delete = []
3377
3526
 
@@ -4121,6 +4270,62 @@ def sync_status(
4121
4270
  action = "ingested"
4122
4271
  reasons = []
4123
4272
 
4273
+ # HH-KNOW-20260730-001: an up-to-date index is not the whole story --
4274
+ # maintenance-outbox items enqueued by `harness archive` still need to be
4275
+ # drained (auto-supersede/reverify-stale/judge-export) before the project
4276
+ # is truly caught up. Drain here whenever the index is current: either it
4277
+ # was just refreshed above, or it was already current on entry. When the
4278
+ # caller did not request an update and the index is stale, skip the drain
4279
+ # -- maintain_knowledge() would otherwise trigger an implicit rebuild via
4280
+ # its own build_index(incremental=True) call, contradicting "don't force
4281
+ # rebuild" for a plain status check.
4282
+ index_current = not reasons
4283
+ outbox_root = _outbox_root(project)
4284
+ maintenance: dict[str, Any]
4285
+ next_action: str | None = None
4286
+ maintain_hint = (
4287
+ "python harness/harness-knowledge-ingest/scripts/harness_knowledge.py "
4288
+ "maintain --project <project> --drain"
4289
+ )
4290
+
4291
+ if index_current:
4292
+ drain_result = drain_maintenance_outbox(project)
4293
+ outbox_ok = bool(drain_result.get("ok")) and drain_result.get("remaining", 0) == 0
4294
+ maintenance = {
4295
+ "attempted": True,
4296
+ "skipped": False,
4297
+ "ok": outbox_ok,
4298
+ "processed": drain_result.get("processed", 0),
4299
+ "remaining": drain_result.get("remaining", 0),
4300
+ "staleRunningRecovered": drain_result.get("staleRunningRecovered", 0),
4301
+ "results": drain_result.get("results", []),
4302
+ }
4303
+ if not outbox_ok:
4304
+ next_action = (
4305
+ "Maintenance outbox is not fully drained. Retry `hunter-harness sync`, "
4306
+ f"or run `{maintain_hint}` to process the remaining items."
4307
+ )
4308
+ else:
4309
+ pending_count = len(list((outbox_root / "pending").glob("*.json"))) if outbox_root.is_dir() else 0
4310
+ failed_count = len(list((outbox_root / "failed").glob("*.json"))) if outbox_root.is_dir() else 0
4311
+ running_count = len(list((outbox_root / "running").glob("*.json"))) if outbox_root.is_dir() else 0
4312
+ outbox_ok = pending_count == 0 and failed_count == 0
4313
+ maintenance = {
4314
+ "attempted": False,
4315
+ "skipped": True,
4316
+ "skippedReason": "index out of date; rerun with --update to rebuild and drain",
4317
+ "ok": outbox_ok,
4318
+ "pending": pending_count,
4319
+ "failed": failed_count,
4320
+ "running": running_count,
4321
+ }
4322
+ next_action = (
4323
+ "Run `hunter-harness sync` (or sync --update) to rebuild the index; "
4324
+ "the maintenance outbox drain is skipped while the index is stale."
4325
+ )
4326
+
4327
+ ok = index_current and bool(maintenance.get("ok"))
4328
+
4124
4329
  result = {
4125
4330
  "project": str(project),
4126
4331
  "upToDate": not reasons,
@@ -4131,6 +4336,9 @@ def sync_status(
4131
4336
  "index": str(index_path),
4132
4337
  "sqlite": str(sqlite_path),
4133
4338
  },
4339
+ "maintenance": maintenance,
4340
+ "ok": ok,
4341
+ "nextAction": next_action,
4134
4342
  }
4135
4343
  if refreshed is not None:
4136
4344
  result["index"] = summarize_index(refreshed)
@@ -4522,16 +4730,33 @@ def dedupe_near_duplicates(
4522
4730
  threshold: float = NEAR_DUPLICATE_THRESHOLD,
4523
4731
  *,
4524
4732
  progress: ProgressReporter | None = None,
4733
+ dirty_entry_ids: set[str] | None = None,
4525
4734
  max_exact_comparisons: int = 50_000,
4526
4735
  time_budget_ms: int = 2_000,
4527
4736
  ) -> dict[str, Any]:
4528
- """Merge near duplicates through sparse buckets and an explicit work budget."""
4737
+ """Merge near duplicates through sparse buckets and an explicit work budget.
4738
+
4739
+ Full / offline path (``dirty_entry_ids is None``): keep historical semantics —
4740
+ every non-superseded status participates, and pairs stay scoped to the same
4741
+ ``(archive, type)`` group.
4742
+
4743
+ Online hot path (``dirty_entry_ids`` provided): exclude stale/superseded from
4744
+ the comparison corpus, keep archive scoping, and only emit pairs that touch
4745
+ at least one dirty entry (HH-KNOW-20260730-002).
4746
+ """
4529
4747
  started = time.monotonic()
4530
4748
  deadline = started + max(0, time_budget_ms) / 1_000
4531
4749
  merges: list[dict[str, Any]] = []
4532
- active_entries = [
4533
- entry for entry in entries if entry.get("status") != "superseded"
4534
- ]
4750
+ if dirty_entry_ids is None:
4751
+ active_entries = [
4752
+ entry for entry in entries if entry.get("status") != "superseded"
4753
+ ]
4754
+ else:
4755
+ active_entries = [
4756
+ entry
4757
+ for entry in entries
4758
+ if entry.get("status") in {"active", "candidate"}
4759
+ ]
4535
4760
  normalized = {id(entry): entry_compare_text(entry) for entry in active_entries}
4536
4761
  similarity_cache: dict[tuple[int, int], float] = {}
4537
4762
  similarity_candidates = 0
@@ -4540,6 +4765,8 @@ def dedupe_near_duplicates(
4540
4765
 
4541
4766
  grouped: dict[tuple[str, str], list[dict[str, Any]]] = {}
4542
4767
  for entry in active_entries:
4768
+ # Same-archive scoping is intentional: near-dup merges are intra-archive.
4769
+ # Cross-archive evolution/conflict is handled by supersede/review paths.
4543
4770
  key = (
4544
4771
  str(entry.get("source", {}).get("archive") or ""),
4545
4772
  str(entry.get("type") or ""),
@@ -4580,6 +4807,13 @@ def dedupe_near_duplicates(
4580
4807
  for right_identity in members[left_index + 1 :]:
4581
4808
  if left_identity == right_identity:
4582
4809
  continue
4810
+ if dirty_entry_ids is not None and not (
4811
+ str(entry_by_identity[left_identity].get("id") or "")
4812
+ in dirty_entry_ids
4813
+ or str(entry_by_identity[right_identity].get("id") or "")
4814
+ in dirty_entry_ids
4815
+ ):
4816
+ continue
4583
4817
  candidate_pairs.add(tuple(sorted((left_identity, right_identity))))
4584
4818
 
4585
4819
  def similarity(left: dict[str, Any], right: dict[str, Any]) -> float:
@@ -5451,15 +5685,35 @@ def maintain_knowledge(project: Path, archive_id: str) -> dict[str, Any]:
5451
5685
  }
5452
5686
 
5453
5687
 
5454
- def drain_maintenance_outbox(project: Path, limit: int = 50) -> dict[str, Any]:
5455
- """Advance all retryable maintenance items in one bounded invocation."""
5688
+ def drain_maintenance_outbox(
5689
+ project: Path, limit: int = 50, *, stale_running_seconds: float = 300.0
5690
+ ) -> dict[str, Any]:
5691
+ """Advance all retryable maintenance items in one bounded invocation.
5692
+
5693
+ Crash recovery: a `running` item left behind by a process that died
5694
+ mid-maintenance never returns to `pending`/`failed` on its own, so it
5695
+ would stall forever. Once a `running` item is older than
5696
+ `stale_running_seconds` it is treated as stuck and re-included here.
5697
+ `claim_outbox` returns a `running` item as-is (it only claims from
5698
+ pending/failed/pending-judge), and `maintain_knowledge` does NOT early
5699
+ return for `status == "running"` (only for `"completed"`) -- it falls
5700
+ through to the try block and retries the maintenance steps in place, so
5701
+ simply including the stale id in the drain batch is sufficient recovery.
5702
+ """
5456
5703
  project = project.resolve()
5457
5704
  root = _outbox_root(project)
5458
- all_archive_ids = sorted({
5705
+ now = time.time()
5706
+ retryable_ids = {
5459
5707
  path.stem
5460
5708
  for status in ("pending", "failed")
5461
5709
  for path in (root / status).glob("*.json")
5462
- })
5710
+ }
5711
+ stale_running_ids = {
5712
+ path.stem
5713
+ for path in (root / "running").glob("*.json")
5714
+ if now - path.stat().st_mtime >= stale_running_seconds
5715
+ }
5716
+ all_archive_ids = sorted(retryable_ids | stale_running_ids)
5463
5717
  archive_ids = all_archive_ids[:max(0, limit)]
5464
5718
  results = [maintain_knowledge(project, archive_id) for archive_id in archive_ids]
5465
5719
  return {
@@ -5467,6 +5721,7 @@ def drain_maintenance_outbox(project: Path, limit: int = 50) -> dict[str, Any]:
5467
5721
  "project": str(project),
5468
5722
  "processed": len(results),
5469
5723
  "remaining": max(0, len(all_archive_ids) - len(results)),
5724
+ "staleRunningRecovered": len(stale_running_ids & set(archive_ids)),
5470
5725
  "results": results,
5471
5726
  }
5472
5727
 
@@ -20,7 +20,7 @@ disallowed-tools:
20
20
  - Bash(awk *)
21
21
  - Bash(curl *)
22
22
  ---
23
- <!-- generated by harness_deploy.py; core=af97a6533498c853; overlay=none; agent=claude-code; do not edit -->
23
+ <!-- generated by harness_deploy.py; core=6a2c743a1f8ceb0b; overlay=none; agent=claude-code; do not edit -->
24
24
  # harness-knowledge-query
25
25
 
26
26
  并行 change 中查询必须传 `--change <change-id>`;成功后以该 change 的 `meta/knowledge-context.json` 为稳定指针。全局 `context-packs/latest.json` 仅向后兼容,不得用于判断多个 active change 的上下文归属。
@@ -20,7 +20,7 @@ disallowed-tools:
20
20
  - Bash(awk *)
21
21
  - Bash(curl *)
22
22
  ---
23
- <!-- generated by harness_deploy.py; core=af97a6533498c853; overlay=none; agent=claude-code; do not edit -->
23
+ <!-- generated by harness_deploy.py; core=6a2c743a1f8ceb0b; overlay=none; agent=claude-code; do not edit -->
24
24
  # harness-plan — 需求规划
25
25
 
26
26
  ## Purpose
@@ -21,7 +21,7 @@ disallowed-tools:
21
21
  - Bash(curl *)
22
22
  - Bash(codegraph *)
23
23
  ---
24
- <!-- generated by harness_deploy.py; core=af97a6533498c853; overlay=none; agent=claude-code; do not edit -->
24
+ <!-- generated by harness_deploy.py; core=6a2c743a1f8ceb0b; overlay=none; agent=claude-code; do not edit -->
25
25
  # harness-review — 代码审查
26
26
 
27
27
  ## Purpose
@@ -146,7 +146,7 @@ review 结果默认只作参考,不阻塞 submit/archive;报告措辞遵循
146
146
 
147
147
  ### 四、Shell 安全 / 敏感信息 / 证据化报告 / CodeGraph 探索
148
148
 
149
- git diff/log 命令通过 `powershell.exe -Command "..."` 执行;review-report 中如发现明文 token/密码/密钥,必须列入 RED 问题并在报告中以 `<TOKEN_REDACTED>` 等占位符引用;RED/YELLOW/OK 结论必须基于实际 diff 内容,不得凭印象判断。代码探索必须优先使用 CodeGraph MCP 工具(`mcp__codegraph__codegraph_explore`),不允许通过普通 Bash 调 codegraph 命令(已列入 `disallowed-tools`);MCP 不可用时降级为 Grep/Glob + Read,并在执行日志记录降级原因。遵循 `../protocols/powershell-protocol.md` / `sensitive-info-protocol.md` / `evidence-based-reporting-protocol.md`。
149
+ git diff/log 命令通过 `powershell.exe -Command "..."` 执行;review-report 中如发现明文 token/密码/密钥,必须列入 RED 问题并在报告中以 `<TOKEN_REDACTED>` 等占位符引用;RED/YELLOW/OK 结论必须基于实际 diff 内容,不得凭印象判断。代码探索必须优先使用 CodeGraph MCP 工具(`mcp__codegraph__codegraph_explore`),不允许通过普通 Bash 调 codegraph 命令(已列入 `disallowed-tools`);采纳任何返回源码前,按 `reference.md` 的 C12 合同校验 executionRoot/rootPath、worktreeId、repositoryId、HEAD 与 index snapshot,`IDENTITY_MISMATCH` 必须读取实际 worktree,不得当作 stale 忽略;MCP 不可用时降级为 Grep/Glob + Read,并在执行日志记录降级原因。遵循 `../protocols/powershell-protocol.md` / `sensitive-info-protocol.md` / `evidence-based-reporting-protocol.md`。
150
150
 
151
151
  ### 五、state snapshot 与源码重验
152
152