@hunter-harness/workflow-harness 0.2.5 → 0.2.6

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 (202) 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 -2
  3. package/harness/bundles/general/claude-code/harness-archive/reference.md +1 -1
  4. package/harness/bundles/general/claude-code/harness-archive/templates/render-summary.mjs +78 -155
  5. package/harness/bundles/general/claude-code/harness-codebase-map/SKILL.md +4 -2
  6. package/harness/bundles/general/claude-code/harness-knowledge-ingest/SKILL.md +5 -3
  7. package/harness/bundles/general/claude-code/harness-knowledge-ingest/scripts/harness_knowledge.py +83 -6
  8. package/harness/bundles/general/claude-code/harness-knowledge-query/SKILL.md +4 -2
  9. package/harness/bundles/general/claude-code/harness-plan/SKILL.md +6 -3
  10. package/harness/bundles/general/claude-code/harness-review/SKILL.md +6 -3
  11. package/harness/bundles/general/claude-code/harness-run/SKILL.md +5 -2
  12. package/harness/bundles/general/claude-code/harness-run/reference.md +1 -1
  13. package/harness/bundles/general/claude-code/harness-submit/SKILL.md +5 -2
  14. package/harness/bundles/general/claude-code/harness-sync/SKILL.md +5 -3
  15. package/harness/bundles/general/claude-code/harness-test/SKILL.md +5 -2
  16. package/harness/bundles/general/claude-code/harness-test/reference.md +1 -1
  17. package/harness/bundles/general/claude-code/protocols/ledger-protocol.md +1 -1
  18. package/harness/bundles/general/claude-code/protocols/report-pipeline-protocol.md +11 -4
  19. package/harness/bundles/general/claude-code/scripts/harness_archive.py +190 -32
  20. package/harness/bundles/general/claude-code/scripts/harness_events.py +134 -40
  21. package/harness/bundles/general/claude-code/scripts/harness_preflight.py +46 -3
  22. package/harness/bundles/general/claude-code/scripts/harness_profile.py +2 -2
  23. package/harness/bundles/general/claude-code/scripts/harness_state.py +148 -0
  24. package/harness/bundles/general/codebuddy/.harness-build.json +1 -1
  25. package/harness/bundles/general/codebuddy/harness-archive/SKILL.md +5 -2
  26. package/harness/bundles/general/codebuddy/harness-archive/reference.md +1 -1
  27. package/harness/bundles/general/codebuddy/harness-archive/templates/render-summary.mjs +78 -155
  28. package/harness/bundles/general/codebuddy/harness-codebase-map/SKILL.md +4 -2
  29. package/harness/bundles/general/codebuddy/harness-knowledge-ingest/SKILL.md +5 -3
  30. package/harness/bundles/general/codebuddy/harness-knowledge-ingest/scripts/harness_knowledge.py +83 -6
  31. package/harness/bundles/general/codebuddy/harness-knowledge-query/SKILL.md +4 -2
  32. package/harness/bundles/general/codebuddy/harness-plan/SKILL.md +6 -3
  33. package/harness/bundles/general/codebuddy/harness-review/SKILL.md +6 -3
  34. package/harness/bundles/general/codebuddy/harness-run/SKILL.md +5 -2
  35. package/harness/bundles/general/codebuddy/harness-run/reference.md +1 -1
  36. package/harness/bundles/general/codebuddy/harness-submit/SKILL.md +5 -2
  37. package/harness/bundles/general/codebuddy/harness-sync/SKILL.md +5 -3
  38. package/harness/bundles/general/codebuddy/harness-test/SKILL.md +5 -2
  39. package/harness/bundles/general/codebuddy/harness-test/reference.md +1 -1
  40. package/harness/bundles/general/codebuddy/protocols/ledger-protocol.md +1 -1
  41. package/harness/bundles/general/codebuddy/protocols/report-pipeline-protocol.md +11 -4
  42. package/harness/bundles/general/codebuddy/scripts/harness_archive.py +190 -32
  43. package/harness/bundles/general/codebuddy/scripts/harness_events.py +134 -40
  44. package/harness/bundles/general/codebuddy/scripts/harness_preflight.py +46 -3
  45. package/harness/bundles/general/codebuddy/scripts/harness_profile.py +2 -2
  46. package/harness/bundles/general/codebuddy/scripts/harness_state.py +148 -0
  47. package/harness/bundles/general/codex/.harness-build.json +1 -1
  48. package/harness/bundles/general/codex/harness-archive/SKILL.md +5 -2
  49. package/harness/bundles/general/codex/harness-archive/reference.md +1 -1
  50. package/harness/bundles/general/codex/harness-archive/templates/render-summary.mjs +78 -155
  51. package/harness/bundles/general/codex/harness-codebase-map/SKILL.md +4 -2
  52. package/harness/bundles/general/codex/harness-knowledge-ingest/SKILL.md +5 -3
  53. package/harness/bundles/general/codex/harness-knowledge-ingest/scripts/harness_knowledge.py +83 -6
  54. package/harness/bundles/general/codex/harness-knowledge-query/SKILL.md +4 -2
  55. package/harness/bundles/general/codex/harness-plan/SKILL.md +6 -3
  56. package/harness/bundles/general/codex/harness-review/SKILL.md +5 -2
  57. package/harness/bundles/general/codex/harness-run/SKILL.md +5 -2
  58. package/harness/bundles/general/codex/harness-run/reference.md +1 -1
  59. package/harness/bundles/general/codex/harness-submit/SKILL.md +5 -2
  60. package/harness/bundles/general/codex/harness-sync/SKILL.md +5 -3
  61. package/harness/bundles/general/codex/harness-test/SKILL.md +5 -2
  62. package/harness/bundles/general/codex/harness-test/reference.md +1 -1
  63. package/harness/bundles/general/codex/protocols/ledger-protocol.md +1 -1
  64. package/harness/bundles/general/codex/protocols/report-pipeline-protocol.md +11 -4
  65. package/harness/bundles/general/codex/scripts/harness_archive.py +190 -32
  66. package/harness/bundles/general/codex/scripts/harness_events.py +134 -40
  67. package/harness/bundles/general/codex/scripts/harness_preflight.py +46 -3
  68. package/harness/bundles/general/codex/scripts/harness_profile.py +2 -2
  69. package/harness/bundles/general/codex/scripts/harness_state.py +148 -0
  70. package/harness/bundles/general/cursor/.harness-build.json +1 -1
  71. package/harness/bundles/general/cursor/harness-archive/SKILL.md +5 -2
  72. package/harness/bundles/general/cursor/harness-archive/reference.md +1 -1
  73. package/harness/bundles/general/cursor/harness-archive/templates/render-summary.mjs +78 -155
  74. package/harness/bundles/general/cursor/harness-codebase-map/SKILL.md +4 -2
  75. package/harness/bundles/general/cursor/harness-knowledge-ingest/SKILL.md +5 -3
  76. package/harness/bundles/general/cursor/harness-knowledge-ingest/scripts/harness_knowledge.py +83 -6
  77. package/harness/bundles/general/cursor/harness-knowledge-query/SKILL.md +4 -2
  78. package/harness/bundles/general/cursor/harness-plan/SKILL.md +6 -3
  79. package/harness/bundles/general/cursor/harness-review/SKILL.md +5 -2
  80. package/harness/bundles/general/cursor/harness-run/SKILL.md +5 -2
  81. package/harness/bundles/general/cursor/harness-run/reference.md +1 -1
  82. package/harness/bundles/general/cursor/harness-submit/SKILL.md +5 -2
  83. package/harness/bundles/general/cursor/harness-sync/SKILL.md +5 -3
  84. package/harness/bundles/general/cursor/harness-test/SKILL.md +5 -2
  85. package/harness/bundles/general/cursor/harness-test/reference.md +1 -1
  86. package/harness/bundles/general/cursor/protocols/ledger-protocol.md +1 -1
  87. package/harness/bundles/general/cursor/protocols/report-pipeline-protocol.md +11 -4
  88. package/harness/bundles/general/cursor/scripts/harness_archive.py +190 -32
  89. package/harness/bundles/general/cursor/scripts/harness_events.py +134 -40
  90. package/harness/bundles/general/cursor/scripts/harness_preflight.py +46 -3
  91. package/harness/bundles/general/cursor/scripts/harness_profile.py +2 -2
  92. package/harness/bundles/general/cursor/scripts/harness_state.py +148 -0
  93. package/harness/bundles/java/claude-code/.harness-build.json +1 -1
  94. package/harness/bundles/java/claude-code/harness-apidoc/SKILL.md +5 -2
  95. package/harness/bundles/java/claude-code/harness-archive/SKILL.md +5 -2
  96. package/harness/bundles/java/claude-code/harness-archive/reference.md +1 -1
  97. package/harness/bundles/java/claude-code/harness-archive/templates/render-summary.mjs +78 -155
  98. package/harness/bundles/java/claude-code/harness-codebase-map/SKILL.md +4 -2
  99. package/harness/bundles/java/claude-code/harness-knowledge-ingest/SKILL.md +5 -3
  100. package/harness/bundles/java/claude-code/harness-knowledge-ingest/scripts/harness_knowledge.py +83 -6
  101. package/harness/bundles/java/claude-code/harness-knowledge-query/SKILL.md +4 -2
  102. package/harness/bundles/java/claude-code/harness-package/SKILL.md +5 -2
  103. package/harness/bundles/java/claude-code/harness-plan/SKILL.md +6 -3
  104. package/harness/bundles/java/claude-code/harness-review/SKILL.md +6 -3
  105. package/harness/bundles/java/claude-code/harness-run/SKILL.md +5 -2
  106. package/harness/bundles/java/claude-code/harness-run/reference.md +1 -1
  107. package/harness/bundles/java/claude-code/harness-submit/SKILL.md +5 -2
  108. package/harness/bundles/java/claude-code/harness-sync/SKILL.md +5 -3
  109. package/harness/bundles/java/claude-code/harness-test/SKILL.md +5 -2
  110. package/harness/bundles/java/claude-code/harness-test/reference.md +1 -1
  111. package/harness/bundles/java/claude-code/protocols/ledger-protocol.md +1 -1
  112. package/harness/bundles/java/claude-code/protocols/report-pipeline-protocol.md +11 -4
  113. package/harness/bundles/java/claude-code/scripts/harness_archive.py +190 -32
  114. package/harness/bundles/java/claude-code/scripts/harness_events.py +134 -40
  115. package/harness/bundles/java/claude-code/scripts/harness_preflight.py +46 -3
  116. package/harness/bundles/java/claude-code/scripts/harness_profile.py +2 -2
  117. package/harness/bundles/java/claude-code/scripts/harness_state.py +148 -0
  118. package/harness/bundles/java/codebuddy/.harness-build.json +1 -1
  119. package/harness/bundles/java/codebuddy/harness-apidoc/SKILL.md +5 -2
  120. package/harness/bundles/java/codebuddy/harness-archive/SKILL.md +5 -2
  121. package/harness/bundles/java/codebuddy/harness-archive/reference.md +1 -1
  122. package/harness/bundles/java/codebuddy/harness-archive/templates/render-summary.mjs +78 -155
  123. package/harness/bundles/java/codebuddy/harness-codebase-map/SKILL.md +4 -2
  124. package/harness/bundles/java/codebuddy/harness-knowledge-ingest/SKILL.md +5 -3
  125. package/harness/bundles/java/codebuddy/harness-knowledge-ingest/scripts/harness_knowledge.py +83 -6
  126. package/harness/bundles/java/codebuddy/harness-knowledge-query/SKILL.md +4 -2
  127. package/harness/bundles/java/codebuddy/harness-package/SKILL.md +5 -2
  128. package/harness/bundles/java/codebuddy/harness-plan/SKILL.md +6 -3
  129. package/harness/bundles/java/codebuddy/harness-review/SKILL.md +6 -3
  130. package/harness/bundles/java/codebuddy/harness-run/SKILL.md +5 -2
  131. package/harness/bundles/java/codebuddy/harness-run/reference.md +1 -1
  132. package/harness/bundles/java/codebuddy/harness-submit/SKILL.md +5 -2
  133. package/harness/bundles/java/codebuddy/harness-sync/SKILL.md +5 -3
  134. package/harness/bundles/java/codebuddy/harness-test/SKILL.md +5 -2
  135. package/harness/bundles/java/codebuddy/harness-test/reference.md +1 -1
  136. package/harness/bundles/java/codebuddy/protocols/ledger-protocol.md +1 -1
  137. package/harness/bundles/java/codebuddy/protocols/report-pipeline-protocol.md +11 -4
  138. package/harness/bundles/java/codebuddy/scripts/harness_archive.py +190 -32
  139. package/harness/bundles/java/codebuddy/scripts/harness_events.py +134 -40
  140. package/harness/bundles/java/codebuddy/scripts/harness_preflight.py +46 -3
  141. package/harness/bundles/java/codebuddy/scripts/harness_profile.py +2 -2
  142. package/harness/bundles/java/codebuddy/scripts/harness_state.py +148 -0
  143. package/harness/bundles/java/codex/.harness-build.json +1 -1
  144. package/harness/bundles/java/codex/harness-apidoc/SKILL.md +5 -2
  145. package/harness/bundles/java/codex/harness-archive/SKILL.md +5 -2
  146. package/harness/bundles/java/codex/harness-archive/reference.md +1 -1
  147. package/harness/bundles/java/codex/harness-archive/templates/render-summary.mjs +78 -155
  148. package/harness/bundles/java/codex/harness-codebase-map/SKILL.md +4 -2
  149. package/harness/bundles/java/codex/harness-knowledge-ingest/SKILL.md +5 -3
  150. package/harness/bundles/java/codex/harness-knowledge-ingest/scripts/harness_knowledge.py +83 -6
  151. package/harness/bundles/java/codex/harness-knowledge-query/SKILL.md +4 -2
  152. package/harness/bundles/java/codex/harness-package/SKILL.md +5 -2
  153. package/harness/bundles/java/codex/harness-plan/SKILL.md +6 -3
  154. package/harness/bundles/java/codex/harness-review/SKILL.md +5 -2
  155. package/harness/bundles/java/codex/harness-run/SKILL.md +5 -2
  156. package/harness/bundles/java/codex/harness-run/reference.md +1 -1
  157. package/harness/bundles/java/codex/harness-submit/SKILL.md +5 -2
  158. package/harness/bundles/java/codex/harness-sync/SKILL.md +5 -3
  159. package/harness/bundles/java/codex/harness-test/SKILL.md +5 -2
  160. package/harness/bundles/java/codex/harness-test/reference.md +1 -1
  161. package/harness/bundles/java/codex/protocols/ledger-protocol.md +1 -1
  162. package/harness/bundles/java/codex/protocols/report-pipeline-protocol.md +11 -4
  163. package/harness/bundles/java/codex/scripts/harness_archive.py +190 -32
  164. package/harness/bundles/java/codex/scripts/harness_events.py +134 -40
  165. package/harness/bundles/java/codex/scripts/harness_preflight.py +46 -3
  166. package/harness/bundles/java/codex/scripts/harness_profile.py +2 -2
  167. package/harness/bundles/java/codex/scripts/harness_state.py +148 -0
  168. package/harness/bundles/java/cursor/.harness-build.json +1 -1
  169. package/harness/bundles/java/cursor/harness-apidoc/SKILL.md +5 -2
  170. package/harness/bundles/java/cursor/harness-archive/SKILL.md +5 -2
  171. package/harness/bundles/java/cursor/harness-archive/reference.md +1 -1
  172. package/harness/bundles/java/cursor/harness-archive/templates/render-summary.mjs +78 -155
  173. package/harness/bundles/java/cursor/harness-codebase-map/SKILL.md +4 -2
  174. package/harness/bundles/java/cursor/harness-knowledge-ingest/SKILL.md +5 -3
  175. package/harness/bundles/java/cursor/harness-knowledge-ingest/scripts/harness_knowledge.py +83 -6
  176. package/harness/bundles/java/cursor/harness-knowledge-query/SKILL.md +4 -2
  177. package/harness/bundles/java/cursor/harness-package/SKILL.md +5 -2
  178. package/harness/bundles/java/cursor/harness-plan/SKILL.md +6 -3
  179. package/harness/bundles/java/cursor/harness-review/SKILL.md +5 -2
  180. package/harness/bundles/java/cursor/harness-run/SKILL.md +5 -2
  181. package/harness/bundles/java/cursor/harness-run/reference.md +1 -1
  182. package/harness/bundles/java/cursor/harness-submit/SKILL.md +5 -2
  183. package/harness/bundles/java/cursor/harness-sync/SKILL.md +5 -3
  184. package/harness/bundles/java/cursor/harness-test/SKILL.md +5 -2
  185. package/harness/bundles/java/cursor/harness-test/reference.md +1 -1
  186. package/harness/bundles/java/cursor/protocols/ledger-protocol.md +1 -1
  187. package/harness/bundles/java/cursor/protocols/report-pipeline-protocol.md +11 -4
  188. package/harness/bundles/java/cursor/scripts/harness_archive.py +190 -32
  189. package/harness/bundles/java/cursor/scripts/harness_events.py +134 -40
  190. package/harness/bundles/java/cursor/scripts/harness_preflight.py +46 -3
  191. package/harness/bundles/java/cursor/scripts/harness_profile.py +2 -2
  192. package/harness/bundles/java/cursor/scripts/harness_state.py +148 -0
  193. package/harness/manifests/general/claude-code.json +24 -24
  194. package/harness/manifests/general/codebuddy.json +24 -24
  195. package/harness/manifests/general/codex.json +24 -24
  196. package/harness/manifests/general/cursor.json +24 -24
  197. package/harness/manifests/java/claude-code.json +26 -26
  198. package/harness/manifests/java/codebuddy.json +26 -26
  199. package/harness/manifests/java/codex.json +26 -26
  200. package/harness/manifests/java/cursor.json +26 -26
  201. package/hunter-workflow-family.json +1 -1
  202. package/package.json +1 -1
@@ -2,5 +2,5 @@
2
2
  "schemaVersion": 1,
3
3
  "overlay": "none",
4
4
  "agent": "claude-code",
5
- "coreHash": "5cadfa54663ecdbf"
5
+ "coreHash": "e29fb748896b6f88"
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=5cadfa54663ecdbf; overlay=none; agent=claude-code; do not edit -->
23
+ <!-- generated by harness_deploy.py; core=e29fb748896b6f88; overlay=none; agent=claude-code; do not edit -->
24
24
  # harness-archive — 变更归档
25
25
 
26
26
  ## Purpose
@@ -66,6 +66,7 @@ disallowed-tools:
66
66
  6. **implementation-detail** — 自适应执行参考;legacy 缺失 🟡WARN,不阻断
67
67
  7. **test-scenarios** — 测试真相源:`plans/<change>-test-scenarios.md`
68
68
  8. **禁止读取 `docs/superpowers/` 作为正式输入** — 旧草稿仅人工线索
69
+ 9. **每个 skill 边界只刷新一次状态快照** — 先运行 `python <skills-root>/scripts/harness_state.py capture --project . --change-dir ".harness/changes/<change-name>" --base <baseCommit> --json`;`changedSegments=[]` 时复用已有 profile/rules/map/knowledge/change/code 指纹,不再重复跑全量 `harness-sync`、知识重建或代码库扫描。只有对应 segment 变化或缺失时才定向刷新;快照不能替代源码读取和验证门禁。
69
70
 
70
71
  状态目录分层:新路径优先,旧路径兼容 → [[../protocols/state-layout-protocol.md|state-layout-protocol]]
71
72
 
@@ -202,7 +203,7 @@ git 命令通过 `powershell.exe -Command "..."` 执行;archive-meta.md 和 fi
202
203
 
203
204
  ## 执行日志
204
205
 
205
- `events.ndjson` 为唯一事实源(schema_version 2,`note` 承载人类可读摘要);`logs/execution-log.md` 由 `harness_events.py` 渲染,**禁止用 Write/Edit 直接维护**。直接修改的内容会在 `phase.end` 或 finalize 时被完整重建覆盖,属于数据丢失;需要保留的详情必须进入事件 `note`。结构 → [[../protocols/report-pipeline-protocol.md|report-pipeline-protocol]]
206
+ `events.ndjson` 为唯一事实源(schema_version 3,兼容读取 v1/v2;`note` 承载人类可读摘要);`logs/execution-log.md` 由 `harness_events.py` 渲染,**禁止用 Write/Edit 直接维护**。直接修改的内容会在 `phase.end` 或 finalize 时被完整重建覆盖,属于数据丢失;需要保留的详情必须进入事件 `note`。结构 → [[../protocols/report-pipeline-protocol.md|report-pipeline-protocol]]
206
207
 
207
208
  ```powershell
208
209
  python <skills-root>/scripts/harness_events.py append --change-dir ".harness/changes/<change-name>" --phase <phase> --type phase.start --note "<触发指令>"
@@ -211,3 +212,5 @@ python <skills-root>/scripts/harness_events.py append --change-dir ".harness/cha
211
212
  > **脚本接线**:`harness_events.py append`;`harness_archive.py finalize`;`harness_preflight.py check`;`harness_ledger.py can-reuse`;`harness_service.py ensure/stop`(须 `--files`/`serviceStart.inputFiles`)。JSON 输出按 D13 护栏解读。
212
213
 
213
214
  > **Task 4 §6.1 写入契约**:普通 `append` = 加锁 -> 追加一行 -> fsync -> 解锁,**不 load 历史、不渲染**(O(1),跨进程锁 `events.ndjson.lock`,UUID 用完整 `uuid4().hex` 无需去重扫描)。仅 `--type phase.end` append 在追加成功后渲染一次 `execution-log.md`;显式 `harness_events.py render` 随时从完整 events 重建;`harness_archive.py finalize` 在 collect 前强制 render 一次。高频 command append 期间 log 可能滞后,phase 边界保持最新。
215
+
216
+ 每次阶段结束必须写 `--status OK|WARN|FAIL|BLOCKED`;重试同一阶段时增加 `--attempt <n>`,不得覆盖或伪装成一次执行。跨工具继续执行时写 `--executor-tool <codex|claude-code|codebuddy|cursor>`,并在接棒事件写 `--handoff-from-tool` / `--handoff-reason`;也可由 `HUNTER_HARNESS_TOOL/AGENT/MODEL/RUN_ID` 环境变量统一注入。
@@ -92,4 +92,4 @@ powershell.exe -NoProfile -Command "& '<node-path>' 'harness-skills/harness-arch
92
92
 
93
93
  ## 执行日志记录
94
94
 
95
- 归档只向 `events.ndjson` 追加事件(schema_version 2);`logs/execution-log.md` 由 `harness_events.py append` 自动渲染,禁止手工 Edit。事件类型与脚本用法见 SKILL.md `## 执行日志` 与 `../protocols/report-pipeline-protocol.md`。
95
+ 归档只向 `events.ndjson` 追加事件(schema_version 3,兼容读取 v1/v2);`logs/execution-log.md` 由 `harness_events.py append` 自动渲染,禁止手工 Edit。事件类型与脚本用法见 SKILL.md `## 执行日志` 与 `../protocols/report-pipeline-protocol.md`。
@@ -1,171 +1,94 @@
1
1
  #!/usr/bin/env node
2
- // render-summary.mjs
3
- // Deterministic UTF-8 renderer for harness archive final-summary.html.
4
- // Input: summary-data.json (schemaVersion 2.2). Output: final-summary.html.
5
- // Keeps UTF-8, avoids PowerShell string interpolation issues.
6
- // All numbers come from summary-data.json or manifest — never re-inferred here.
7
-
8
- import fs from 'node:fs';
9
- import path from 'node:path';
2
+ // Compact deterministic renderer for Harness archive summary-data.json.
3
+ import fs from "node:fs";
4
+ import path from "node:path";
10
5
 
11
6
  const args = process.argv.slice(2);
12
- function getArg(name, fallback) {
13
- const i = args.indexOf(name);
14
- return i >= 0 && args[i + 1] ? args[i + 1] : fallback;
15
- }
16
-
17
- const summaryPath = getArg('--summary', args[0]);
18
- const outPath = getArg('--out', args[1] || 'final-summary.html');
7
+ const arg = (name, fallback) => {
8
+ const index = args.indexOf(name);
9
+ return index >= 0 && args[index + 1] ? args[index + 1] : fallback;
10
+ };
11
+ const summaryPath = arg("--summary", args[0]);
12
+ const outPath = arg("--out", args[1] || "final-summary.html");
19
13
  if (!summaryPath) {
20
- console.error('Usage: node render-summary.mjs --summary summary-data.json --out final-summary.html');
14
+ console.error("Usage: node render-summary.mjs --summary summary-data.json --out final-summary.html");
21
15
  process.exit(2);
22
16
  }
23
17
 
24
- const data = JSON.parse(fs.readFileSync(summaryPath, 'utf8'));
25
- const esc = (v) => String(v ?? '').replace(/[&<>"']/g, (c) => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));
26
- const arr = (v) => Array.isArray(v) ? v : [];
27
- const obj = (v) => v && typeof v === 'object' ? v : {};
28
- const num = (v) => Number(v) || 0;
29
- const badgeClass = (s) => {
30
- const v = String(s ?? '').toUpperCase();
31
- if (v.includes('FAIL') || v.includes('ERROR')) return 'bad';
32
- if (v.includes('WARN') || v.includes('BLOCK') || v.includes('SKIP') || v.includes('CONDITIONAL') || v.includes('PARTIAL') || v.includes('NOT_RUN')) return 'warn';
33
- if (v.includes('ADVISORY') || v.includes('REUSED')) return 'info';
34
- return 'ok';
18
+ const data = JSON.parse(fs.readFileSync(summaryPath, "utf8"));
19
+ const esc = (value) => String(value ?? "").replace(/[&<>"']/g, (char) => ({
20
+ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;"
21
+ })[char]);
22
+ const list = (value) => Array.isArray(value) ? value : [];
23
+ const record = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : {};
24
+ const number = (value) => Number(value) || 0;
25
+ const shortHash = (value) => String(value || "N/A").slice(0, 10);
26
+ const statusClass = (value) => {
27
+ const status = String(value || "UNKNOWN").toUpperCase();
28
+ if (/FAIL|ERROR/.test(status)) return "danger";
29
+ if (/WARN|BLOCK|SKIP|CONDITIONAL|PARTIAL|NOT_RUN|UNKNOWN/.test(status)) return "warning";
30
+ if (/ADVISORY|REUSED/.test(status)) return "neutral";
31
+ return "success";
35
32
  };
36
- const badge = (s) => `<span class="badge ${badgeClass(s)}">${esc(s || 'N/A')}</span>`;
37
-
38
- // pass-rate progress bar: parses "183/185" style strings
39
- const parseRate = (rate) => {
40
- if (!rate) return null;
41
- const m = String(rate).match(/(\d+)\s*\/\s*(\d+)/);
42
- return m ? { passed: +m[1], total: +m[2] } : null;
33
+ const pill = (value) => `<span class="pill ${statusClass(value)}">${esc(value || "UNKNOWN")}</span>`;
34
+ const describe = (value) => {
35
+ if (typeof value === "string") return value;
36
+ if (!value || typeof value !== "object") return String(value ?? "");
37
+ return value.message || value.action || value.note || value.summary || JSON.stringify(value);
43
38
  };
44
- const rateBar = (rateStr, status) => {
45
- const r = parseRate(rateStr);
46
- if (!r || r.total <= 0) return `<span class="muted">N/A</span>`;
47
- const pct = Math.round(r.passed / r.total * 100);
48
- const cls = badgeClass(status || (r.passed >= r.total ? 'OK' : 'WARN'));
49
- return `<div class="rate"><div class="bar-track"><div class="bar-fill ${cls}" style="width:${pct}%"></div></div><span class="muted small">${esc(rateStr)} · ${pct}%</span></div>`;
39
+ const duration = (minutes) => {
40
+ const value = number(minutes);
41
+ if (value < 1) return `${Math.round(value * 60)} 秒`;
42
+ if (value < 60) return `${Math.round(value * 10) / 10} 分钟`;
43
+ return `${Math.floor(value / 60)} 小时 ${Math.round(value % 60)} 分钟`;
50
44
  };
51
45
 
52
- const stageRows = Object.entries(obj(data.stageStatus)).map(([k,v]) => `<tr><td>${esc(k)}</td><td>${badge(v)}</td></tr>`).join('\n') || `<tr><td colspan="2" class="muted">未记录阶段状态</td></tr>`;
53
-
54
- const verification = obj(data.verification);
55
- const unit = obj(verification.unitTests);
56
- const api = obj(verification.apiTests);
57
- const unitStatus = (num(unit.failures) + num(unit.errors)) > 0 ? 'WARN' : 'OK';
58
- const verificationRows = [
59
- ['Unit Tests', `${rateBar(unit.passRate, unitStatus)}<div class="muted small">${esc(unit.run ?? 0)} run · ${esc(unit.failures ?? 0)} fail · ${esc(unit.errors ?? 0)} err · ${esc(unit.skipped ?? 0)} skip · ${esc(unit.source || 'not recorded')}</div>`],
60
- ['API Tests', `${rateBar(api.passRate, api.status)}<div class="muted small">${esc(api.passed ?? 0)}/${esc(api.total ?? 0)} · ${esc(api.status || data.stageStatus?.test || 'N/A')}</div>`],
61
- ['DB Compat', badge(verification.dbCompatibility || 'N/A')],
62
- ['Coverage', esc(verification.coverageDisplay || 'N/A')],
63
- ['Overall', badge(data.finalStatus || data.overallStatus || 'N/A')]
64
- ].map(([a,b]) => `<tr><td>${a}</td><td>${b}</td></tr>`).join('\n');
65
-
66
- const diffStat = obj(data.diffStat);
67
- const changedRows = arr(data.changedFiles).map(f => `<tr><td><code>${esc(f.path || f.file || '')}</code></td><td>${esc(f.summary || f.change || '')}</td><td class="ins">+${esc(f.insertions ?? 0)}</td><td class="del">-${esc(f.deletions ?? 0)}</td></tr>`).join('\n') || `<tr><td colspan="4" class="muted">未记录变更文件</td></tr>`;
68
- const artifactList = arr(data.artifacts);
69
- const artifactRows = artifactList.map(a => `<tr><td>${esc(a.name || '')}</td><td><code>${esc(a.path || '')}</code></td><td>${esc(a.size || '')}</td><td><code>${esc(a.sha256 || '')}</code></td></tr>`).join('\n');
70
- const artifactCard = artifactList.length > 0 ? `<div class="card"><h2>📦 产物清单</h2><table><tr><th>名称</th><th>路径</th><th>大小</th><th>SHA-256</th></tr>${artifactRows}</table></div>` : '';
71
- const notes = arr(data.maintenanceNotes).map(n => `<li>${esc(n)}</li>`).join('\n') || `<li class="muted">无额外维护说明</li>`;
72
- const risks = arr(data.knownRisks).map(n => `<li>${esc(n)}</li>`).join('\n') || `<li class="muted">无已知遗留风险</li>`;
73
- const manual = arr(data.manualActions).map(n => `<li>${esc(n)}</li>`).join('\n') || `<li class="muted">无人工后续动作</li>`;
74
- const uncommittedList = arr(data.uncommittedTestEvidence);
75
- const uncommittedTests = uncommittedList.map(n => `<li>${esc(n)}</li>`).join('\n');
76
- const uncommittedCard = uncommittedList.length > 0 ? `<div class="card"><h2>🧪 未提交测试证据</h2><ul>${uncommittedTests}</ul></div>` : '';
77
- const review = obj(data.reviewSummary);
78
- const manifest = obj(data.archiveManifest);
79
- const timeline = arr(data.timeline).map(t => `<tr><td>${esc(t.stage || '')}</td><td>${badge(t.result || '')}</td><td>${esc(t.evidence || '')}</td></tr>`).join('\n');
80
-
81
- // durations + skillCalls (schema 2.1); fall back to timeline when absent
82
- const durations = obj(data.durations);
83
- const durStages = arr(durations.stages);
84
- const maxMin = durStages.reduce((m, s) => Math.max(m, num(s.minutes)), 0);
85
- const durRows = durStages.map(s => {
86
- const min = num(s.minutes);
87
- const pct = maxMin > 0 ? Math.round(min / maxMin * 100) : 0;
88
- return `<div class="dur-row"><div class="dur-label">${esc(s.skill || s.stage || '')} <span class="muted small">${esc(s.result || '')}</span></div><div class="dur-track"><div class="dur-bar" style="width:${pct}%"></div></div><div class="dur-min">${esc(min)}m</div></div>`;
89
- }).join('\n');
90
- const skillCallsHtml = arr(data.skillCalls).map(s => `${esc(s.skill)}×${esc(s.count)} ${badge(s.result)}`).join(' ') || '<span class="muted">未记录</span>';
91
-
92
- const durCard = durStages.length > 0
93
- ? `<div class="card"><h2>⏱️ 阶段耗时与 Skill 调用</h2>${durRows}<div class="muted small" style="margin-top:10px">Skill 调用:${skillCallsHtml}</div></div>`
94
- : (timeline ? `<div class="card"><h2>阶段时间线</h2><table><tr><th>阶段</th><th>结果</th><th>证据</th></tr>${timeline}</table></div>` : '');
46
+ const stages = Object.entries(record(data.stageStatus));
47
+ const verification = record(data.verification);
48
+ const unit = record(verification.unitTests);
49
+ const api = record(verification.apiTests);
50
+ const durations = record(data.durations);
51
+ const durationStages = list(durations.stages);
52
+ const maxMinutes = Math.max(1, ...durationStages.map((item) => number(item.minutes)));
53
+ const diff = record(data.diffStat);
54
+ const risks = list(data.knownRisks);
55
+ const actions = list(data.manualActions);
56
+ const files = list(data.changedFiles);
57
+ const commands = list(record(data.reportPipeline).commands);
58
+ const timeline = list(data.timeline);
95
59
 
96
- const totalLabel = durations.totalLabel || (durations.totalMinutes ? durations.totalMinutes + 'm' : 'N/A');
60
+ const stageHtml = stages.map(([name, status]) => `<div class="row"><span>${esc(name)}</span>${pill(status)}</div>`).join("") || '<p class="empty">没有阶段状态记录</p>';
61
+ const verificationHtml = [
62
+ ["单元测试", unit.status || ((number(unit.failures) + number(unit.errors)) > 0 ? "FAIL" : (number(unit.run) > 0 ? "OK" : "NOT_RUN")), `${number(unit.run)} 个 · ${number(unit.failures)} 失败 · ${number(unit.errors)} 错误`],
63
+ ["API 测试", api.status || "NOT_RUN", `${number(api.passed)}/${number(api.total)} 通过 · ${number(api.blocked)} 阻塞`],
64
+ ["数据库兼容", verification.dbCompatibility || "NOT_RUN", verification.coverageDisplay || "未记录覆盖率"]
65
+ ].map(([name, status, note]) => `<div class="row"><div><strong>${esc(name)}</strong><small>${esc(note)}</small></div>${pill(status)}</div>`).join("");
66
+ const durationHtml = durationStages.map((item) => {
67
+ const width = Math.max(2, Math.round(number(item.minutes) / maxMinutes * 100));
68
+ const attempts = list(item.attempts).length;
69
+ return `<div class="duration"><div><span>${esc(item.skill || item.stage)}</span><span>${duration(item.minutes)}${attempts > 1 ? ` · ${attempts} 次尝试` : ""}</span></div><i><b style="width:${width}%"></b></i></div>`;
70
+ }).join("") || '<p class="empty">没有可计算的阶段耗时</p>';
71
+ const riskHtml = risks.map((item) => `<li>${esc(describe(item))}</li>`).join("") || "<li>未记录已知风险</li>";
72
+ const actionHtml = actions.map((item) => `<li>${esc(describe(item))}</li>`).join("") || "<li>无需人工后续动作</li>";
73
+ const fileRows = files.map((item) => `<tr><td><code>${esc(item.path || item.file)}</code></td><td class="plus">+${number(item.insertions)}</td><td class="minus">-${number(item.deletions)}</td></tr>`).join("") || '<tr><td colspan="3">没有变更文件证据</td></tr>';
74
+ const timelineRows = timeline.map((item) => `<tr><td>${esc(item.phase || item.stage || "-")}</td><td>${esc(item.attempt || "-")}</td><td>${pill(item.status || item.result || item.type)}</td><td>${esc(item.executorTool || item.executor_tool || item.summary || "-")}</td></tr>`).join("") || '<tr><td colspan="4">没有时间线记录</td></tr>';
75
+ const commandRows = commands.map((item) => `<tr><td>${esc(item.phase || "-")}</td><td><code>${esc(item.command)}</code></td><td>${pill(number(item.exit_code) === 0 ? "OK" : "FAIL")}</td></tr>`).join("") || '<tr><td colspan="3">没有命令证据</td></tr>';
97
76
 
98
- const html = `<!doctype html>
99
- <html lang="zh-CN">
100
- <head>
101
- <meta charset="utf-8" />
102
- <meta name="viewport" content="width=device-width, initial-scale=1" />
103
- <title>变更最终报告 - ${esc(data.changeName || '')}</title>
104
- <style>
105
- :root { color-scheme: dark; }
106
- * { box-sizing: border-box; }
107
- body { font-family: -apple-system,BlinkMacSystemFont,'Segoe UI','Microsoft YaHei',sans-serif; margin: 0; background: #0b1020; color: #e5e7eb; }
108
- main { max-width: 1120px; margin: 0 auto; padding: 32px; }
109
- .card { background: rgba(255,255,255,.06); border: 1px solid rgba(255,255,255,.12); border-radius: 16px; padding: 20px; margin: 16px 0; box-shadow: 0 12px 40px rgba(0,0,0,.18); }
110
- h1 { margin: 0 0 8px; font-size: 30px; }
111
- h2 { margin: 0 0 12px; font-size: 20px; }
112
- h3 { margin: 18px 0 8px; }
113
- .muted { color: #94a3b8; }
114
- .small { font-size: 12px; }
115
- table { width: 100%; border-collapse: collapse; table-layout: fixed; }
116
- th, td { border-bottom: 1px solid rgba(255,255,255,.12); padding: 10px; text-align: left; vertical-align: top; overflow-wrap: anywhere; }
117
- th { color: #cbd5e1; font-weight: 600; }
118
- tbody tr:nth-child(even) { background: rgba(255,255,255,.03); }
119
- tbody tr:hover { background: rgba(96,165,250,.08); }
120
- code { color: #bfdbfe; word-break: break-all; }
121
- .ins { color: #86efac; font-weight: 600; }
122
- .del { color: #fca5a5; font-weight: 600; }
123
- .badge { display: inline-block; padding: 4px 8px; border-radius: 999px; font-weight: 700; font-size: 12px; }
124
- .badge.ok { background: rgba(34,197,94,.16); color: #86efac; }
125
- .badge.warn { background: rgba(245,158,11,.18); color: #fcd34d; }
126
- .badge.bad { background: rgba(239,68,68,.18); color: #fca5a5; }
127
- .badge.info { background: rgba(59,130,246,.18); color: #93c5fd; }
128
- .grid { display: grid; grid-template-columns: repeat(auto-fit,minmax(200px,1fr)); gap: 16px; }
129
- .kpi { font-size: 18px; font-weight: 700; margin-top: 4px; word-break: break-all; }
130
- .hero { border-color: rgba(96,165,250,.35); background: linear-gradient(135deg, rgba(59,130,246,.12), rgba(255,255,255,.04)); }
131
- ul { margin-top: 8px; }
132
- .bar-track { background: rgba(255,255,255,.08); border-radius: 999px; height: 8px; overflow: hidden; margin: 4px 0; }
133
- .bar-fill { height: 100%; border-radius: 999px; }
134
- .bar-fill.ok { background: #22c55e; }
135
- .bar-fill.warn { background: #f59e0b; }
136
- .bar-fill.bad { background: #ef4444; }
137
- .bar-fill.info { background: #3b82f6; }
138
- .rate { display: flex; flex-direction: column; gap: 2px; min-width: 160px; }
139
- .dur-row { display: grid; grid-template-columns: 150px 1fr 50px; align-items: center; gap: 10px; padding: 6px 0; }
140
- .dur-label { color: #93c5fd; font-size: 13px; }
141
- .dur-track { background: rgba(255,255,255,.06); border-radius: 4px; height: 18px; overflow: hidden; }
142
- .dur-bar { height: 100%; background: linear-gradient(90deg, #3b82f6, #60a5fa); border-radius: 4px; }
143
- .dur-min { color: #94a3b8; font-size: 13px; text-align: right; }
144
- @media (max-width: 640px) { main { padding: 16px; } .grid { grid-template-columns: 1fr; } .dur-row { grid-template-columns: 100px 1fr 40px; } }
145
- </style>
146
- </head>
147
- <body><main>
148
- <div class="card hero">
149
- <h1>变更最终报告:${esc(data.changeName || '')}</h1>
150
- <p class="muted">${esc(data.businessGoal || '未记录业务目标')}</p>
151
- </div>
152
- <div class="grid">
153
- <div class="card"><div class="muted">🎯 最终状态</div><div class="kpi">${badge(data.finalStatus || data.overallStatus || 'N/A')}</div></div>
154
- <div class="card"><div class="muted">🔒 Final Commit</div><div class="kpi"><code>${esc(data.finalCommit || 'N/A')}</code></div><div class="muted small">${esc(data.finalCommitBranch || '')}</div></div>
155
- <div class="card"><div class="muted">📊 代码变更</div><div class="kpi">${esc(diffStat.filesChanged ?? 'N/A')} 文件</div><div class="muted small"><span class="ins">+${esc(diffStat.insertions ?? 0)}</span> <span class="del">-${esc(diffStat.deletions ?? 0)}</span></div></div>
156
- <div class="card"><div class="muted">⏱️ 总耗时</div><div class="kpi">${esc(totalLabel)}</div><div class="muted small">${esc(durStages.length)} 个阶段</div></div>
157
- <div class="card"><div class="muted">📝 Review</div><div class="kpi">${esc(review.status || 'N/A')}</div><div class="muted small">RED ${esc(review.red ?? 0)}(修${esc(review.redFixed ?? 0)}/确${esc(review.redConfirmed ?? 0)}) · YEL ${esc(review.yellow ?? 0)}(修${esc(review.yellowFixed ?? 0)}/留${esc(review.yellowDeferred ?? 0)})</div></div>
158
- <div class="card"><div class="muted">📦 Archive</div><div class="kpi">${esc(manifest.totalArchiveFiles ?? 'N/A')} 文件</div><div class="muted small">${esc(manifest.checksumStatus || 'N/A')}</div></div>
159
- </div>
160
- <div class="card"><h2>📋 阶段状态</h2><table><tr><th>阶段</th><th>状态</th></tr>${stageRows}</table></div>
161
- ${durCard}
162
- <div class="card"><h2>🛡️ 验证明细</h2><table><tr><th>类型</th><th>结果</th></tr>${verificationRows}</table></div>
163
- <div class="card"><h2>📁 变更文件</h2><table><tr><th>文件</th><th>说明</th><th>+</th><th>-</th></tr>${changedRows}</table></div>
164
- ${artifactCard}
165
- ${uncommittedCard}
166
- <div class="card"><h2>👨‍🔧 给后续维护者</h2><ul>${notes}</ul></div>
167
- <div class="card"><h2>⚠️ 已知风险 / 人工确认项</h2><ul>${risks}</ul><h3>人工后续动作</h3><ul>${manual}</ul></div>
77
+ const html = `<!doctype html><html lang="zh-CN"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
78
+ <title>Harness 最终报告 · ${esc(data.changeName)}</title><style>
79
+ :root{color-scheme:light dark;--bg:#f3f6fa;--card:#fff;--soft:#f7f9fc;--text:#182132;--muted:#667085;--line:#dce3ed;--blue:#2563eb;--good:#087443;--warn:#9b5a00;--bad:#b42318;--shadow:0 8px 24px rgba(18,35,70,.07)}
80
+ @media(prefers-color-scheme:dark){:root{--bg:#0b1018;--card:#121925;--soft:#182130;--text:#eef3fb;--muted:#9aa8bc;--line:#29364a;--blue:#7aa9ff;--good:#55d99d;--warn:#f0bc63;--bad:#ff8b83;--shadow:0 12px 30px rgba(0,0,0,.25)}}
81
+ *{box-sizing:border-box}body{margin:0;background:var(--bg);color:var(--text);font:14px/1.5 Inter,"Segoe UI","Microsoft YaHei",sans-serif}main{width:min(1140px,calc(100% - 28px));margin:22px auto 44px}.hero,.card,.metric,details{background:var(--card);border:1px solid var(--line);border-radius:14px;box-shadow:var(--shadow)}.hero{padding:23px 25px;border-top:3px solid var(--blue)}.eyebrow{color:var(--blue);font-weight:750;letter-spacing:.08em}h1{font-size:27px;line-height:1.2;margin:5px 0 7px;overflow-wrap:anywhere}.goal,.empty,small{color:var(--muted)}.status{display:flex;align-items:center;gap:10px;margin-top:16px}.metrics{display:grid;grid-template-columns:repeat(4,1fr);gap:11px;margin:11px 0}.metric{padding:14px 15px}.metric strong{display:block;font-size:19px;margin:4px 0}.grid{display:grid;grid-template-columns:1.12fr .88fr;gap:11px}.card{padding:17px;margin-bottom:11px}h2{font-size:16px;margin:0 0 10px}.row{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:9px 0;border-bottom:1px solid var(--line)}.row:last-child{border:0}.row small{display:block;margin-top:2px}.pill{display:inline-flex;border-radius:999px;padding:3px 9px;font-size:11px;font-weight:750;white-space:nowrap}.pill.success{color:var(--good);background:color-mix(in srgb,var(--good) 13%,transparent)}.pill.warning{color:var(--warn);background:color-mix(in srgb,var(--warn) 14%,transparent)}.pill.danger{color:var(--bad);background:color-mix(in srgb,var(--bad) 13%,transparent)}.pill.neutral{color:var(--blue);background:color-mix(in srgb,var(--blue) 12%,transparent)}.duration{margin:11px 0}.duration>div{display:flex;justify-content:space-between;color:var(--muted);font-size:12px}.duration>div span:first-child{color:var(--text);font-weight:650}.duration i{display:block;height:7px;background:var(--soft);border-radius:99px;overflow:hidden;margin-top:6px}.duration b{display:block;height:100%;background:linear-gradient(90deg,var(--blue),#7c70ff)}.risk{display:grid;grid-template-columns:1fr 1fr;gap:10px}.risk>div{background:var(--soft);border-radius:10px;padding:11px}.risk h3{font-size:13px;margin:0 0 5px}.risk ul{padding-left:18px;margin:0;color:var(--muted)}details{margin:9px 0}summary{cursor:pointer;padding:13px 15px;font-weight:650}details>div{padding:0 15px 15px;overflow:auto}table{width:100%;border-collapse:collapse}th,td{text-align:left;padding:8px 9px;border-bottom:1px solid var(--line);vertical-align:top}th{color:var(--muted);font-size:11px;letter-spacing:.04em}code{font-family:"Cascadia Code",Consolas,monospace;color:var(--blue);overflow-wrap:anywhere}.plus{color:var(--good)}.minus{color:var(--bad)}dl{display:grid;grid-template-columns:145px 1fr;gap:7px 11px}dt{color:var(--muted)}dd{margin:0;overflow-wrap:anywhere}
82
+ @media(max-width:800px){.metrics{grid-template-columns:1fr 1fr}.grid,.risk{grid-template-columns:1fr}}@media(max-width:480px){main{width:calc(100% - 18px);margin-top:9px}.metrics{grid-template-columns:1fr}.hero{padding:19px}h1{font-size:23px}dl{grid-template-columns:1fr}}
83
+ </style></head><body><main>
84
+ <section class="hero"><div class="eyebrow">HARNESS EXECUTION REPORT</div><h1>${esc(data.changeName || "未命名变更")}</h1><p class="goal">${esc(data.businessGoal || "未记录业务目标")}</p><div class="status">${pill(data.finalStatus)}<span>基于事件、验证账本与 Git 证据生成</span></div></section>
85
+ <section class="metrics"><article class="metric"><small>任务提交</small><strong><code title="${esc(data.finalCommit)}">${esc(shortHash(data.finalCommit))}</code></strong><small>${esc(data.finalCommitBranch || "未记录分支")}</small></article><article class="metric"><small>代码范围</small><strong>${number(diff.filesChanged)} 个文件</strong><small><span class="plus">+${number(diff.insertions)}</span> · <span class="minus">-${number(diff.deletions)}</span></small></article><article class="metric"><small>总耗时</small><strong>${esc(durations.totalLabel || duration(durations.totalMinutes))}</strong><small>${durationStages.length} 个阶段</small></article><article class="metric"><small>归档完整性</small><strong>${esc(record(data.archiveManifest).checksumStatus || "UNKNOWN")}</strong><small>${number(record(data.archiveManifest).totalArchiveFiles)} 个归档文件</small></article></section>
86
+ <section class="grid"><div><article class="card"><h2>验证结论</h2>${verificationHtml}</article><article class="card"><h2>阶段耗时</h2>${durationHtml}</article></div><div><article class="card"><h2>阶段状态</h2>${stageHtml}</article><article class="card"><h2>风险与后续</h2><div class="risk"><div><h3>已知风险</h3><ul>${riskHtml}</ul></div><div><h3>人工动作</h3><ul>${actionHtml}</ul></div></div></article></div></section>
87
+ <details><summary>变更文件(${files.length})</summary><div><table><thead><tr><th>文件</th><th>新增</th><th>删除</th></tr></thead><tbody>${fileRows}</tbody></table></div></details>
88
+ <details><summary>执行时间线与工具交接(${timeline.length})</summary><div><table><thead><tr><th>阶段</th><th>尝试</th><th>状态</th><th>来源 / 摘要</th></tr></thead><tbody>${timelineRows}</tbody></table></div></details>
89
+ <details><summary>命令证据(${commands.length})</summary><div><table><thead><tr><th>阶段</th><th>命令</th><th>结果</th></tr></thead><tbody>${commandRows}</tbody></table></div></details>
90
+ <details><summary>技术元数据</summary><div><dl><dt>完整提交</dt><dd><code>${esc(data.finalCommit || "N/A")}</code></dd><dt>基线提交</dt><dd><code>${esc(data.baseCommit || "N/A")}</code></dd><dt>Git 范围</dt><dd><code>${esc(diff.range || "N/A")}</code></dd><dt>报告数据版本</dt><dd>${esc(data.schemaVersion || "N/A")}</dd><dt>事实来源</dt><dd>${esc(list(record(data.reportPipeline).sources).join(" · ") || "N/A")}</dd></dl></div></details>
168
91
  </main></body></html>`;
169
92
 
170
93
  fs.mkdirSync(path.dirname(outPath), { recursive: true });
171
- fs.writeFileSync(outPath, html, 'utf8');
94
+ fs.writeFileSync(outPath, html, "utf8");
@@ -20,7 +20,7 @@ disallowed-tools:
20
20
  - Bash(awk *)
21
21
  - Bash(curl *)
22
22
  ---
23
- <!-- generated by harness_deploy.py; core=5cadfa54663ecdbf; overlay=none; agent=claude-code; do not edit -->
23
+ <!-- generated by harness_deploy.py; core=e29fb748896b6f88; overlay=none; agent=claude-code; do not edit -->
24
24
  # harness-codebase-map — 代码库地图
25
25
 
26
26
  ## Purpose
@@ -102,7 +102,7 @@ disallowed-tools:
102
102
 
103
103
  ## 执行日志
104
104
 
105
- `events.ndjson` 为唯一事实源(schema_version 2,`note` 承载人类可读摘要);`logs/execution-log.md` 由 `harness_events.py` 渲染,**禁止用 Write/Edit 直接维护**。直接修改的内容会在 `phase.end` 或 finalize 时被完整重建覆盖,属于数据丢失;需要保留的详情必须进入事件 `note`。结构 → [[../protocols/report-pipeline-protocol.md|report-pipeline-protocol]]
105
+ `events.ndjson` 为唯一事实源(schema_version 3,兼容读取 v1/v2;`note` 承载人类可读摘要);`logs/execution-log.md` 由 `harness_events.py` 渲染,**禁止用 Write/Edit 直接维护**。直接修改的内容会在 `phase.end` 或 finalize 时被完整重建覆盖,属于数据丢失;需要保留的详情必须进入事件 `note`。结构 → [[../protocols/report-pipeline-protocol.md|report-pipeline-protocol]]
106
106
 
107
107
  ```powershell
108
108
  python <skills-root>/scripts/harness_events.py append --change-dir ".harness/changes/<change-name>" --phase <phase> --type phase.start --note "<触发指令>"
@@ -111,3 +111,5 @@ python <skills-root>/scripts/harness_events.py append --change-dir ".harness/cha
111
111
  > **脚本接线**:`harness_events.py append`;`harness_archive.py finalize`;`harness_preflight.py check`;`harness_ledger.py can-reuse`;`harness_service.py ensure/stop`(须 `--files`/`serviceStart.inputFiles`)。JSON 输出按 D13 护栏解读。
112
112
 
113
113
  > **Task 4 §6.1 写入契约**:普通 `append` = 加锁 -> 追加一行 -> fsync -> 解锁,**不 load 历史、不渲染**(O(1),跨进程锁 `events.ndjson.lock`,UUID 用完整 `uuid4().hex` 无需去重扫描)。仅 `--type phase.end` append 在追加成功后渲染一次 `execution-log.md`;显式 `harness_events.py render` 随时从完整 events 重建;`harness_archive.py finalize` 在 collect 前强制 render 一次。高频 command append 期间 log 可能滞后,phase 边界保持最新。
114
+
115
+ 每次阶段结束必须写 `--status OK|WARN|FAIL|BLOCKED`;重试同一阶段时增加 `--attempt <n>`,不得覆盖或伪装成一次执行。跨工具继续执行时写 `--executor-tool <codex|claude-code|codebuddy|cursor>`,并在接棒事件写 `--handoff-from-tool` / `--handoff-reason`;也可由 `HUNTER_HARNESS_TOOL/AGENT/MODEL/RUN_ID` 环境变量统一注入。
@@ -20,7 +20,7 @@ disallowed-tools:
20
20
  - Bash(awk *)
21
21
  - Bash(curl *)
22
22
  ---
23
- <!-- generated by harness_deploy.py; core=5cadfa54663ecdbf; overlay=none; agent=claude-code; do not edit -->
23
+ <!-- generated by harness_deploy.py; core=e29fb748896b6f88; 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/` 索引。
@@ -236,7 +236,7 @@ powershell.exe -Command "python '<skill-dir>\scripts\harness_knowledge.py' sync
236
236
  - 当前 `active` / `candidate` / `stale` 数量统计。
237
237
  - 产物路径(`index.json`、`index.sqlite`、`views/`、`reports/`)。
238
238
  - `lifecycle` 摘要:`validatorsApplied`、`candidateAutoPromoted`、`pendingAgentJudge` 等。
239
- - **已处理报告**(auto + judge 后):promoted/dropped/superseded/kept/skipped 计数与 `reports/judge-decisions-*.json`、`judgements-*.json` 路径。
239
+ - **已处理报告**(auto + judge 后):promoted/dropped/superseded/kept/deferred/skipped 计数与 `reports/judge-decisions-*.json`、`judgements-*.json` 路径;人工判断还会追加到 `judgements/decisions.json`,后续 ingest 不得抹除。`defer` 必须带 `reviewAfter`,到期前不重复进入待判断清单。
240
240
  - 仅当 `upToDate=false` 时建议 `sync --update`;查询历史 → `/harness-knowledge-query`。**禁止**默认列出「请人工确认 N 条 candidate」待办。
241
241
 
242
242
  ## 渐进披露
@@ -249,7 +249,7 @@ powershell.exe -Command "python '<skill-dir>\scripts\harness_knowledge.py' sync
249
249
 
250
250
  ## 执行日志
251
251
 
252
- `events.ndjson` 为唯一事实源(schema_version 2,`note` 承载人类可读摘要);`logs/execution-log.md` 由 `harness_events.py` 渲染,**禁止用 Write/Edit 直接维护**。直接修改的内容会在 `phase.end` 或 finalize 时被完整重建覆盖,属于数据丢失;需要保留的详情必须进入事件 `note`。结构 → [[../protocols/report-pipeline-protocol.md|report-pipeline-protocol]]
252
+ `events.ndjson` 为唯一事实源(schema_version 3,兼容读取 v1/v2;`note` 承载人类可读摘要);`logs/execution-log.md` 由 `harness_events.py` 渲染,**禁止用 Write/Edit 直接维护**。直接修改的内容会在 `phase.end` 或 finalize 时被完整重建覆盖,属于数据丢失;需要保留的详情必须进入事件 `note`。结构 → [[../protocols/report-pipeline-protocol.md|report-pipeline-protocol]]
253
253
 
254
254
  ```powershell
255
255
  python <skills-root>/scripts/harness_events.py append --change-dir ".harness/changes/<change-name>" --phase <phase> --type phase.start --note "<触发指令>"
@@ -258,3 +258,5 @@ python <skills-root>/scripts/harness_events.py append --change-dir ".harness/cha
258
258
  > **脚本接线**:`harness_events.py append`;`harness_archive.py finalize`;`harness_preflight.py check`;`harness_ledger.py can-reuse`;`harness_service.py ensure/stop`(须 `--files`/`serviceStart.inputFiles`)。JSON 输出按 D13 护栏解读。
259
259
 
260
260
  > **Task 4 §6.1 写入契约**:普通 `append` = 加锁 -> 追加一行 -> fsync -> 解锁,**不 load 历史、不渲染**(O(1),跨进程锁 `events.ndjson.lock`,UUID 用完整 `uuid4().hex` 无需去重扫描)。仅 `--type phase.end` append 在追加成功后渲染一次 `execution-log.md`;显式 `harness_events.py render` 随时从完整 events 重建;`harness_archive.py finalize` 在 collect 前强制 render 一次。高频 command append 期间 log 可能滞后,phase 边界保持最新。
261
+
262
+ 每次阶段结束必须写 `--status OK|WARN|FAIL|BLOCKED`;重试同一阶段时增加 `--attempt <n>`,不得覆盖或伪装成一次执行。跨工具继续执行时写 `--executor-tool <codex|claude-code|codebuddy|cursor>`,并在接棒事件写 `--handoff-from-tool` / `--handoff-reason`;也可由 `HUNTER_HARNESS_TOOL/AGENT/MODEL/RUN_ID` 环境变量统一注入。
@@ -776,13 +776,14 @@ def write_cached_archive_entries(
776
776
 
777
777
  def load_preserved_entries(knowledge: Path) -> list[dict[str, Any]]:
778
778
  active_entries = load_entries_from_dir(knowledge / "entries" / "active")
779
- demoted_entries = [
779
+ manually_decided_entries = [
780
780
  entry
781
- for status in ["candidate", "stale"]
781
+ for status in ["candidate", "stale", "superseded", "conflicted"]
782
782
  for entry in load_entries_from_dir(knowledge / "entries" / status)
783
783
  if entry.get("lifecycle", {}).get("demotedAt")
784
+ or entry.get("lifecycle", {}).get("judgeAction")
784
785
  ]
785
- return active_entries + demoted_entries
786
+ return active_entries + manually_decided_entries
786
787
 
787
788
 
788
789
  def combine_generated_with_preserved(knowledge: Path, generated: list[dict[str, Any]]) -> list[dict[str, Any]]:
@@ -3367,7 +3368,7 @@ def render_entry_for_context(entry: dict[str, Any], include_stale: bool = False)
3367
3368
  NEAR_DUPLICATE_THRESHOLD = 0.88
3368
3369
  AUTO_SUPERSEDE_SIMILARITY = 0.75
3369
3370
  AUTO_SUPERSEDE_WEAK_SIMILARITY = 0.55
3370
- JUDGE_ACTIONS = {"promote", "drop", "supersede", "keep-conflict"}
3371
+ JUDGE_ACTIONS = {"promote", "drop", "supersede", "keep-conflict", "defer"}
3371
3372
  ENTRY_STATUSES = {"candidate", "active", "stale", "superseded", "conflicted"}
3372
3373
 
3373
3374
 
@@ -3829,6 +3830,8 @@ def judge_export(project: Path) -> dict[str, Any]:
3829
3830
  for entry in entries:
3830
3831
  if entry.get("status") != "conflicted":
3831
3832
  continue
3833
+ if entry.get("lifecycle", {}).get("judgeAction") in {"keep-conflict", "defer"}:
3834
+ continue
3832
3835
  for other_id in entry.get("lifecycle", {}).get("conflictsWith") or []:
3833
3836
  pair = tuple(sorted([entry["id"], str(other_id)]))
3834
3837
  if pair in seen_pairs:
@@ -3878,6 +3881,10 @@ def judge_export(project: Path) -> dict[str, Any]:
3878
3881
  for entry in entries:
3879
3882
  if entry.get("status") != "candidate":
3880
3883
  continue
3884
+ if entry.get("lifecycle", {}).get("judgeAction") == "defer":
3885
+ review_after = str(entry.get("lifecycle", {}).get("reviewAfter") or "")
3886
+ if not review_after or review_after > dt.date.today().isoformat():
3887
+ continue
3881
3888
  promote_candidates.append(
3882
3889
  {
3883
3890
  "kind": "promote-candidate",
@@ -3984,6 +3991,18 @@ def apply_judge_decision(
3984
3991
  relocate_entry_file(knowledge, entry, source_path)
3985
3992
  path_by_id[entry["id"]] = knowledge / "entries" / "conflicted" / entry_filename(entry)
3986
3993
  after_status = "conflicted"
3994
+ elif action == "defer":
3995
+ lifecycle = entry.setdefault("lifecycle", {})
3996
+ lifecycle["judgeAction"] = action
3997
+ lifecycle["judgeReason"] = reason
3998
+ lifecycle["deferredAt"] = now_iso()
3999
+ lifecycle["reviewAfter"] = decision.get("reviewAfter")
4000
+ lifecycle["lastCheckedAt"] = now_iso()
4001
+ relocate_entry_file(knowledge, entry, source_path)
4002
+ path_by_id[entry["id"]] = (
4003
+ knowledge / "entries" / str(entry.get("status") or "candidate") / entry_filename(entry)
4004
+ )
4005
+ after_status = str(entry.get("status") or "candidate")
3987
4006
  else:
3988
4007
  raise ValueError(f"unsupported judge action: {action}")
3989
4008
 
@@ -3997,6 +4016,35 @@ def apply_judge_decision(
3997
4016
  }
3998
4017
 
3999
4018
 
4019
+ def persist_judge_decisions(knowledge: Path, applied: list[dict[str, Any]]) -> Path:
4020
+ """Keep one durable latest decision per entry independently of generated reports."""
4021
+ path = knowledge / "judgements" / "decisions.json"
4022
+ current: dict[str, Any] = {"schemaVersion": 1, "decisions": {}}
4023
+ if path.exists():
4024
+ try:
4025
+ loaded = read_json(path)
4026
+ if isinstance(loaded, dict) and isinstance(loaded.get("decisions"), dict):
4027
+ current = loaded
4028
+ except (OSError, json.JSONDecodeError):
4029
+ pass
4030
+ decisions = dict(current.get("decisions") or {})
4031
+ for record in applied:
4032
+ decisions[str(record["id"])] = {
4033
+ "id": record["id"],
4034
+ "action": record["action"],
4035
+ "reason": record.get("reason") or "",
4036
+ "supersededBy": record.get("supersededBy"),
4037
+ "status": record.get("after", {}).get("status"),
4038
+ "decidedAt": now_iso(),
4039
+ }
4040
+ write_json(path, {
4041
+ "schemaVersion": 1,
4042
+ "updatedAt": now_iso(),
4043
+ "decisions": dict(sorted(decisions.items())),
4044
+ })
4045
+ return path
4046
+
4047
+
4000
4048
  def judge_apply(project: Path, decisions_path: Path, force: bool = False) -> dict[str, Any]:
4001
4049
  project = project.resolve()
4002
4050
  knowledge = project / ".harness" / "knowledge"
@@ -4051,11 +4099,13 @@ def judge_apply(project: Path, decisions_path: Path, force: bool = False) -> dic
4051
4099
  }
4052
4100
  judgement_path = knowledge / "reports" / f"judgements-{timestamp()}.json"
4053
4101
  write_json(judgement_path, judgement)
4102
+ decisions_ledger = persist_judge_decisions(knowledge, applied)
4054
4103
  index = refresh_outputs_from_entry_files(project, knowledge)
4055
4104
  return {
4056
4105
  "project": str(project),
4057
4106
  "generatedAt": judgement["generatedAt"],
4058
4107
  "judgement": str(judgement_path),
4108
+ "decisionsLedger": str(decisions_ledger),
4059
4109
  "applied": len(applied),
4060
4110
  "errors": errors,
4061
4111
  "stats": index.get("stats", {}),
@@ -4262,6 +4312,26 @@ def maintain_knowledge(project: Path, archive_id: str) -> dict[str, Any]:
4262
4312
  }
4263
4313
 
4264
4314
 
4315
+ def drain_maintenance_outbox(project: Path, limit: int = 50) -> dict[str, Any]:
4316
+ """Advance all retryable maintenance items in one bounded invocation."""
4317
+ project = project.resolve()
4318
+ root = _outbox_root(project)
4319
+ all_archive_ids = sorted({
4320
+ path.stem
4321
+ for status in ("pending", "failed")
4322
+ for path in (root / status).glob("*.json")
4323
+ })
4324
+ archive_ids = all_archive_ids[:max(0, limit)]
4325
+ results = [maintain_knowledge(project, archive_id) for archive_id in archive_ids]
4326
+ return {
4327
+ "ok": all(result.get("ok") for result in results),
4328
+ "project": str(project),
4329
+ "processed": len(results),
4330
+ "remaining": max(0, len(all_archive_ids) - len(results)),
4331
+ "results": results,
4332
+ }
4333
+
4334
+
4265
4335
  def main(argv: list[str] | None = None) -> int:
4266
4336
  parser = argparse.ArgumentParser(description="Build and query Harness knowledge indexes.")
4267
4337
  sub = parser.add_subparsers(dest="command", required=True)
@@ -4378,7 +4448,10 @@ def main(argv: list[str] | None = None) -> int:
4378
4448
  help="Single-process maintenance: claim outbox, ingest+dedupe, supersede, reverify, judge-export",
4379
4449
  )
4380
4450
  maintain.add_argument("--project", default=".", help="Project root containing .harness/knowledge")
4381
- maintain.add_argument("--archive-id", required=True, help="Archive id of the outbox item to maintain")
4451
+ maintain_target = maintain.add_mutually_exclusive_group(required=True)
4452
+ maintain_target.add_argument("--archive-id", help="Archive id of the outbox item to maintain")
4453
+ maintain_target.add_argument("--drain", action="store_true", help="Process all pending/failed items once")
4454
+ maintain.add_argument("--limit", type=int, default=50, help="Maximum items processed by --drain")
4382
4455
  maintain.add_argument("--json", action="store_true", help="Emit machine-readable JSON (default behavior)")
4383
4456
 
4384
4457
  args = parser.parse_args(argv)
@@ -4468,7 +4541,11 @@ def main(argv: list[str] | None = None) -> int:
4468
4541
  print(json.dumps(result, ensure_ascii=False, indent=2))
4469
4542
  return 0
4470
4543
  if args.command == "maintain":
4471
- result = maintain_knowledge(Path(args.project), args.archive_id)
4544
+ result = (
4545
+ drain_maintenance_outbox(Path(args.project), args.limit)
4546
+ if args.drain
4547
+ else maintain_knowledge(Path(args.project), args.archive_id)
4548
+ )
4472
4549
  print(json.dumps(result, ensure_ascii=False, indent=2))
4473
4550
  return 0 if result.get("ok") else 1
4474
4551
  return 2
@@ -20,7 +20,7 @@ disallowed-tools:
20
20
  - Bash(awk *)
21
21
  - Bash(curl *)
22
22
  ---
23
- <!-- generated by harness_deploy.py; core=5cadfa54663ecdbf; overlay=none; agent=claude-code; do not edit -->
23
+ <!-- generated by harness_deploy.py; core=e29fb748896b6f88; overlay=none; agent=claude-code; do not edit -->
24
24
  # harness-knowledge-query
25
25
 
26
26
  根据用户的新需求或排查问题,从项目本地 `.harness/knowledge` 中检索历史需求、决策、实现、风险和测试证据,并生成 AI 可读的 context pack。
@@ -147,7 +147,7 @@ powershell.exe -Command "python '<ingest-skill-dir>\scripts\harness_knowledge.py
147
147
 
148
148
  ## 执行日志
149
149
 
150
- `events.ndjson` 为唯一事实源(schema_version 2,`note` 承载人类可读摘要);`logs/execution-log.md` 由 `harness_events.py` 渲染,**禁止用 Write/Edit 直接维护**。直接修改的内容会在 `phase.end` 或 finalize 时被完整重建覆盖,属于数据丢失;需要保留的详情必须进入事件 `note`。结构 → [[../protocols/report-pipeline-protocol.md|report-pipeline-protocol]]
150
+ `events.ndjson` 为唯一事实源(schema_version 3,兼容读取 v1/v2;`note` 承载人类可读摘要);`logs/execution-log.md` 由 `harness_events.py` 渲染,**禁止用 Write/Edit 直接维护**。直接修改的内容会在 `phase.end` 或 finalize 时被完整重建覆盖,属于数据丢失;需要保留的详情必须进入事件 `note`。结构 → [[../protocols/report-pipeline-protocol.md|report-pipeline-protocol]]
151
151
 
152
152
  ```powershell
153
153
  python <skills-root>/scripts/harness_events.py append --change-dir ".harness/changes/<change-name>" --phase <phase> --type phase.start --note "<触发指令>"
@@ -156,3 +156,5 @@ python <skills-root>/scripts/harness_events.py append --change-dir ".harness/cha
156
156
  > **脚本接线**:`harness_events.py append`;`harness_archive.py finalize`;`harness_preflight.py check`;`harness_ledger.py can-reuse`;`harness_service.py ensure/stop`(须 `--files`/`serviceStart.inputFiles`)。JSON 输出按 D13 护栏解读。
157
157
 
158
158
  > **Task 4 §6.1 写入契约**:普通 `append` = 加锁 -> 追加一行 -> fsync -> 解锁,**不 load 历史、不渲染**(O(1),跨进程锁 `events.ndjson.lock`,UUID 用完整 `uuid4().hex` 无需去重扫描)。仅 `--type phase.end` append 在追加成功后渲染一次 `execution-log.md`;显式 `harness_events.py render` 随时从完整 events 重建;`harness_archive.py finalize` 在 collect 前强制 render 一次。高频 command append 期间 log 可能滞后,phase 边界保持最新。
159
+
160
+ 每次阶段结束必须写 `--status OK|WARN|FAIL|BLOCKED`;重试同一阶段时增加 `--attempt <n>`,不得覆盖或伪装成一次执行。跨工具继续执行时写 `--executor-tool <codex|claude-code|codebuddy|cursor>`,并在接棒事件写 `--handoff-from-tool` / `--handoff-reason`;也可由 `HUNTER_HARNESS_TOOL/AGENT/MODEL/RUN_ID` 环境变量统一注入。