hunter-harness 0.1.3 → 0.2.0

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 (566) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +11 -0
  3. package/dist/bin.js +891 -271
  4. package/package.json +1 -1
  5. package/resources/harness/bundles/general/claude-code/.harness-build.json +6 -0
  6. package/resources/harness/{general → bundles/general/claude-code}/agents/harness-reviewer.md +2 -2
  7. package/resources/harness/{general → bundles/general/claude-code}/harness-archive/SKILL.md +1 -1
  8. package/resources/harness/{java → bundles/general/claude-code}/harness-codebase-map/SKILL.md +1 -1
  9. package/resources/harness/{java → bundles/general/claude-code}/harness-knowledge-ingest/SKILL.md +1 -1
  10. package/resources/harness/{general → bundles/general/claude-code}/harness-knowledge-ingest/reference.md +1 -1
  11. package/resources/harness/{general → bundles/general/claude-code}/harness-knowledge-query/SKILL.md +1 -1
  12. package/resources/harness/{general → bundles/general/claude-code}/harness-plan/SKILL.md +1 -1
  13. package/resources/harness/{java → bundles/general/claude-code}/harness-review/SKILL.md +8 -4
  14. package/resources/harness/{general → bundles/general/claude-code}/harness-review/checklist.md +3 -3
  15. package/resources/harness/{general → bundles/general/claude-code}/harness-run/SKILL.md +1 -1
  16. package/resources/harness/{general → bundles/general/claude-code}/harness-run/checklist.md +1 -1
  17. package/resources/harness/{general → bundles/general/claude-code}/harness-run/reference.md +2 -2
  18. package/resources/harness/{general → bundles/general/claude-code}/harness-submit/SKILL.md +1 -1
  19. package/resources/harness/{java → bundles/general/claude-code}/harness-sync/SKILL.md +2 -2
  20. package/resources/harness/{general → bundles/general/claude-code}/harness-sync/reference.md +7 -7
  21. package/resources/harness/{general → bundles/general/claude-code}/harness-test/SKILL.md +1 -1
  22. package/resources/harness/{general → bundles/general/claude-code}/harness-test/checklist.md +1 -1
  23. package/resources/harness/{java → bundles/general/claude-code}/scripts/harness_archive.py +2300 -2300
  24. package/resources/harness/{general → bundles/general/claude-code}/scripts/harness_deploy.py +74 -13
  25. package/resources/harness/{general → bundles/general/claude-code}/scripts/harness_events.py +680 -680
  26. package/resources/harness/{general → bundles/general/claude-code}/scripts/harness_ledger.py +660 -660
  27. package/resources/harness/{java → bundles/general/claude-code}/scripts/harness_preflight.py +765 -765
  28. package/resources/harness/{general → bundles/general/claude-code}/scripts/harness_service.py +1331 -1331
  29. package/resources/harness/bundles/general/codebuddy/.harness-build.json +6 -0
  30. package/resources/harness/bundles/general/codebuddy/agents/harness-evaluator.md +94 -0
  31. package/resources/harness/bundles/general/codebuddy/agents/harness-explorer.md +53 -0
  32. package/resources/harness/bundles/general/codebuddy/agents/harness-reviewer.md +52 -0
  33. package/resources/harness/bundles/general/codebuddy/harness-archive/SKILL.md +198 -0
  34. package/resources/harness/bundles/general/codebuddy/harness-codebase-map/SKILL.md +95 -0
  35. package/resources/harness/bundles/general/codebuddy/harness-knowledge-ingest/SKILL.md +229 -0
  36. package/resources/harness/{java → bundles/general/codebuddy}/harness-knowledge-ingest/reference.md +1 -1
  37. package/resources/harness/bundles/general/codebuddy/harness-knowledge-query/SKILL.md +147 -0
  38. package/resources/harness/bundles/general/codebuddy/harness-plan/SKILL.md +109 -0
  39. package/resources/harness/bundles/general/codebuddy/harness-review/SKILL.md +142 -0
  40. package/resources/harness/{java → bundles/general/codebuddy}/harness-review/checklist.md +3 -3
  41. package/resources/harness/bundles/general/codebuddy/harness-run/SKILL.md +113 -0
  42. package/resources/harness/bundles/general/codebuddy/harness-run/checklist.md +259 -0
  43. package/resources/harness/bundles/general/codebuddy/harness-run/reference.md +834 -0
  44. package/resources/harness/bundles/general/codebuddy/harness-submit/SKILL.md +142 -0
  45. package/resources/harness/bundles/general/codebuddy/harness-sync/SKILL.md +65 -0
  46. package/resources/harness/{java → bundles/general/codebuddy}/harness-sync/reference.md +7 -7
  47. package/resources/harness/bundles/general/codebuddy/harness-test/SKILL.md +160 -0
  48. package/resources/harness/bundles/general/codebuddy/harness-test/checklist.md +319 -0
  49. package/resources/harness/bundles/general/codebuddy/harness-test/reference.md +791 -0
  50. package/resources/harness/{general → bundles/general/codebuddy}/scripts/harness_archive.py +2300 -2300
  51. package/resources/harness/{java → bundles/general/codebuddy}/scripts/harness_deploy.py +74 -13
  52. package/resources/harness/{java → bundles/general/codebuddy}/scripts/harness_events.py +680 -680
  53. package/resources/harness/{java → bundles/general/codebuddy}/scripts/harness_ledger.py +660 -660
  54. package/resources/harness/{general → bundles/general/codebuddy}/scripts/harness_preflight.py +765 -765
  55. package/resources/harness/{java → bundles/general/codebuddy}/scripts/harness_service.py +1331 -1331
  56. package/resources/harness/bundles/general/codex/.harness-build.json +6 -0
  57. package/resources/harness/bundles/general/codex/CONTEXT.md +65 -0
  58. package/resources/harness/bundles/general/codex/README.md +478 -0
  59. package/resources/harness/bundles/general/codex/harness-archive/SKILL.md +198 -0
  60. package/resources/harness/bundles/general/codex/harness-archive/checklist.md +52 -0
  61. package/resources/harness/bundles/general/codex/harness-archive/reference.md +103 -0
  62. package/resources/harness/bundles/general/codex/harness-archive/scripts/gen-manifest.ps1 +31 -0
  63. package/resources/harness/bundles/general/codex/harness-archive/templates/render-summary.mjs +171 -0
  64. package/resources/harness/bundles/general/codex/harness-archive/templates/summary-data-template.json +128 -0
  65. package/resources/harness/bundles/general/codex/harness-codebase-map/SKILL.md +95 -0
  66. package/resources/harness/bundles/general/codex/harness-codebase-map/checklist.md +126 -0
  67. package/resources/harness/bundles/general/codex/harness-codebase-map/reference.md +364 -0
  68. package/resources/harness/bundles/general/codex/harness-codebase-map/templates/ARCHITECTURE.md +57 -0
  69. package/resources/harness/bundles/general/codex/harness-codebase-map/templates/CONCERNS.md +49 -0
  70. package/resources/harness/bundles/general/codex/harness-codebase-map/templates/CONVENTIONS.md +57 -0
  71. package/resources/harness/bundles/general/codex/harness-codebase-map/templates/INTEGRATIONS.md +52 -0
  72. package/resources/harness/bundles/general/codex/harness-codebase-map/templates/STACK.md +55 -0
  73. package/resources/harness/bundles/general/codex/harness-codebase-map/templates/STRUCTURE.md +69 -0
  74. package/resources/harness/bundles/general/codex/harness-codebase-map/templates/TESTING.md +64 -0
  75. package/resources/harness/bundles/general/codex/harness-codebase-map/templates/map-manifest.schema.json +73 -0
  76. package/resources/harness/bundles/general/codex/harness-codebase-map/templates/map-summary.md +64 -0
  77. package/resources/harness/bundles/general/codex/harness-knowledge-ingest/SKILL.md +229 -0
  78. package/resources/harness/bundles/general/codex/harness-knowledge-ingest/design.md +842 -0
  79. package/resources/harness/bundles/general/codex/harness-knowledge-ingest/evaluations/harness_knowledge_evaluation.xml +42 -0
  80. package/resources/harness/bundles/general/codex/harness-knowledge-ingest/mcp-config.example.json +10 -0
  81. package/resources/harness/bundles/general/codex/harness-knowledge-ingest/reference.md +309 -0
  82. package/resources/harness/bundles/general/codex/harness-knowledge-ingest/scripts/harness_knowledge.py +4082 -0
  83. package/resources/harness/bundles/general/codex/harness-knowledge-ingest/scripts/harness_knowledge_mcp.py +273 -0
  84. package/resources/harness/bundles/general/codex/harness-knowledge-query/SKILL.md +147 -0
  85. package/resources/harness/bundles/general/codex/harness-plan/SKILL.md +109 -0
  86. package/resources/harness/bundles/general/codex/harness-plan/checklist.md +243 -0
  87. package/resources/harness/bundles/general/codex/harness-plan/protocols.md +124 -0
  88. package/resources/harness/bundles/general/codex/harness-plan/reference.md +376 -0
  89. package/resources/harness/bundles/general/codex/harness-review/SKILL.md +137 -0
  90. package/resources/harness/bundles/general/codex/harness-review/checklist.md +124 -0
  91. package/resources/harness/bundles/general/codex/harness-review/protocols.md +68 -0
  92. package/resources/harness/bundles/general/codex/harness-review/reference.md +86 -0
  93. package/resources/harness/bundles/general/codex/harness-run/SKILL.md +113 -0
  94. package/resources/harness/bundles/general/codex/harness-run/checklist.md +259 -0
  95. package/resources/harness/bundles/general/codex/harness-run/protocols.md +78 -0
  96. package/resources/harness/bundles/general/codex/harness-run/reference.md +834 -0
  97. package/resources/harness/bundles/general/codex/harness-submit/SKILL.md +142 -0
  98. package/resources/harness/bundles/general/codex/harness-submit/checklist.md +407 -0
  99. package/resources/harness/bundles/general/codex/harness-submit/reference.md +152 -0
  100. package/resources/harness/bundles/general/codex/harness-sync/SKILL.md +65 -0
  101. package/resources/harness/bundles/general/codex/harness-sync/reference.md +153 -0
  102. package/resources/harness/bundles/general/codex/harness-test/SKILL.md +160 -0
  103. package/resources/harness/bundles/general/codex/harness-test/checklist.md +319 -0
  104. package/resources/harness/bundles/general/codex/harness-test/pitfalls.md +260 -0
  105. package/resources/harness/bundles/general/codex/harness-test/reference.md +791 -0
  106. package/resources/harness/bundles/general/codex/protocols/archive-report-protocol.md +175 -0
  107. package/resources/harness/bundles/general/codex/protocols/evidence-based-reporting-protocol.md +134 -0
  108. package/resources/harness/bundles/general/codex/protocols/ledger-protocol.md +270 -0
  109. package/resources/harness/bundles/general/codex/protocols/powershell-protocol.md +181 -0
  110. package/resources/harness/bundles/general/codex/protocols/report-pipeline-protocol.md +150 -0
  111. package/resources/harness/bundles/general/codex/protocols/sensitive-info-protocol.md +92 -0
  112. package/resources/harness/bundles/general/codex/protocols/state-layout-protocol.md +131 -0
  113. package/resources/harness/bundles/general/codex/protocols/submit-protocol.md +78 -0
  114. package/resources/harness/bundles/general/codex/scripts/harness_archive.py +2319 -0
  115. package/resources/harness/bundles/general/codex/scripts/harness_deploy.py +674 -0
  116. package/resources/harness/bundles/general/codex/scripts/harness_events.py +680 -0
  117. package/resources/harness/bundles/general/codex/scripts/harness_ledger.py +671 -0
  118. package/resources/harness/bundles/general/codex/scripts/harness_preflight.py +780 -0
  119. package/resources/harness/bundles/general/codex/scripts/harness_service.py +1331 -0
  120. package/resources/harness/bundles/general/cursor/.harness-build.json +6 -0
  121. package/resources/harness/bundles/general/cursor/CONTEXT.md +65 -0
  122. package/resources/harness/bundles/general/cursor/README.md +478 -0
  123. package/resources/harness/bundles/general/cursor/harness-archive/SKILL.md +198 -0
  124. package/resources/harness/bundles/general/cursor/harness-archive/checklist.md +52 -0
  125. package/resources/harness/bundles/general/cursor/harness-archive/reference.md +103 -0
  126. package/resources/harness/bundles/general/cursor/harness-archive/scripts/gen-manifest.ps1 +31 -0
  127. package/resources/harness/bundles/general/cursor/harness-archive/templates/render-summary.mjs +171 -0
  128. package/resources/harness/bundles/general/cursor/harness-archive/templates/summary-data-template.json +128 -0
  129. package/resources/harness/bundles/general/cursor/harness-codebase-map/SKILL.md +95 -0
  130. package/resources/harness/bundles/general/cursor/harness-codebase-map/checklist.md +126 -0
  131. package/resources/harness/bundles/general/cursor/harness-codebase-map/reference.md +364 -0
  132. package/resources/harness/bundles/general/cursor/harness-codebase-map/templates/ARCHITECTURE.md +57 -0
  133. package/resources/harness/bundles/general/cursor/harness-codebase-map/templates/CONCERNS.md +49 -0
  134. package/resources/harness/bundles/general/cursor/harness-codebase-map/templates/CONVENTIONS.md +57 -0
  135. package/resources/harness/bundles/general/cursor/harness-codebase-map/templates/INTEGRATIONS.md +52 -0
  136. package/resources/harness/bundles/general/cursor/harness-codebase-map/templates/STACK.md +55 -0
  137. package/resources/harness/bundles/general/cursor/harness-codebase-map/templates/STRUCTURE.md +69 -0
  138. package/resources/harness/bundles/general/cursor/harness-codebase-map/templates/TESTING.md +64 -0
  139. package/resources/harness/bundles/general/cursor/harness-codebase-map/templates/map-manifest.schema.json +73 -0
  140. package/resources/harness/bundles/general/cursor/harness-codebase-map/templates/map-summary.md +64 -0
  141. package/resources/harness/bundles/general/cursor/harness-knowledge-ingest/SKILL.md +229 -0
  142. package/resources/harness/bundles/general/cursor/harness-knowledge-ingest/design.md +842 -0
  143. package/resources/harness/bundles/general/cursor/harness-knowledge-ingest/evaluations/harness_knowledge_evaluation.xml +42 -0
  144. package/resources/harness/bundles/general/cursor/harness-knowledge-ingest/mcp-config.example.json +10 -0
  145. package/resources/harness/bundles/general/cursor/harness-knowledge-ingest/reference.md +309 -0
  146. package/resources/harness/bundles/general/cursor/harness-knowledge-ingest/scripts/harness_knowledge.py +4082 -0
  147. package/resources/harness/bundles/general/cursor/harness-knowledge-ingest/scripts/harness_knowledge_mcp.py +273 -0
  148. package/resources/harness/bundles/general/cursor/harness-knowledge-query/SKILL.md +147 -0
  149. package/resources/harness/bundles/general/cursor/harness-plan/SKILL.md +109 -0
  150. package/resources/harness/bundles/general/cursor/harness-plan/checklist.md +243 -0
  151. package/resources/harness/bundles/general/cursor/harness-plan/protocols.md +124 -0
  152. package/resources/harness/bundles/general/cursor/harness-plan/reference.md +376 -0
  153. package/resources/harness/bundles/general/cursor/harness-review/SKILL.md +137 -0
  154. package/resources/harness/bundles/general/cursor/harness-review/checklist.md +124 -0
  155. package/resources/harness/bundles/general/cursor/harness-review/protocols.md +68 -0
  156. package/resources/harness/bundles/general/cursor/harness-review/reference.md +86 -0
  157. package/resources/harness/bundles/general/cursor/harness-run/SKILL.md +113 -0
  158. package/resources/harness/bundles/general/cursor/harness-run/checklist.md +259 -0
  159. package/resources/harness/bundles/general/cursor/harness-run/protocols.md +78 -0
  160. package/resources/harness/bundles/general/cursor/harness-run/reference.md +834 -0
  161. package/resources/harness/bundles/general/cursor/harness-submit/SKILL.md +142 -0
  162. package/resources/harness/bundles/general/cursor/harness-submit/checklist.md +407 -0
  163. package/resources/harness/bundles/general/cursor/harness-submit/reference.md +152 -0
  164. package/resources/harness/bundles/general/cursor/harness-sync/SKILL.md +65 -0
  165. package/resources/harness/bundles/general/cursor/harness-sync/reference.md +153 -0
  166. package/resources/harness/bundles/general/cursor/harness-test/SKILL.md +160 -0
  167. package/resources/harness/bundles/general/cursor/harness-test/checklist.md +319 -0
  168. package/resources/harness/bundles/general/cursor/harness-test/pitfalls.md +260 -0
  169. package/resources/harness/bundles/general/cursor/harness-test/reference.md +791 -0
  170. package/resources/harness/bundles/general/cursor/protocols/archive-report-protocol.md +175 -0
  171. package/resources/harness/bundles/general/cursor/protocols/evidence-based-reporting-protocol.md +134 -0
  172. package/resources/harness/bundles/general/cursor/protocols/ledger-protocol.md +270 -0
  173. package/resources/harness/bundles/general/cursor/protocols/powershell-protocol.md +181 -0
  174. package/resources/harness/bundles/general/cursor/protocols/report-pipeline-protocol.md +150 -0
  175. package/resources/harness/bundles/general/cursor/protocols/sensitive-info-protocol.md +92 -0
  176. package/resources/harness/bundles/general/cursor/protocols/state-layout-protocol.md +131 -0
  177. package/resources/harness/bundles/general/cursor/protocols/submit-protocol.md +78 -0
  178. package/resources/harness/bundles/general/cursor/scripts/harness_archive.py +2319 -0
  179. package/resources/harness/bundles/general/cursor/scripts/harness_deploy.py +674 -0
  180. package/resources/harness/bundles/general/cursor/scripts/harness_events.py +680 -0
  181. package/resources/harness/bundles/general/cursor/scripts/harness_ledger.py +671 -0
  182. package/resources/harness/bundles/general/cursor/scripts/harness_preflight.py +780 -0
  183. package/resources/harness/bundles/general/cursor/scripts/harness_service.py +1331 -0
  184. package/resources/harness/bundles/java/claude-code/.harness-build.json +6 -0
  185. package/resources/harness/bundles/java/claude-code/CONTEXT.md +65 -0
  186. package/resources/harness/bundles/java/claude-code/README.md +478 -0
  187. package/resources/harness/{java → bundles/java/claude-code}/agents/harness-reviewer.md +2 -2
  188. package/resources/harness/{java → bundles/java/claude-code}/harness-apidoc/SKILL.md +1 -1
  189. package/resources/harness/{java → bundles/java/claude-code}/harness-archive/SKILL.md +1 -1
  190. package/resources/harness/bundles/java/claude-code/harness-archive/checklist.md +52 -0
  191. package/resources/harness/bundles/java/claude-code/harness-archive/reference.md +103 -0
  192. package/resources/harness/bundles/java/claude-code/harness-archive/scripts/gen-manifest.ps1 +31 -0
  193. package/resources/harness/bundles/java/claude-code/harness-archive/templates/render-summary.mjs +171 -0
  194. package/resources/harness/bundles/java/claude-code/harness-archive/templates/summary-data-template.json +128 -0
  195. package/resources/harness/{general → bundles/java/claude-code}/harness-codebase-map/SKILL.md +1 -1
  196. package/resources/harness/bundles/java/claude-code/harness-codebase-map/checklist.md +126 -0
  197. package/resources/harness/bundles/java/claude-code/harness-codebase-map/reference.md +364 -0
  198. package/resources/harness/bundles/java/claude-code/harness-codebase-map/templates/ARCHITECTURE.md +57 -0
  199. package/resources/harness/bundles/java/claude-code/harness-codebase-map/templates/CONCERNS.md +49 -0
  200. package/resources/harness/bundles/java/claude-code/harness-codebase-map/templates/CONVENTIONS.md +57 -0
  201. package/resources/harness/bundles/java/claude-code/harness-codebase-map/templates/INTEGRATIONS.md +52 -0
  202. package/resources/harness/bundles/java/claude-code/harness-codebase-map/templates/STACK.md +55 -0
  203. package/resources/harness/bundles/java/claude-code/harness-codebase-map/templates/STRUCTURE.md +69 -0
  204. package/resources/harness/bundles/java/claude-code/harness-codebase-map/templates/TESTING.md +64 -0
  205. package/resources/harness/bundles/java/claude-code/harness-codebase-map/templates/map-manifest.schema.json +73 -0
  206. package/resources/harness/bundles/java/claude-code/harness-codebase-map/templates/map-summary.md +64 -0
  207. package/resources/harness/{general → bundles/java/claude-code}/harness-knowledge-ingest/SKILL.md +1 -1
  208. package/resources/harness/bundles/java/claude-code/harness-knowledge-ingest/design.md +842 -0
  209. package/resources/harness/bundles/java/claude-code/harness-knowledge-ingest/evaluations/harness_knowledge_evaluation.xml +42 -0
  210. package/resources/harness/bundles/java/claude-code/harness-knowledge-ingest/mcp-config.example.json +10 -0
  211. package/resources/harness/bundles/java/claude-code/harness-knowledge-ingest/reference.md +309 -0
  212. package/resources/harness/bundles/java/claude-code/harness-knowledge-ingest/scripts/harness_knowledge.py +4082 -0
  213. package/resources/harness/bundles/java/claude-code/harness-knowledge-ingest/scripts/harness_knowledge_mcp.py +273 -0
  214. package/resources/harness/{java → bundles/java/claude-code}/harness-knowledge-query/SKILL.md +1 -1
  215. package/resources/harness/{java → bundles/java/claude-code}/harness-package/SKILL.md +1 -1
  216. package/resources/harness/{java → bundles/java/claude-code}/harness-package/reference.md +1 -1
  217. package/resources/harness/{java → bundles/java/claude-code}/harness-plan/SKILL.md +1 -1
  218. package/resources/harness/bundles/java/claude-code/harness-plan/checklist.md +243 -0
  219. package/resources/harness/bundles/java/claude-code/harness-plan/protocols.md +124 -0
  220. package/resources/harness/bundles/java/claude-code/harness-plan/reference.md +376 -0
  221. package/resources/harness/{general → bundles/java/claude-code}/harness-review/SKILL.md +8 -4
  222. package/resources/harness/bundles/java/claude-code/harness-review/checklist.md +124 -0
  223. package/resources/harness/bundles/java/claude-code/harness-review/protocols.md +68 -0
  224. package/resources/harness/bundles/java/claude-code/harness-review/reference.md +86 -0
  225. package/resources/harness/{java → bundles/java/claude-code}/harness-run/SKILL.md +1 -1
  226. package/resources/harness/{java → bundles/java/claude-code}/harness-run/checklist.md +1 -1
  227. package/resources/harness/bundles/java/claude-code/harness-run/protocols.md +78 -0
  228. package/resources/harness/{java → bundles/java/claude-code}/harness-run/reference.md +2 -2
  229. package/resources/harness/{java → bundles/java/claude-code}/harness-submit/SKILL.md +1 -1
  230. package/resources/harness/bundles/java/claude-code/harness-submit/checklist.md +407 -0
  231. package/resources/harness/bundles/java/claude-code/harness-submit/reference.md +152 -0
  232. package/resources/harness/{general → bundles/java/claude-code}/harness-sync/SKILL.md +2 -2
  233. package/resources/harness/bundles/java/claude-code/harness-sync/reference.md +153 -0
  234. package/resources/harness/{java → bundles/java/claude-code}/harness-test/SKILL.md +1 -1
  235. package/resources/harness/{java → bundles/java/claude-code}/harness-test/checklist.md +316 -316
  236. package/resources/harness/bundles/java/claude-code/harness-test/pitfalls.md +260 -0
  237. package/resources/harness/{java → bundles/java/claude-code}/harness-test/reference.md +788 -788
  238. package/resources/harness/bundles/java/claude-code/protocols/archive-report-protocol.md +175 -0
  239. package/resources/harness/bundles/java/claude-code/protocols/evidence-based-reporting-protocol.md +134 -0
  240. package/resources/harness/bundles/java/claude-code/protocols/ledger-protocol.md +270 -0
  241. package/resources/harness/bundles/java/claude-code/protocols/powershell-protocol.md +181 -0
  242. package/resources/harness/bundles/java/claude-code/protocols/report-pipeline-protocol.md +150 -0
  243. package/resources/harness/bundles/java/claude-code/protocols/sensitive-info-protocol.md +92 -0
  244. package/resources/harness/bundles/java/claude-code/protocols/state-layout-protocol.md +131 -0
  245. package/resources/harness/bundles/java/claude-code/protocols/submit-protocol.md +78 -0
  246. package/resources/harness/bundles/java/claude-code/scripts/harness_archive.py +2319 -0
  247. package/resources/harness/bundles/java/claude-code/scripts/harness_deploy.py +674 -0
  248. package/resources/harness/bundles/java/claude-code/scripts/harness_events.py +680 -0
  249. package/resources/harness/bundles/java/claude-code/scripts/harness_ledger.py +671 -0
  250. package/resources/harness/bundles/java/claude-code/scripts/harness_preflight.py +780 -0
  251. package/resources/harness/bundles/java/claude-code/scripts/harness_service.py +1331 -0
  252. package/resources/harness/bundles/java/codebuddy/.harness-build.json +6 -0
  253. package/resources/harness/bundles/java/codebuddy/CONTEXT.md +65 -0
  254. package/resources/harness/bundles/java/codebuddy/README.md +478 -0
  255. package/resources/harness/bundles/java/codebuddy/agents/harness-evaluator.md +94 -0
  256. package/resources/harness/bundles/java/codebuddy/agents/harness-explorer.md +53 -0
  257. package/resources/harness/bundles/java/codebuddy/agents/harness-reviewer.md +52 -0
  258. package/resources/harness/bundles/java/codebuddy/harness-apidoc/SKILL.md +68 -0
  259. package/resources/harness/bundles/java/codebuddy/harness-apidoc/checklist.md +142 -0
  260. package/resources/harness/bundles/java/codebuddy/harness-apidoc/reference.md +233 -0
  261. package/resources/harness/bundles/java/codebuddy/harness-archive/SKILL.md +198 -0
  262. package/resources/harness/bundles/java/codebuddy/harness-archive/checklist.md +52 -0
  263. package/resources/harness/bundles/java/codebuddy/harness-archive/reference.md +103 -0
  264. package/resources/harness/bundles/java/codebuddy/harness-archive/scripts/gen-manifest.ps1 +31 -0
  265. package/resources/harness/bundles/java/codebuddy/harness-archive/templates/render-summary.mjs +171 -0
  266. package/resources/harness/bundles/java/codebuddy/harness-archive/templates/summary-data-template.json +128 -0
  267. package/resources/harness/bundles/java/codebuddy/harness-codebase-map/SKILL.md +95 -0
  268. package/resources/harness/bundles/java/codebuddy/harness-codebase-map/checklist.md +126 -0
  269. package/resources/harness/bundles/java/codebuddy/harness-codebase-map/reference.md +364 -0
  270. package/resources/harness/bundles/java/codebuddy/harness-codebase-map/templates/ARCHITECTURE.md +57 -0
  271. package/resources/harness/bundles/java/codebuddy/harness-codebase-map/templates/CONCERNS.md +49 -0
  272. package/resources/harness/bundles/java/codebuddy/harness-codebase-map/templates/CONVENTIONS.md +57 -0
  273. package/resources/harness/bundles/java/codebuddy/harness-codebase-map/templates/INTEGRATIONS.md +52 -0
  274. package/resources/harness/bundles/java/codebuddy/harness-codebase-map/templates/STACK.md +55 -0
  275. package/resources/harness/bundles/java/codebuddy/harness-codebase-map/templates/STRUCTURE.md +69 -0
  276. package/resources/harness/bundles/java/codebuddy/harness-codebase-map/templates/TESTING.md +64 -0
  277. package/resources/harness/bundles/java/codebuddy/harness-codebase-map/templates/map-manifest.schema.json +73 -0
  278. package/resources/harness/bundles/java/codebuddy/harness-codebase-map/templates/map-summary.md +64 -0
  279. package/resources/harness/bundles/java/codebuddy/harness-knowledge-ingest/SKILL.md +229 -0
  280. package/resources/harness/bundles/java/codebuddy/harness-knowledge-ingest/design.md +842 -0
  281. package/resources/harness/bundles/java/codebuddy/harness-knowledge-ingest/evaluations/harness_knowledge_evaluation.xml +42 -0
  282. package/resources/harness/bundles/java/codebuddy/harness-knowledge-ingest/mcp-config.example.json +10 -0
  283. package/resources/harness/bundles/java/codebuddy/harness-knowledge-ingest/reference.md +309 -0
  284. package/resources/harness/bundles/java/codebuddy/harness-knowledge-ingest/scripts/harness_knowledge.py +4082 -0
  285. package/resources/harness/bundles/java/codebuddy/harness-knowledge-ingest/scripts/harness_knowledge_mcp.py +273 -0
  286. package/resources/harness/bundles/java/codebuddy/harness-knowledge-query/SKILL.md +147 -0
  287. package/resources/harness/bundles/java/codebuddy/harness-package/SKILL.md +69 -0
  288. package/resources/harness/bundles/java/codebuddy/harness-package/checklist.md +322 -0
  289. package/resources/harness/bundles/java/codebuddy/harness-package/reference.md +210 -0
  290. package/resources/harness/bundles/java/codebuddy/harness-plan/SKILL.md +109 -0
  291. package/resources/harness/bundles/java/codebuddy/harness-plan/checklist.md +243 -0
  292. package/resources/harness/bundles/java/codebuddy/harness-plan/protocols.md +124 -0
  293. package/resources/harness/bundles/java/codebuddy/harness-plan/reference.md +376 -0
  294. package/resources/harness/bundles/java/codebuddy/harness-review/SKILL.md +142 -0
  295. package/resources/harness/bundles/java/codebuddy/harness-review/checklist.md +124 -0
  296. package/resources/harness/bundles/java/codebuddy/harness-review/protocols.md +68 -0
  297. package/resources/harness/bundles/java/codebuddy/harness-review/reference.md +86 -0
  298. package/resources/harness/bundles/java/codebuddy/harness-run/SKILL.md +129 -0
  299. package/resources/harness/bundles/java/codebuddy/harness-run/checklist.md +250 -0
  300. package/resources/harness/bundles/java/codebuddy/harness-run/protocols.md +78 -0
  301. package/resources/harness/bundles/java/codebuddy/harness-run/reference.md +814 -0
  302. package/resources/harness/bundles/java/codebuddy/harness-submit/SKILL.md +149 -0
  303. package/resources/harness/bundles/java/codebuddy/harness-submit/checklist.md +407 -0
  304. package/resources/harness/bundles/java/codebuddy/harness-submit/reference.md +152 -0
  305. package/resources/harness/bundles/java/codebuddy/harness-sync/SKILL.md +65 -0
  306. package/resources/harness/bundles/java/codebuddy/harness-sync/reference.md +153 -0
  307. package/resources/harness/bundles/java/codebuddy/harness-test/SKILL.md +172 -0
  308. package/resources/harness/bundles/java/codebuddy/harness-test/checklist.md +317 -0
  309. package/resources/harness/bundles/java/codebuddy/harness-test/pitfalls-java.md +16 -0
  310. package/resources/harness/bundles/java/codebuddy/harness-test/pitfalls.md +260 -0
  311. package/resources/harness/bundles/java/codebuddy/harness-test/reference.md +788 -0
  312. package/resources/harness/bundles/java/codebuddy/protocols/archive-report-protocol.md +175 -0
  313. package/resources/harness/bundles/java/codebuddy/protocols/evidence-based-reporting-protocol.md +134 -0
  314. package/resources/harness/bundles/java/codebuddy/protocols/ledger-protocol.md +270 -0
  315. package/resources/harness/bundles/java/codebuddy/protocols/powershell-protocol.md +181 -0
  316. package/resources/harness/bundles/java/codebuddy/protocols/report-pipeline-protocol.md +150 -0
  317. package/resources/harness/bundles/java/codebuddy/protocols/sensitive-info-protocol.md +92 -0
  318. package/resources/harness/bundles/java/codebuddy/protocols/state-layout-protocol.md +131 -0
  319. package/resources/harness/bundles/java/codebuddy/protocols/submit-protocol.md +78 -0
  320. package/resources/harness/bundles/java/codebuddy/scripts/harness_archive.py +2319 -0
  321. package/resources/harness/bundles/java/codebuddy/scripts/harness_deploy.py +674 -0
  322. package/resources/harness/bundles/java/codebuddy/scripts/harness_events.py +680 -0
  323. package/resources/harness/bundles/java/codebuddy/scripts/harness_ledger.py +671 -0
  324. package/resources/harness/bundles/java/codebuddy/scripts/harness_preflight.py +780 -0
  325. package/resources/harness/bundles/java/codebuddy/scripts/harness_service.py +1331 -0
  326. package/resources/harness/bundles/java/codex/.harness-build.json +6 -0
  327. package/resources/harness/bundles/java/codex/CONTEXT.md +65 -0
  328. package/resources/harness/bundles/java/codex/README.md +478 -0
  329. package/resources/harness/bundles/java/codex/harness-apidoc/SKILL.md +68 -0
  330. package/resources/harness/bundles/java/codex/harness-apidoc/checklist.md +142 -0
  331. package/resources/harness/bundles/java/codex/harness-apidoc/reference.md +233 -0
  332. package/resources/harness/bundles/java/codex/harness-archive/SKILL.md +198 -0
  333. package/resources/harness/bundles/java/codex/harness-archive/checklist.md +52 -0
  334. package/resources/harness/bundles/java/codex/harness-archive/reference.md +103 -0
  335. package/resources/harness/bundles/java/codex/harness-archive/scripts/gen-manifest.ps1 +31 -0
  336. package/resources/harness/bundles/java/codex/harness-archive/templates/render-summary.mjs +171 -0
  337. package/resources/harness/bundles/java/codex/harness-archive/templates/summary-data-template.json +128 -0
  338. package/resources/harness/bundles/java/codex/harness-codebase-map/SKILL.md +95 -0
  339. package/resources/harness/bundles/java/codex/harness-codebase-map/checklist.md +126 -0
  340. package/resources/harness/bundles/java/codex/harness-codebase-map/reference.md +364 -0
  341. package/resources/harness/bundles/java/codex/harness-codebase-map/templates/ARCHITECTURE.md +57 -0
  342. package/resources/harness/bundles/java/codex/harness-codebase-map/templates/CONCERNS.md +49 -0
  343. package/resources/harness/bundles/java/codex/harness-codebase-map/templates/CONVENTIONS.md +57 -0
  344. package/resources/harness/bundles/java/codex/harness-codebase-map/templates/INTEGRATIONS.md +52 -0
  345. package/resources/harness/bundles/java/codex/harness-codebase-map/templates/STACK.md +55 -0
  346. package/resources/harness/bundles/java/codex/harness-codebase-map/templates/STRUCTURE.md +69 -0
  347. package/resources/harness/bundles/java/codex/harness-codebase-map/templates/TESTING.md +64 -0
  348. package/resources/harness/bundles/java/codex/harness-codebase-map/templates/map-manifest.schema.json +73 -0
  349. package/resources/harness/bundles/java/codex/harness-codebase-map/templates/map-summary.md +64 -0
  350. package/resources/harness/bundles/java/codex/harness-knowledge-ingest/SKILL.md +229 -0
  351. package/resources/harness/bundles/java/codex/harness-knowledge-ingest/design.md +842 -0
  352. package/resources/harness/bundles/java/codex/harness-knowledge-ingest/evaluations/harness_knowledge_evaluation.xml +42 -0
  353. package/resources/harness/bundles/java/codex/harness-knowledge-ingest/mcp-config.example.json +10 -0
  354. package/resources/harness/bundles/java/codex/harness-knowledge-ingest/reference.md +309 -0
  355. package/resources/harness/bundles/java/codex/harness-knowledge-ingest/scripts/harness_knowledge.py +4082 -0
  356. package/resources/harness/bundles/java/codex/harness-knowledge-ingest/scripts/harness_knowledge_mcp.py +273 -0
  357. package/resources/harness/bundles/java/codex/harness-knowledge-query/SKILL.md +147 -0
  358. package/resources/harness/bundles/java/codex/harness-package/SKILL.md +69 -0
  359. package/resources/harness/bundles/java/codex/harness-package/checklist.md +322 -0
  360. package/resources/harness/bundles/java/codex/harness-package/reference.md +210 -0
  361. package/resources/harness/bundles/java/codex/harness-plan/SKILL.md +109 -0
  362. package/resources/harness/bundles/java/codex/harness-plan/checklist.md +243 -0
  363. package/resources/harness/bundles/java/codex/harness-plan/protocols.md +124 -0
  364. package/resources/harness/bundles/java/codex/harness-plan/reference.md +376 -0
  365. package/resources/harness/bundles/java/codex/harness-review/SKILL.md +137 -0
  366. package/resources/harness/bundles/java/codex/harness-review/checklist.md +124 -0
  367. package/resources/harness/bundles/java/codex/harness-review/protocols.md +68 -0
  368. package/resources/harness/bundles/java/codex/harness-review/reference.md +86 -0
  369. package/resources/harness/bundles/java/codex/harness-run/SKILL.md +129 -0
  370. package/resources/harness/bundles/java/codex/harness-run/checklist.md +250 -0
  371. package/resources/harness/bundles/java/codex/harness-run/protocols.md +78 -0
  372. package/resources/harness/bundles/java/codex/harness-run/reference.md +814 -0
  373. package/resources/harness/bundles/java/codex/harness-submit/SKILL.md +149 -0
  374. package/resources/harness/bundles/java/codex/harness-submit/checklist.md +407 -0
  375. package/resources/harness/bundles/java/codex/harness-submit/reference.md +152 -0
  376. package/resources/harness/bundles/java/codex/harness-sync/SKILL.md +65 -0
  377. package/resources/harness/bundles/java/codex/harness-sync/reference.md +153 -0
  378. package/resources/harness/bundles/java/codex/harness-test/SKILL.md +172 -0
  379. package/resources/harness/bundles/java/codex/harness-test/checklist.md +317 -0
  380. package/resources/harness/bundles/java/codex/harness-test/pitfalls-java.md +16 -0
  381. package/resources/harness/bundles/java/codex/harness-test/pitfalls.md +260 -0
  382. package/resources/harness/bundles/java/codex/harness-test/reference.md +788 -0
  383. package/resources/harness/bundles/java/codex/protocols/archive-report-protocol.md +175 -0
  384. package/resources/harness/bundles/java/codex/protocols/evidence-based-reporting-protocol.md +134 -0
  385. package/resources/harness/bundles/java/codex/protocols/ledger-protocol.md +270 -0
  386. package/resources/harness/bundles/java/codex/protocols/powershell-protocol.md +181 -0
  387. package/resources/harness/bundles/java/codex/protocols/report-pipeline-protocol.md +150 -0
  388. package/resources/harness/bundles/java/codex/protocols/sensitive-info-protocol.md +92 -0
  389. package/resources/harness/bundles/java/codex/protocols/state-layout-protocol.md +131 -0
  390. package/resources/harness/bundles/java/codex/protocols/submit-protocol.md +78 -0
  391. package/resources/harness/bundles/java/codex/scripts/harness_archive.py +2319 -0
  392. package/resources/harness/bundles/java/codex/scripts/harness_deploy.py +674 -0
  393. package/resources/harness/bundles/java/codex/scripts/harness_events.py +680 -0
  394. package/resources/harness/bundles/java/codex/scripts/harness_ledger.py +671 -0
  395. package/resources/harness/bundles/java/codex/scripts/harness_preflight.py +780 -0
  396. package/resources/harness/bundles/java/codex/scripts/harness_service.py +1331 -0
  397. package/resources/harness/bundles/java/cursor/.harness-build.json +6 -0
  398. package/resources/harness/bundles/java/cursor/CONTEXT.md +65 -0
  399. package/resources/harness/bundles/java/cursor/README.md +478 -0
  400. package/resources/harness/bundles/java/cursor/harness-apidoc/SKILL.md +68 -0
  401. package/resources/harness/bundles/java/cursor/harness-apidoc/checklist.md +142 -0
  402. package/resources/harness/bundles/java/cursor/harness-apidoc/reference.md +233 -0
  403. package/resources/harness/bundles/java/cursor/harness-archive/SKILL.md +198 -0
  404. package/resources/harness/bundles/java/cursor/harness-archive/checklist.md +52 -0
  405. package/resources/harness/bundles/java/cursor/harness-archive/reference.md +103 -0
  406. package/resources/harness/bundles/java/cursor/harness-archive/scripts/gen-manifest.ps1 +31 -0
  407. package/resources/harness/bundles/java/cursor/harness-archive/templates/render-summary.mjs +171 -0
  408. package/resources/harness/bundles/java/cursor/harness-archive/templates/summary-data-template.json +128 -0
  409. package/resources/harness/bundles/java/cursor/harness-codebase-map/SKILL.md +95 -0
  410. package/resources/harness/bundles/java/cursor/harness-codebase-map/checklist.md +126 -0
  411. package/resources/harness/bundles/java/cursor/harness-codebase-map/reference.md +364 -0
  412. package/resources/harness/bundles/java/cursor/harness-codebase-map/templates/ARCHITECTURE.md +57 -0
  413. package/resources/harness/bundles/java/cursor/harness-codebase-map/templates/CONCERNS.md +49 -0
  414. package/resources/harness/bundles/java/cursor/harness-codebase-map/templates/CONVENTIONS.md +57 -0
  415. package/resources/harness/bundles/java/cursor/harness-codebase-map/templates/INTEGRATIONS.md +52 -0
  416. package/resources/harness/bundles/java/cursor/harness-codebase-map/templates/STACK.md +55 -0
  417. package/resources/harness/bundles/java/cursor/harness-codebase-map/templates/STRUCTURE.md +69 -0
  418. package/resources/harness/bundles/java/cursor/harness-codebase-map/templates/TESTING.md +64 -0
  419. package/resources/harness/bundles/java/cursor/harness-codebase-map/templates/map-manifest.schema.json +73 -0
  420. package/resources/harness/bundles/java/cursor/harness-codebase-map/templates/map-summary.md +64 -0
  421. package/resources/harness/bundles/java/cursor/harness-knowledge-ingest/SKILL.md +229 -0
  422. package/resources/harness/bundles/java/cursor/harness-knowledge-ingest/design.md +842 -0
  423. package/resources/harness/bundles/java/cursor/harness-knowledge-ingest/evaluations/harness_knowledge_evaluation.xml +42 -0
  424. package/resources/harness/bundles/java/cursor/harness-knowledge-ingest/mcp-config.example.json +10 -0
  425. package/resources/harness/bundles/java/cursor/harness-knowledge-ingest/reference.md +309 -0
  426. package/resources/harness/bundles/java/cursor/harness-knowledge-ingest/scripts/harness_knowledge.py +4082 -0
  427. package/resources/harness/bundles/java/cursor/harness-knowledge-ingest/scripts/harness_knowledge_mcp.py +273 -0
  428. package/resources/harness/bundles/java/cursor/harness-knowledge-query/SKILL.md +147 -0
  429. package/resources/harness/bundles/java/cursor/harness-package/SKILL.md +69 -0
  430. package/resources/harness/bundles/java/cursor/harness-package/checklist.md +322 -0
  431. package/resources/harness/bundles/java/cursor/harness-package/reference.md +210 -0
  432. package/resources/harness/bundles/java/cursor/harness-plan/SKILL.md +109 -0
  433. package/resources/harness/bundles/java/cursor/harness-plan/checklist.md +243 -0
  434. package/resources/harness/bundles/java/cursor/harness-plan/protocols.md +124 -0
  435. package/resources/harness/bundles/java/cursor/harness-plan/reference.md +376 -0
  436. package/resources/harness/bundles/java/cursor/harness-review/SKILL.md +137 -0
  437. package/resources/harness/bundles/java/cursor/harness-review/checklist.md +124 -0
  438. package/resources/harness/bundles/java/cursor/harness-review/protocols.md +68 -0
  439. package/resources/harness/bundles/java/cursor/harness-review/reference.md +86 -0
  440. package/resources/harness/bundles/java/cursor/harness-run/SKILL.md +129 -0
  441. package/resources/harness/bundles/java/cursor/harness-run/checklist.md +250 -0
  442. package/resources/harness/bundles/java/cursor/harness-run/protocols.md +78 -0
  443. package/resources/harness/bundles/java/cursor/harness-run/reference.md +814 -0
  444. package/resources/harness/bundles/java/cursor/harness-submit/SKILL.md +149 -0
  445. package/resources/harness/bundles/java/cursor/harness-submit/checklist.md +407 -0
  446. package/resources/harness/bundles/java/cursor/harness-submit/reference.md +152 -0
  447. package/resources/harness/bundles/java/cursor/harness-sync/SKILL.md +65 -0
  448. package/resources/harness/bundles/java/cursor/harness-sync/reference.md +153 -0
  449. package/resources/harness/bundles/java/cursor/harness-test/SKILL.md +172 -0
  450. package/resources/harness/bundles/java/cursor/harness-test/checklist.md +317 -0
  451. package/resources/harness/bundles/java/cursor/harness-test/pitfalls-java.md +16 -0
  452. package/resources/harness/bundles/java/cursor/harness-test/pitfalls.md +260 -0
  453. package/resources/harness/bundles/java/cursor/harness-test/reference.md +788 -0
  454. package/resources/harness/bundles/java/cursor/protocols/archive-report-protocol.md +175 -0
  455. package/resources/harness/bundles/java/cursor/protocols/evidence-based-reporting-protocol.md +134 -0
  456. package/resources/harness/bundles/java/cursor/protocols/ledger-protocol.md +270 -0
  457. package/resources/harness/bundles/java/cursor/protocols/powershell-protocol.md +181 -0
  458. package/resources/harness/bundles/java/cursor/protocols/report-pipeline-protocol.md +150 -0
  459. package/resources/harness/bundles/java/cursor/protocols/sensitive-info-protocol.md +92 -0
  460. package/resources/harness/bundles/java/cursor/protocols/state-layout-protocol.md +131 -0
  461. package/resources/harness/bundles/java/cursor/protocols/submit-protocol.md +78 -0
  462. package/resources/harness/bundles/java/cursor/scripts/harness_archive.py +2319 -0
  463. package/resources/harness/bundles/java/cursor/scripts/harness_deploy.py +674 -0
  464. package/resources/harness/bundles/java/cursor/scripts/harness_events.py +680 -0
  465. package/resources/harness/bundles/java/cursor/scripts/harness_ledger.py +671 -0
  466. package/resources/harness/bundles/java/cursor/scripts/harness_preflight.py +780 -0
  467. package/resources/harness/bundles/java/cursor/scripts/harness_service.py +1331 -0
  468. package/resources/harness/manifests/{general.json → general/claude-code.json} +27 -26
  469. package/resources/harness/manifests/general/codebuddy.json +277 -0
  470. package/resources/harness/manifests/general/codex.json +265 -0
  471. package/resources/harness/manifests/general/cursor.json +265 -0
  472. package/resources/harness/manifests/{java.json → java/claude-code.json} +31 -30
  473. package/resources/harness/manifests/java/codebuddy.json +305 -0
  474. package/resources/harness/manifests/java/codex.json +293 -0
  475. package/resources/harness/manifests/java/cursor.json +293 -0
  476. package/resources/harness/general/.harness-build.json +0 -5
  477. package/resources/harness/java/.harness-build.json +0 -5
  478. /package/resources/harness/{general → bundles/general/claude-code}/CONTEXT.md +0 -0
  479. /package/resources/harness/{general → bundles/general/claude-code}/README.md +0 -0
  480. /package/resources/harness/{general → bundles/general/claude-code}/agents/harness-evaluator.md +0 -0
  481. /package/resources/harness/{general → bundles/general/claude-code}/agents/harness-explorer.md +0 -0
  482. /package/resources/harness/{general → bundles/general/claude-code}/harness-archive/checklist.md +0 -0
  483. /package/resources/harness/{general → bundles/general/claude-code}/harness-archive/reference.md +0 -0
  484. /package/resources/harness/{general → bundles/general/claude-code}/harness-archive/scripts/gen-manifest.ps1 +0 -0
  485. /package/resources/harness/{general → bundles/general/claude-code}/harness-archive/templates/render-summary.mjs +0 -0
  486. /package/resources/harness/{general → bundles/general/claude-code}/harness-archive/templates/summary-data-template.json +0 -0
  487. /package/resources/harness/{general → bundles/general/claude-code}/harness-codebase-map/checklist.md +0 -0
  488. /package/resources/harness/{general → bundles/general/claude-code}/harness-codebase-map/reference.md +0 -0
  489. /package/resources/harness/{general → bundles/general/claude-code}/harness-codebase-map/templates/ARCHITECTURE.md +0 -0
  490. /package/resources/harness/{general → bundles/general/claude-code}/harness-codebase-map/templates/CONCERNS.md +0 -0
  491. /package/resources/harness/{general → bundles/general/claude-code}/harness-codebase-map/templates/CONVENTIONS.md +0 -0
  492. /package/resources/harness/{general → bundles/general/claude-code}/harness-codebase-map/templates/INTEGRATIONS.md +0 -0
  493. /package/resources/harness/{general → bundles/general/claude-code}/harness-codebase-map/templates/STACK.md +0 -0
  494. /package/resources/harness/{general → bundles/general/claude-code}/harness-codebase-map/templates/STRUCTURE.md +0 -0
  495. /package/resources/harness/{general → bundles/general/claude-code}/harness-codebase-map/templates/TESTING.md +0 -0
  496. /package/resources/harness/{general → bundles/general/claude-code}/harness-codebase-map/templates/map-manifest.schema.json +0 -0
  497. /package/resources/harness/{general → bundles/general/claude-code}/harness-codebase-map/templates/map-summary.md +0 -0
  498. /package/resources/harness/{general → bundles/general/claude-code}/harness-knowledge-ingest/design.md +0 -0
  499. /package/resources/harness/{general → bundles/general/claude-code}/harness-knowledge-ingest/evaluations/harness_knowledge_evaluation.xml +0 -0
  500. /package/resources/harness/{general → bundles/general/claude-code}/harness-knowledge-ingest/mcp-config.example.json +0 -0
  501. /package/resources/harness/{general → bundles/general/claude-code}/harness-knowledge-ingest/scripts/harness_knowledge.py +0 -0
  502. /package/resources/harness/{general → bundles/general/claude-code}/harness-knowledge-ingest/scripts/harness_knowledge_mcp.py +0 -0
  503. /package/resources/harness/{general → bundles/general/claude-code}/harness-plan/checklist.md +0 -0
  504. /package/resources/harness/{general → bundles/general/claude-code}/harness-plan/protocols.md +0 -0
  505. /package/resources/harness/{general → bundles/general/claude-code}/harness-plan/reference.md +0 -0
  506. /package/resources/harness/{general → bundles/general/claude-code}/harness-review/protocols.md +0 -0
  507. /package/resources/harness/{general → bundles/general/claude-code}/harness-review/reference.md +0 -0
  508. /package/resources/harness/{general → bundles/general/claude-code}/harness-run/protocols.md +0 -0
  509. /package/resources/harness/{general → bundles/general/claude-code}/harness-submit/checklist.md +0 -0
  510. /package/resources/harness/{general → bundles/general/claude-code}/harness-submit/reference.md +0 -0
  511. /package/resources/harness/{general → bundles/general/claude-code}/harness-test/pitfalls.md +0 -0
  512. /package/resources/harness/{general → bundles/general/claude-code}/harness-test/reference.md +0 -0
  513. /package/resources/harness/{general → bundles/general/claude-code}/protocols/archive-report-protocol.md +0 -0
  514. /package/resources/harness/{general → bundles/general/claude-code}/protocols/evidence-based-reporting-protocol.md +0 -0
  515. /package/resources/harness/{general → bundles/general/claude-code}/protocols/ledger-protocol.md +0 -0
  516. /package/resources/harness/{general → bundles/general/claude-code}/protocols/powershell-protocol.md +0 -0
  517. /package/resources/harness/{general → bundles/general/claude-code}/protocols/report-pipeline-protocol.md +0 -0
  518. /package/resources/harness/{general → bundles/general/claude-code}/protocols/sensitive-info-protocol.md +0 -0
  519. /package/resources/harness/{general → bundles/general/claude-code}/protocols/state-layout-protocol.md +0 -0
  520. /package/resources/harness/{general → bundles/general/claude-code}/protocols/submit-protocol.md +0 -0
  521. /package/resources/harness/{java → bundles/general/codebuddy}/CONTEXT.md +0 -0
  522. /package/resources/harness/{java → bundles/general/codebuddy}/README.md +0 -0
  523. /package/resources/harness/{java → bundles/general/codebuddy}/harness-archive/checklist.md +0 -0
  524. /package/resources/harness/{java → bundles/general/codebuddy}/harness-archive/reference.md +0 -0
  525. /package/resources/harness/{java → bundles/general/codebuddy}/harness-archive/scripts/gen-manifest.ps1 +0 -0
  526. /package/resources/harness/{java → bundles/general/codebuddy}/harness-archive/templates/render-summary.mjs +0 -0
  527. /package/resources/harness/{java → bundles/general/codebuddy}/harness-archive/templates/summary-data-template.json +0 -0
  528. /package/resources/harness/{java → bundles/general/codebuddy}/harness-codebase-map/checklist.md +0 -0
  529. /package/resources/harness/{java → bundles/general/codebuddy}/harness-codebase-map/reference.md +0 -0
  530. /package/resources/harness/{java → bundles/general/codebuddy}/harness-codebase-map/templates/ARCHITECTURE.md +0 -0
  531. /package/resources/harness/{java → bundles/general/codebuddy}/harness-codebase-map/templates/CONCERNS.md +0 -0
  532. /package/resources/harness/{java → bundles/general/codebuddy}/harness-codebase-map/templates/CONVENTIONS.md +0 -0
  533. /package/resources/harness/{java → bundles/general/codebuddy}/harness-codebase-map/templates/INTEGRATIONS.md +0 -0
  534. /package/resources/harness/{java → bundles/general/codebuddy}/harness-codebase-map/templates/STACK.md +0 -0
  535. /package/resources/harness/{java → bundles/general/codebuddy}/harness-codebase-map/templates/STRUCTURE.md +0 -0
  536. /package/resources/harness/{java → bundles/general/codebuddy}/harness-codebase-map/templates/TESTING.md +0 -0
  537. /package/resources/harness/{java → bundles/general/codebuddy}/harness-codebase-map/templates/map-manifest.schema.json +0 -0
  538. /package/resources/harness/{java → bundles/general/codebuddy}/harness-codebase-map/templates/map-summary.md +0 -0
  539. /package/resources/harness/{java → bundles/general/codebuddy}/harness-knowledge-ingest/design.md +0 -0
  540. /package/resources/harness/{java → bundles/general/codebuddy}/harness-knowledge-ingest/evaluations/harness_knowledge_evaluation.xml +0 -0
  541. /package/resources/harness/{java → bundles/general/codebuddy}/harness-knowledge-ingest/mcp-config.example.json +0 -0
  542. /package/resources/harness/{java → bundles/general/codebuddy}/harness-knowledge-ingest/scripts/harness_knowledge.py +0 -0
  543. /package/resources/harness/{java → bundles/general/codebuddy}/harness-knowledge-ingest/scripts/harness_knowledge_mcp.py +0 -0
  544. /package/resources/harness/{java → bundles/general/codebuddy}/harness-plan/checklist.md +0 -0
  545. /package/resources/harness/{java → bundles/general/codebuddy}/harness-plan/protocols.md +0 -0
  546. /package/resources/harness/{java → bundles/general/codebuddy}/harness-plan/reference.md +0 -0
  547. /package/resources/harness/{java → bundles/general/codebuddy}/harness-review/protocols.md +0 -0
  548. /package/resources/harness/{java → bundles/general/codebuddy}/harness-review/reference.md +0 -0
  549. /package/resources/harness/{java → bundles/general/codebuddy}/harness-run/protocols.md +0 -0
  550. /package/resources/harness/{java → bundles/general/codebuddy}/harness-submit/checklist.md +0 -0
  551. /package/resources/harness/{java → bundles/general/codebuddy}/harness-submit/reference.md +0 -0
  552. /package/resources/harness/{java → bundles/general/codebuddy}/harness-test/pitfalls.md +0 -0
  553. /package/resources/harness/{java → bundles/general/codebuddy}/protocols/archive-report-protocol.md +0 -0
  554. /package/resources/harness/{java → bundles/general/codebuddy}/protocols/evidence-based-reporting-protocol.md +0 -0
  555. /package/resources/harness/{java → bundles/general/codebuddy}/protocols/ledger-protocol.md +0 -0
  556. /package/resources/harness/{java → bundles/general/codebuddy}/protocols/powershell-protocol.md +0 -0
  557. /package/resources/harness/{java → bundles/general/codebuddy}/protocols/report-pipeline-protocol.md +0 -0
  558. /package/resources/harness/{java → bundles/general/codebuddy}/protocols/sensitive-info-protocol.md +0 -0
  559. /package/resources/harness/{java → bundles/general/codebuddy}/protocols/state-layout-protocol.md +0 -0
  560. /package/resources/harness/{java → bundles/general/codebuddy}/protocols/submit-protocol.md +0 -0
  561. /package/resources/harness/{java → bundles/java/claude-code}/agents/harness-evaluator.md +0 -0
  562. /package/resources/harness/{java → bundles/java/claude-code}/agents/harness-explorer.md +0 -0
  563. /package/resources/harness/{java → bundles/java/claude-code}/harness-apidoc/checklist.md +0 -0
  564. /package/resources/harness/{java → bundles/java/claude-code}/harness-apidoc/reference.md +0 -0
  565. /package/resources/harness/{java → bundles/java/claude-code}/harness-package/checklist.md +0 -0
  566. /package/resources/harness/{java → bundles/java/claude-code}/harness-test/pitfalls-java.md +0 -0
@@ -0,0 +1,2319 @@
1
+ #!/usr/bin/env python3
2
+ """Harness archive finalize / status / replay (D3).
3
+
4
+ Subcommands:
5
+ status — pre-archive gate checks (read-only)
6
+ finalize — single-process archive: manifest → move → collect → render →
7
+ validate → after-manifest → delete-or-keep → knowledge/service
8
+ replay — read-only re-collect + validate for historical archives
9
+
10
+ Python 3.10+, stdlib only. UTF-8 without BOM. Windows path safe.
11
+ Depends on P0-1 harness_events.py; optionally P0-3 harness_knowledge.py.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import argparse
17
+ import datetime as dt
18
+ import hashlib
19
+ import json
20
+ import os
21
+ import re
22
+ import shutil
23
+ import subprocess
24
+ import sys
25
+ import tempfile
26
+ import uuid
27
+ from pathlib import Path
28
+ from typing import Any
29
+
30
+
31
+ if hasattr(sys.stdout, "reconfigure"):
32
+ sys.stdout.reconfigure(encoding="utf-8")
33
+ if hasattr(sys.stderr, "reconfigure"):
34
+ sys.stderr.reconfigure(encoding="utf-8")
35
+
36
+
37
+ SCRIPTS_DIR = Path(__file__).resolve().parent
38
+ SKILLS_ROOT = SCRIPTS_DIR.parent
39
+ RENDER_SCRIPT = SKILLS_ROOT / "harness-archive" / "templates" / "render-summary.mjs"
40
+ SUMMARY_TEMPLATE = (
41
+ SKILLS_ROOT / "harness-archive" / "templates" / "summary-data-template.json"
42
+ )
43
+ KNOWLEDGE_SCRIPT = (
44
+ SKILLS_ROOT / "harness-knowledge-ingest" / "scripts" / "harness_knowledge.py"
45
+ )
46
+ SERVICE_SCRIPT = SCRIPTS_DIR / "harness_service.py"
47
+
48
+ # Manifest compare must ignore self-mutating log files appended during finalize.
49
+ MANIFEST_COMPARE_EXCLUDE = frozenset(
50
+ {
51
+ "logs/execution-log.md",
52
+ "execution-log.md",
53
+ "events.ndjson",
54
+ }
55
+ )
56
+
57
+ SCHEMA_VERSION = "2.2"
58
+ NOT_AVAILABLE = "not_available"
59
+
60
+ # Ensure sibling harness_events is importable.
61
+ if str(SCRIPTS_DIR) not in sys.path:
62
+ sys.path.insert(0, str(SCRIPTS_DIR))
63
+
64
+ import harness_events as he # noqa: E402
65
+
66
+
67
+ # ---------------------------------------------------------------------------
68
+ # I/O helpers
69
+ # ---------------------------------------------------------------------------
70
+
71
+
72
+ def now_iso() -> str:
73
+ return dt.datetime.now().astimezone().isoformat(timespec="milliseconds")
74
+
75
+
76
+ def today_date() -> str:
77
+ return dt.date.today().isoformat()
78
+
79
+
80
+ def emit_json(payload: dict[str, Any]) -> None:
81
+ sys.stdout.write(json.dumps(payload, ensure_ascii=False, indent=2) + "\n")
82
+
83
+
84
+ def emit_error(message: str, *, as_json: bool, extra: dict[str, Any] | None = None) -> int:
85
+ payload: dict[str, Any] = {"ok": False, "error": message}
86
+ if extra:
87
+ payload.update(extra)
88
+ if as_json:
89
+ sys.stderr.write(json.dumps(payload, ensure_ascii=False) + "\n")
90
+ else:
91
+ sys.stderr.write(f"error: {message}\n")
92
+ return 1
93
+
94
+
95
+ def resolve_path(raw: str) -> Path:
96
+ return Path(raw).expanduser().resolve()
97
+
98
+
99
+ def read_json(path: Path) -> Any:
100
+ return json.loads(path.read_text(encoding="utf-8-sig"))
101
+
102
+
103
+ def write_json(path: Path, data: Any) -> None:
104
+ path.parent.mkdir(parents=True, exist_ok=True)
105
+ path.write_text(
106
+ json.dumps(data, ensure_ascii=False, indent=2) + "\n",
107
+ encoding="utf-8",
108
+ newline="\n",
109
+ )
110
+
111
+
112
+ def sha256_file(path: Path) -> str:
113
+ h = hashlib.sha256()
114
+ with path.open("rb") as f:
115
+ for chunk in iter(lambda: f.read(1024 * 1024), b""):
116
+ h.update(chunk)
117
+ return h.hexdigest()
118
+
119
+
120
+ def find_project_root(change_or_archive_dir: Path) -> Path:
121
+ """Resolve project root from .harness/changes|archive/<name>."""
122
+ p = change_or_archive_dir.resolve()
123
+ if p.parent.name in {"changes", "archive"} and p.parent.parent.name == ".harness":
124
+ return p.parent.parent.parent
125
+ # Fallback: walk up looking for .harness
126
+ for parent in p.parents:
127
+ if (parent / ".harness").is_dir():
128
+ return parent
129
+ return p.parent
130
+
131
+
132
+ def infer_change_name(dir_path: Path) -> str:
133
+ name = dir_path.name
134
+ m = re.match(r"^(\d{4}-\d{2}-\d{2})-(.+)$", name)
135
+ if m:
136
+ return m.group(2)
137
+ return name
138
+
139
+
140
+ def load_template() -> dict[str, Any]:
141
+ if SUMMARY_TEMPLATE.is_file():
142
+ data = read_json(SUMMARY_TEMPLATE)
143
+ if isinstance(data, dict):
144
+ return data
145
+ return {"schemaVersion": SCHEMA_VERSION}
146
+
147
+
148
+ # ---------------------------------------------------------------------------
149
+ # Events append (reuse P0-1)
150
+ # ---------------------------------------------------------------------------
151
+
152
+
153
+ def append_event(
154
+ change_dir: Path,
155
+ *,
156
+ phase: str,
157
+ type_: str,
158
+ **fields: Any,
159
+ ) -> dict[str, Any]:
160
+ """Append one event via harness_events primitives and re-render execution-log."""
161
+ path = he.events_path(change_dir)
162
+ existing = he.load_events(path) if path.exists() else []
163
+ event: dict[str, Any] = {
164
+ "schema_version": he.SCHEMA_VERSION,
165
+ "id": he.new_event_id(existing),
166
+ "timestamp": now_iso(),
167
+ "phase": phase,
168
+ "type": type_,
169
+ "note": "",
170
+ }
171
+ for key, value in fields.items():
172
+ if value is not None:
173
+ event[key] = value
174
+ line = json.dumps(event, ensure_ascii=False, separators=(",", ":"))
175
+ he.atomic_append_line(path, line)
176
+ all_events = existing + [he.normalize_event(event)]
177
+ he.write_execution_log(change_dir, he.render_execution_log(all_events))
178
+ return event
179
+
180
+
181
+ # ---------------------------------------------------------------------------
182
+ # Manifest (Python port of gen-manifest.ps1)
183
+ # ---------------------------------------------------------------------------
184
+
185
+
186
+ def generate_manifest(root: Path, output_path: Path) -> dict[str, Any]:
187
+ """Build path/size/sha256 manifest; exclude the output file itself."""
188
+ root = root.resolve()
189
+ exclude: Path | None = None
190
+ if output_path.exists():
191
+ exclude = output_path.resolve()
192
+
193
+ files: list[dict[str, Any]] = []
194
+ total_bytes = 0
195
+ for path in sorted(root.rglob("*")):
196
+ if not path.is_file():
197
+ continue
198
+ if exclude is not None and path.resolve() == exclude:
199
+ continue
200
+ rel = path.relative_to(root).as_posix()
201
+ size = path.stat().st_size
202
+ total_bytes += size
203
+ files.append(
204
+ {
205
+ "path": rel,
206
+ "sizeBytes": size,
207
+ "sha256": sha256_file(path),
208
+ }
209
+ )
210
+
211
+ result = {
212
+ "root": str(root),
213
+ "generatedAt": dt.datetime.now().isoformat(timespec="seconds"),
214
+ "fileCount": len(files),
215
+ "totalBytes": total_bytes,
216
+ "files": files,
217
+ }
218
+ write_json(output_path, result)
219
+ return result
220
+
221
+
222
+ def _manifest_path_excluded(rel: str) -> bool:
223
+ norm = rel.replace("\\", "/")
224
+ if norm in MANIFEST_COMPARE_EXCLUDE:
225
+ return True
226
+ if norm == "events.ndjson" or norm.endswith("/events.ndjson"):
227
+ return True
228
+ if norm.endswith("execution-log.md"):
229
+ return True
230
+ return False
231
+
232
+
233
+ def _manifest_index(manifest: dict[str, Any]) -> dict[str, dict[str, Any]]:
234
+ out: dict[str, dict[str, Any]] = {}
235
+ for item in manifest.get("files") or []:
236
+ if not isinstance(item, dict):
237
+ continue
238
+ rel = str(item.get("path") or "").replace("\\", "/")
239
+ if not rel or _manifest_path_excluded(rel):
240
+ continue
241
+ out[rel] = item
242
+ return out
243
+
244
+
245
+ def compare_manifests(
246
+ before: dict[str, Any],
247
+ after: dict[str, Any],
248
+ ) -> dict[str, Any]:
249
+ """Compare before/after; exclude execution-log / events self-appends.
250
+
251
+ Files only in after are treated as generated (OK).
252
+ Files in before missing or hash-mismatched in after are errors.
253
+ """
254
+ b_idx = _manifest_index(before)
255
+ a_idx = _manifest_index(after)
256
+
257
+ missing: list[str] = []
258
+ mismatched: list[dict[str, str]] = []
259
+ for rel, b_item in b_idx.items():
260
+ a_item = a_idx.get(rel)
261
+ if a_item is None:
262
+ missing.append(rel)
263
+ continue
264
+ if str(a_item.get("sha256")) != str(b_item.get("sha256")):
265
+ mismatched.append(
266
+ {
267
+ "path": rel,
268
+ "before": str(b_item.get("sha256")),
269
+ "after": str(a_item.get("sha256")),
270
+ }
271
+ )
272
+
273
+ generated = sorted(set(a_idx) - set(b_idx))
274
+ moved_ok = len(b_idx) - len(missing) - len(mismatched)
275
+ ok = not missing and not mismatched
276
+ return {
277
+ "ok": ok,
278
+ "movedFiles": moved_ok,
279
+ "generatedFiles": len(generated),
280
+ "totalArchiveFiles": int(after.get("fileCount") or len(a_idx)),
281
+ "missing": missing,
282
+ "mismatched": mismatched,
283
+ "generated": generated,
284
+ "checksumStatus": "OK" if ok else "FAIL",
285
+ }
286
+
287
+
288
+ # ---------------------------------------------------------------------------
289
+ # Evidence loaders
290
+ # ---------------------------------------------------------------------------
291
+
292
+
293
+ def load_ledger(change_dir: Path) -> dict[str, Any] | None:
294
+ for rel in (
295
+ "evidence/verification-ledger.json",
296
+ "verification-ledger.json",
297
+ ):
298
+ path = change_dir / rel
299
+ if path.is_file():
300
+ try:
301
+ data = read_json(path)
302
+ return data if isinstance(data, dict) else None
303
+ except (OSError, json.JSONDecodeError):
304
+ return None
305
+ return None
306
+
307
+
308
+ def load_execution_log(change_dir: Path) -> str:
309
+ for rel in ("logs/execution-log.md", "execution-log.md"):
310
+ path = change_dir / rel
311
+ if path.is_file():
312
+ try:
313
+ return path.read_text(encoding="utf-8-sig")
314
+ except OSError:
315
+ return ""
316
+ return ""
317
+
318
+
319
+ def load_existing_summary(change_dir: Path) -> dict[str, Any] | None:
320
+ for rel in (
321
+ "reports/final/summary-data.json",
322
+ "summary-data.json",
323
+ ):
324
+ path = change_dir / rel
325
+ if path.is_file():
326
+ try:
327
+ data = read_json(path)
328
+ return data if isinstance(data, dict) else None
329
+ except (OSError, json.JSONDecodeError):
330
+ return None
331
+ return None
332
+
333
+
334
+ def find_test_reports(change_dir: Path) -> list[Path]:
335
+ patterns = [
336
+ "tests/test-report-*.md",
337
+ "reports/test/test-report-*.md",
338
+ "reports/test/*.md",
339
+ ]
340
+ found: list[Path] = []
341
+ for pattern in patterns:
342
+ found.extend(sorted(change_dir.glob(pattern)))
343
+ return found
344
+
345
+
346
+ def find_review_reports(change_dir: Path) -> list[Path]:
347
+ patterns = [
348
+ "reports/review/review-report-*.md",
349
+ "reviews/review-report-*.md",
350
+ "reports/review/fixback-*.md",
351
+ ]
352
+ found: list[Path] = []
353
+ for pattern in patterns:
354
+ found.extend(sorted(change_dir.glob(pattern)))
355
+ return found
356
+
357
+
358
+ def git_run(project: Path, *args: str) -> tuple[int, str, str]:
359
+ try:
360
+ proc = subprocess.run(
361
+ ["git", "-C", str(project), *args],
362
+ capture_output=True,
363
+ text=True,
364
+ encoding="utf-8",
365
+ errors="replace",
366
+ timeout=30,
367
+ check=False,
368
+ )
369
+ return proc.returncode, proc.stdout.strip(), proc.stderr.strip()
370
+ except (OSError, subprocess.TimeoutExpired) as exc:
371
+ return 1, "", str(exc)
372
+
373
+
374
+ def extract_final_pushed_hash(execution_log: str) -> str | None:
375
+ """Parse 'final pushed hash' from execution-log (submit phase)."""
376
+ patterns = [
377
+ r"final pushed hash[:\s`]+([0-9a-f]{7,40})",
378
+ r"finalPushedHash[:\s\"']+([0-9a-f]{7,40})",
379
+ r"pushed hash[:\s`]+([0-9a-f]{7,40})",
380
+ ]
381
+ for pat in patterns:
382
+ m = re.search(pat, execution_log, re.IGNORECASE)
383
+ if m:
384
+ return m.group(1)
385
+ return None
386
+
387
+
388
+ def worktree_requested(change_dir: Path) -> bool:
389
+ for rel in ("meta/worktree.json", "worktree.json"):
390
+ path = change_dir / rel
391
+ if path.is_file():
392
+ try:
393
+ data = read_json(path)
394
+ if isinstance(data, dict):
395
+ return bool(data.get("requested"))
396
+ except (OSError, json.JSONDecodeError):
397
+ pass
398
+ return False
399
+
400
+
401
+ # ---------------------------------------------------------------------------
402
+ # status
403
+ # ---------------------------------------------------------------------------
404
+
405
+
406
+ def check_status(change_dir: Path) -> dict[str, Any]:
407
+ """Read-only archive preconditions. Never mutates."""
408
+ blockers: list[dict[str, str]] = []
409
+ warnings: list[dict[str, str]] = []
410
+ checks: dict[str, Any] = {}
411
+
412
+ if not change_dir.is_dir():
413
+ return {
414
+ "ok": False,
415
+ "archivable": False,
416
+ "change_dir": str(change_dir),
417
+ "blockers": [{"code": "missing-change-dir", "message": f"not found: {change_dir}"}],
418
+ "warnings": [],
419
+ "checks": {},
420
+ }
421
+
422
+ project = find_project_root(change_dir)
423
+ checks["project_root"] = str(project)
424
+
425
+ # --- commit pushed ---
426
+ code, out, err = git_run(project, "rev-parse", "--is-inside-work-tree")
427
+ if code != 0:
428
+ warnings.append(
429
+ {
430
+ "code": "git-unavailable",
431
+ "message": f"not a git work tree or git missing: {err or out}",
432
+ }
433
+ )
434
+ checks["commit_pushed"] = None
435
+ else:
436
+ code, out, err = git_run(project, "log", "@{u}..HEAD", "--oneline")
437
+ if code != 0:
438
+ # No upstream configured
439
+ warnings.append(
440
+ {
441
+ "code": "no-upstream",
442
+ "message": err or "no upstream configured; cannot verify push",
443
+ }
444
+ )
445
+ checks["commit_pushed"] = None
446
+ elif out.strip():
447
+ blockers.append(
448
+ {
449
+ "code": "unpushed-commits",
450
+ "message": f"unpushed commits:\n{out}",
451
+ }
452
+ )
453
+ checks["commit_pushed"] = False
454
+ else:
455
+ checks["commit_pushed"] = True
456
+
457
+ # --- final hash ---
458
+ code, head, _ = git_run(project, "rev-parse", "HEAD")
459
+ head_hash = head if code == 0 else None
460
+ checks["head"] = head_hash
461
+
462
+ expected_hash: str | None = None
463
+ hash_source: str | None = None
464
+ ledger = load_ledger(change_dir)
465
+ if worktree_requested(change_dir) and ledger:
466
+ merge_hash = ledger.get("mergeFinalHash") or (
467
+ ledger.get("merge") or {}
468
+ ).get("finalHash")
469
+ if merge_hash:
470
+ expected_hash = str(merge_hash)
471
+ hash_source = "verification-ledger.mergeFinalHash"
472
+ if expected_hash is None:
473
+ log_text = load_execution_log(change_dir)
474
+ pushed = extract_final_pushed_hash(log_text)
475
+ if pushed:
476
+ expected_hash = pushed
477
+ hash_source = "execution-log.final-pushed-hash"
478
+ if expected_hash is None and ledger:
479
+ for key in ("finalCommit", "finalHash", "headCommit"):
480
+ if ledger.get(key):
481
+ expected_hash = str(ledger[key])
482
+ hash_source = f"verification-ledger.{key}"
483
+ break
484
+
485
+ checks["expected_final_hash"] = expected_hash
486
+ checks["hash_source"] = hash_source
487
+
488
+ if head_hash and expected_hash:
489
+ # Allow short-hash prefix match
490
+ if head_hash.startswith(expected_hash) or expected_hash.startswith(head_hash[:7]):
491
+ checks["final_hash_match"] = True
492
+ else:
493
+ checks["final_hash_match"] = False
494
+ blockers.append(
495
+ {
496
+ "code": "final-hash-mismatch",
497
+ "message": (
498
+ f"HEAD={head_hash} != expected={expected_hash} "
499
+ f"(source={hash_source})"
500
+ ),
501
+ }
502
+ )
503
+ elif head_hash and expected_hash is None:
504
+ warnings.append(
505
+ {
506
+ "code": "final-hash-unknown",
507
+ "message": "could not determine expected final hash from ledger/log",
508
+ }
509
+ )
510
+ checks["final_hash_match"] = None
511
+ else:
512
+ checks["final_hash_match"] = None
513
+
514
+ # --- test / review reports ---
515
+ test_reports = find_test_reports(change_dir)
516
+ review_reports = find_review_reports(change_dir)
517
+ log_text = load_execution_log(change_dir)
518
+ has_review_section = bool(
519
+ re.search(r"harness-review|##\s*review", log_text, re.IGNORECASE)
520
+ )
521
+
522
+ checks["test_reports"] = [str(p.relative_to(change_dir)) for p in test_reports]
523
+ checks["review_reports"] = [str(p.relative_to(change_dir)) for p in review_reports]
524
+
525
+ if test_reports:
526
+ checks["test_report_status"] = "present"
527
+ else:
528
+ checks["test_report_status"] = "missing-mark-skipped"
529
+ warnings.append(
530
+ {
531
+ "code": "test-report-missing",
532
+ "message": (
533
+ "no test-report-*.md; archive must mark verification as "
534
+ "NOT_RUN/USER_SKIPPED (not fabricated pass rates)"
535
+ ),
536
+ }
537
+ )
538
+
539
+ if review_reports:
540
+ checks["review_report_status"] = "present"
541
+ elif has_review_section:
542
+ checks["review_report_status"] = "ran-but-not-persisted"
543
+ warnings.append(
544
+ {
545
+ "code": "review-not-persisted",
546
+ "message": (
547
+ "execution-log has review section but no review-report file; "
548
+ "prefer persisting report before archive"
549
+ ),
550
+ }
551
+ )
552
+ else:
553
+ checks["review_report_status"] = "not-run"
554
+ warnings.append(
555
+ {
556
+ "code": "review-not-run",
557
+ "message": "no review report; mark reviewSummary as ADVISORY_NOT_RUN",
558
+ }
559
+ )
560
+
561
+ archivable = len(blockers) == 0
562
+ return {
563
+ "ok": True,
564
+ "archivable": archivable,
565
+ "change_dir": str(change_dir),
566
+ "change_name": infer_change_name(change_dir),
567
+ "blockers": blockers,
568
+ "warnings": warnings,
569
+ "checks": checks,
570
+ }
571
+
572
+
573
+ # ---------------------------------------------------------------------------
574
+ # collect
575
+ # ---------------------------------------------------------------------------
576
+
577
+
578
+ def _deepcopy_json(obj: Any) -> Any:
579
+ return json.loads(json.dumps(obj))
580
+
581
+
582
+ def _na_if_missing(value: Any, *, allow_empty: bool = False) -> Any:
583
+ if value is None:
584
+ return NOT_AVAILABLE
585
+ if not allow_empty and value == "":
586
+ return NOT_AVAILABLE
587
+ return value
588
+
589
+
590
+ def _ledger_unit_tests(ledger: dict[str, Any] | None) -> dict[str, Any]:
591
+ empty = {
592
+ "run": 0,
593
+ "failures": 0,
594
+ "errors": 0,
595
+ "skipped": 0,
596
+ "passRate": NOT_AVAILABLE,
597
+ "source": "not-run",
598
+ }
599
+ if not ledger:
600
+ return empty
601
+ validations = ledger.get("validations") or ledger.get("verification") or {}
602
+ unit = validations.get("unitTest") or validations.get("unitTests") or {}
603
+ if not isinstance(unit, dict):
604
+ return empty
605
+ status = str(unit.get("status") or "").upper()
606
+ evidence = unit.get("evidence") or {}
607
+ if isinstance(evidence, dict):
608
+ run = evidence.get("run", unit.get("run", 0))
609
+ failures = evidence.get("failures", unit.get("failures", 0))
610
+ errors = evidence.get("errors", unit.get("errors", 0))
611
+ skipped = evidence.get("skipped", unit.get("skipped", 0))
612
+ pass_rate = evidence.get("passRate") or unit.get("passRate")
613
+ else:
614
+ run = unit.get("run", 0)
615
+ failures = unit.get("failures", 0)
616
+ errors = unit.get("errors", 0)
617
+ skipped = unit.get("skipped", 0)
618
+ pass_rate = unit.get("passRate")
619
+
620
+ source = "committed"
621
+ if status in {"NOT_RUN", "SKIPPED", "USER_SKIPPED"}:
622
+ source = "not-run"
623
+ elif str(unit.get("reused") or "").lower() in {"true", "1"} or "REUSED" in status:
624
+ source = "committed"
625
+
626
+ result = {
627
+ "run": int(run or 0),
628
+ "failures": int(failures or 0),
629
+ "errors": int(errors or 0),
630
+ "skipped": int(skipped or 0),
631
+ "passRate": pass_rate if pass_rate is not None else NOT_AVAILABLE,
632
+ "source": source,
633
+ }
634
+ if status in {"NOT_RUN", "USER_SKIPPED", "SKIPPED"}:
635
+ result["status"] = status if status != "SKIPPED" else "USER_SKIPPED"
636
+ return result
637
+
638
+
639
+ def _ledger_api_tests(ledger: dict[str, Any] | None) -> dict[str, Any]:
640
+ empty = {
641
+ "status": "NOT_RUN",
642
+ "total": 0,
643
+ "passed": 0,
644
+ "failed": 0,
645
+ "blocked": 0,
646
+ "passRate": NOT_AVAILABLE,
647
+ }
648
+ if not ledger:
649
+ return empty
650
+ validations = ledger.get("validations") or ledger.get("verification") or {}
651
+ api = validations.get("apiTest") or validations.get("apiTests") or {}
652
+ if not isinstance(api, dict):
653
+ return empty
654
+ status = str(api.get("status") or "NOT_RUN").upper()
655
+ if status in {"OK", "PASS", "PASSED", "SUCCESS"}:
656
+ status = "OK"
657
+ elif status in {"SKIP", "SKIPPED"}:
658
+ status = "USER_SKIPPED"
659
+ evidence = api.get("evidence") if isinstance(api.get("evidence"), dict) else {}
660
+ total = evidence.get("total", api.get("total", 0))
661
+ passed = evidence.get("passed", api.get("passed", 0))
662
+ failed = evidence.get("failed", api.get("failed", 0))
663
+ blocked = evidence.get("blocked", api.get("blocked", 0))
664
+ pass_rate = evidence.get("passRate") or api.get("passRate")
665
+ return {
666
+ "status": status,
667
+ "total": int(total or 0),
668
+ "passed": int(passed or 0),
669
+ "failed": int(failed or 0),
670
+ "blocked": int(blocked or 0),
671
+ "passRate": pass_rate if pass_rate is not None else NOT_AVAILABLE,
672
+ }
673
+
674
+
675
+ def _ledger_db_compat(ledger: dict[str, Any] | None) -> str:
676
+ if not ledger:
677
+ return "NOT_RUN"
678
+ validations = ledger.get("validations") or {}
679
+ db = validations.get("dbCompatibility") or validations.get("db") or {}
680
+ if isinstance(db, dict):
681
+ return str(db.get("status") or "NOT_RUN").upper()
682
+ if isinstance(db, str) and db.strip():
683
+ return db.strip().upper()
684
+ top = ledger.get("dbCompatibility")
685
+ if isinstance(top, str) and top.strip():
686
+ return top.strip().upper()
687
+ return "NOT_RUN"
688
+
689
+
690
+ def _parse_durations_from_log(log_text: str) -> dict[str, Any]:
691
+ """Best-effort parse of harness-* sections from execution-log."""
692
+ stages: list[dict[str, Any]] = []
693
+ # Match both old hand-written and events-rendered phase headers.
694
+ section_re = re.compile(
695
+ r"(?:###\s*\[\d+\]\s*)?harness-(\w+)|##\s+(plan|run|test|review|submit|merge|archive)\b",
696
+ re.IGNORECASE,
697
+ )
698
+ start_re = re.compile(r"\*\*开始\*\*:\s*(.+)")
699
+ end_re = re.compile(r"\*\*结束\*\*:\s*(.+)")
700
+ dur_re = re.compile(r"\*\*耗时\*\*:\s*(.+)")
701
+ result_re = re.compile(r"\*\*结果\*\*:\s*(.+)")
702
+
703
+ lines = log_text.splitlines()
704
+ i = 0
705
+ while i < len(lines):
706
+ m = section_re.search(lines[i])
707
+ if not m:
708
+ i += 1
709
+ continue
710
+ stage = (m.group(1) or m.group(2) or "").lower()
711
+ skill = f"harness-{stage}" if not stage.startswith("harness") else stage
712
+ started = ended = None
713
+ minutes = 0.0
714
+ result = "OK"
715
+ j = i + 1
716
+ while j < len(lines) and not section_re.search(lines[j]):
717
+ sm = start_re.search(lines[j])
718
+ if sm:
719
+ started = sm.group(1).strip()
720
+ em = end_re.search(lines[j])
721
+ if em:
722
+ ended = em.group(1).strip()
723
+ dm = dur_re.search(lines[j])
724
+ if dm:
725
+ raw = dm.group(1).strip()
726
+ mm = re.search(r"(\d+)\s*分", raw)
727
+ ss = re.search(r"(\d+)\s*秒", raw)
728
+ minutes = (int(mm.group(1)) if mm else 0) + (
729
+ (int(ss.group(1)) if ss else 0) / 60.0
730
+ )
731
+ if minutes == 0:
732
+ ms = re.search(r"([\d.]+)\s*s", raw, re.I)
733
+ if ms:
734
+ minutes = float(ms.group(1)) / 60.0
735
+ rm = result_re.search(lines[j])
736
+ if rm:
737
+ raw_r = rm.group(1)
738
+ if "FAIL" in raw_r or "❌" in raw_r:
739
+ result = "FAIL"
740
+ elif "WARN" in raw_r or "🟡" in raw_r:
741
+ result = "WARN"
742
+ else:
743
+ result = "OK"
744
+ j += 1
745
+ stages.append(
746
+ {
747
+ "stage": stage.replace("harness-", ""),
748
+ "skill": skill if skill.startswith("harness-") else f"harness-{stage}",
749
+ "startedAt": started or NOT_AVAILABLE,
750
+ "endedAt": ended or NOT_AVAILABLE,
751
+ "minutes": round(minutes, 2),
752
+ "result": result,
753
+ }
754
+ )
755
+ i = j
756
+
757
+ total = round(sum(float(s["minutes"]) for s in stages), 2)
758
+ return {
759
+ "totalLabel": f"约 {int(round(total))} 分" if total else "约 0 分",
760
+ "totalMinutes": total,
761
+ "stages": stages,
762
+ }
763
+
764
+
765
+ def _skill_calls_from_stages(stages: list[dict[str, Any]]) -> list[dict[str, Any]]:
766
+ counts: dict[str, dict[str, Any]] = {}
767
+ order: list[str] = []
768
+ for s in stages:
769
+ skill = str(s.get("skill") or s.get("stage") or "unknown")
770
+ if skill not in counts:
771
+ counts[skill] = {"skill": skill, "count": 0, "result": s.get("result") or "OK"}
772
+ order.append(skill)
773
+ counts[skill]["count"] += 1
774
+ counts[skill]["result"] = s.get("result") or counts[skill]["result"]
775
+ return [counts[k] for k in order]
776
+
777
+
778
+ def _phases_from_events_summary(summary: dict[str, Any]) -> dict[str, Any]:
779
+ phases_out: dict[str, Any] = {}
780
+ for name in ("plan", "run", "test", "review", "submit", "archive"):
781
+ phases_out[name] = {"duration_ms": None, "event_count": 0}
782
+ for name, info in (summary.get("phases") or {}).items():
783
+ key = str(name).lower()
784
+ if key not in phases_out:
785
+ phases_out[key] = {"duration_ms": None, "event_count": 0}
786
+ phases_out[key] = {
787
+ "duration_ms": info.get("duration_ms"),
788
+ "event_count": int(info.get("event_count") or 0),
789
+ }
790
+ return phases_out
791
+
792
+
793
+ def _commands_from_events(events: list[dict[str, Any]]) -> list[dict[str, Any]]:
794
+ out: list[dict[str, Any]] = []
795
+ for e in events:
796
+ if e.get("type") != "command":
797
+ continue
798
+ out.append(
799
+ {
800
+ "command": e.get("command") or "",
801
+ "exit_code": e.get("exit_code"),
802
+ "duration_ms": e.get("duration_ms"),
803
+ "phase": e.get("phase"),
804
+ "timestamp": e.get("timestamp"),
805
+ }
806
+ )
807
+ return out
808
+
809
+
810
+ def _verification_checks_from_events(
811
+ events: list[dict[str, Any]],
812
+ ledger: dict[str, Any] | None,
813
+ ) -> list[dict[str, Any]]:
814
+ checks: list[dict[str, Any]] = []
815
+ seen: set[str] = set()
816
+ for e in events:
817
+ if e.get("type") != "verification":
818
+ continue
819
+ name = str(e.get("name") or "unnamed")
820
+ status = str(e.get("status") or "unknown").lower()
821
+ checks.append(
822
+ {
823
+ "name": name,
824
+ "status": status,
825
+ "command": e.get("command") or "",
826
+ "source": "events.ndjson",
827
+ }
828
+ )
829
+ seen.add(name.lower())
830
+ if ledger:
831
+ validations = ledger.get("validations") or {}
832
+ if isinstance(validations, dict):
833
+ for name, info in validations.items():
834
+ if name.lower() in seen:
835
+ continue
836
+ if not isinstance(info, dict):
837
+ continue
838
+ checks.append(
839
+ {
840
+ "name": name,
841
+ "status": str(info.get("status") or "unknown").lower(),
842
+ "command": str(info.get("command") or ""),
843
+ "source": "evidence/verification-ledger.json",
844
+ }
845
+ )
846
+ return checks
847
+
848
+
849
+ def _artifacts_from_events(events: list[dict[str, Any]]) -> list[dict[str, Any]]:
850
+ out: list[dict[str, Any]] = []
851
+ for e in events:
852
+ if e.get("type") != "artifact":
853
+ continue
854
+ out.append(
855
+ {
856
+ "path": e.get("path") or "",
857
+ "kind": e.get("kind") or "",
858
+ "phase": e.get("phase") or "",
859
+ }
860
+ )
861
+ return out
862
+
863
+
864
+ def _durations_from_event_phases(event_summary: dict[str, Any]) -> dict[str, Any]:
865
+ stages: list[dict[str, Any]] = []
866
+ total_ms = 0
867
+ for name, info in (event_summary.get("phases") or {}).items():
868
+ dur = info.get("duration_ms")
869
+ minutes = round((dur or 0) / 60000, 2) if dur is not None else 0
870
+ if dur:
871
+ total_ms += int(dur)
872
+ stages.append(
873
+ {
874
+ "stage": str(name),
875
+ "skill": f"harness-{name}",
876
+ "startedAt": info.get("started_at") or NOT_AVAILABLE,
877
+ "endedAt": info.get("ended_at") or NOT_AVAILABLE,
878
+ "minutes": minutes,
879
+ "result": "OK",
880
+ }
881
+ )
882
+ total_min = round(total_ms / 60000, 2)
883
+ return {
884
+ "totalLabel": f"约 {int(round(total_min))} 分",
885
+ "totalMinutes": total_min,
886
+ "stages": stages,
887
+ }
888
+
889
+
890
+ def _stage_status_from_sources(
891
+ events: list[dict[str, Any]],
892
+ ledger: dict[str, Any] | None,
893
+ change_dir: Path,
894
+ ) -> dict[str, str]:
895
+ status = {
896
+ "plan": "OK",
897
+ "run": "OK",
898
+ "test": "OK",
899
+ "review": "ADVISORY",
900
+ "submit": "OK",
901
+ "archive": "OK",
902
+ }
903
+ # Issues in events can downgrade
904
+ for e in events:
905
+ if e.get("type") != "issue":
906
+ continue
907
+ sev = str(e.get("severity") or "").lower()
908
+ phase = str(e.get("phase") or "").lower()
909
+ if phase in status and sev in {"error", "fail", "failed", "critical"}:
910
+ status[phase] = "FAIL"
911
+ elif phase in status and sev in {"warn", "warning"} and status[phase] == "OK":
912
+ status[phase] = "WARN"
913
+
914
+ api = _ledger_api_tests(ledger)
915
+ db = _ledger_db_compat(ledger)
916
+ if api.get("status") == "USER_SKIPPED":
917
+ status["test"] = "USER_SKIPPED"
918
+ elif db == "BLOCKED_BY_DBA":
919
+ status["test"] = "BLOCKED_BY_DBA"
920
+ elif api.get("status") == "NOT_RUN" and not find_test_reports(change_dir):
921
+ unit = _ledger_unit_tests(ledger)
922
+ if unit.get("source") == "not-run" and unit.get("run", 0) == 0:
923
+ status["test"] = "USER_SKIPPED"
924
+
925
+ if not find_review_reports(change_dir):
926
+ log_text = load_execution_log(change_dir)
927
+ if not re.search(r"harness-review|##\s*review", log_text, re.IGNORECASE):
928
+ status["review"] = "ADVISORY"
929
+
930
+ return status
931
+
932
+
933
+ def _compute_final_status(
934
+ stage_status: dict[str, str],
935
+ verification: dict[str, Any],
936
+ ) -> str:
937
+ api = verification.get("apiTests") or {}
938
+ db = str(verification.get("dbCompatibility") or "")
939
+ api_status = str(api.get("status") or "")
940
+ if api_status == "USER_SKIPPED" or db == "BLOCKED_BY_DBA":
941
+ return "CONDITIONAL_OK"
942
+ for v in stage_status.values():
943
+ if v == "FAIL":
944
+ return "FAIL"
945
+ unit = verification.get("unitTests") or {}
946
+ if int(unit.get("failures") or 0) > 0 or int(unit.get("errors") or 0) > 0:
947
+ return "FAIL"
948
+ if int(api.get("failed") or 0) > 0:
949
+ return "FAIL"
950
+ for v in stage_status.values():
951
+ if v in {"WARN", "PARTIAL", "USER_SKIPPED", "BLOCKED_BY_DBA"}:
952
+ return "WARN" if v == "WARN" else "CONDITIONAL_OK"
953
+ return "OK"
954
+
955
+
956
+ def _changed_files_from_git(
957
+ project: Path,
958
+ base: str | None,
959
+ head: str | None,
960
+ ) -> tuple[dict[str, Any], list[dict[str, Any]]]:
961
+ diff_stat = {
962
+ "filesChanged": 0,
963
+ "insertions": 0,
964
+ "deletions": 0,
965
+ "range": NOT_AVAILABLE,
966
+ }
967
+ changed: list[dict[str, Any]] = []
968
+ if not base or not head:
969
+ return diff_stat, changed
970
+ rng = f"{base}..{head}"
971
+ code, out, _ = git_run(project, "diff", "--numstat", rng)
972
+ if code != 0 or not out:
973
+ diff_stat["range"] = rng
974
+ return diff_stat, changed
975
+ insertions = deletions = 0
976
+ files = 0
977
+ for line in out.splitlines():
978
+ parts = line.split("\t")
979
+ if len(parts) < 3:
980
+ continue
981
+ ins_s, del_s, path = parts[0], parts[1], parts[2]
982
+ ins = int(ins_s) if ins_s.isdigit() else 0
983
+ dele = int(del_s) if del_s.isdigit() else 0
984
+ insertions += ins
985
+ deletions += dele
986
+ files += 1
987
+ changed.append(
988
+ {
989
+ "path": path,
990
+ "summary": "",
991
+ "insertions": ins,
992
+ "deletions": dele,
993
+ }
994
+ )
995
+ diff_stat = {
996
+ "filesChanged": files,
997
+ "insertions": insertions,
998
+ "deletions": deletions,
999
+ "range": rng,
1000
+ }
1001
+ return diff_stat, changed
1002
+
1003
+
1004
+ def _review_summary(change_dir: Path, existing: dict[str, Any] | None) -> dict[str, Any]:
1005
+ base = {
1006
+ "status": "ADVISORY",
1007
+ "red": 0,
1008
+ "yellow": 0,
1009
+ "redFixed": 0,
1010
+ "redConfirmed": 0,
1011
+ "yellowFixed": 0,
1012
+ "yellowDeferred": 0,
1013
+ "summary": "",
1014
+ }
1015
+ if existing and isinstance(existing.get("reviewSummary"), dict):
1016
+ merged = dict(base)
1017
+ merged.update(existing["reviewSummary"])
1018
+ return merged
1019
+ if not find_review_reports(change_dir):
1020
+ log_text = load_execution_log(change_dir)
1021
+ if not re.search(r"harness-review|##\s*review", log_text, re.IGNORECASE):
1022
+ base["status"] = "ADVISORY_NOT_RUN"
1023
+ return base
1024
+
1025
+
1026
+ def collect_summary_data(
1027
+ change_dir: Path,
1028
+ *,
1029
+ before_manifest: dict[str, Any] | None = None,
1030
+ after_manifest: dict[str, Any] | None = None,
1031
+ compare_result: dict[str, Any] | None = None,
1032
+ write: bool = True,
1033
+ for_replay: bool = False,
1034
+ ) -> dict[str, Any]:
1035
+ """Build schema 2.2 summary-data from events/ledger/log/manifest/reports."""
1036
+ template = load_template()
1037
+ data = _deepcopy_json(template)
1038
+ # Clear placeholder strings from template
1039
+ data["schemaVersion"] = SCHEMA_VERSION
1040
+ change_name = infer_change_name(change_dir)
1041
+ data["changeName"] = change_name
1042
+
1043
+ sources: list[str] = []
1044
+ events: list[dict[str, Any]] = []
1045
+ events_file = he.events_path(change_dir)
1046
+ has_events = events_file.is_file() and events_file.stat().st_size > 0
1047
+ if has_events:
1048
+ try:
1049
+ events = he.load_events(events_file)
1050
+ sources.append("events.ndjson")
1051
+ except ValueError:
1052
+ events = []
1053
+
1054
+ ledger = load_ledger(change_dir)
1055
+ if ledger:
1056
+ sources.append("evidence/verification-ledger.json")
1057
+
1058
+ log_text = load_execution_log(change_dir)
1059
+ if log_text:
1060
+ sources.append("logs/execution-log.md")
1061
+
1062
+ existing = load_existing_summary(change_dir)
1063
+ if existing and for_replay:
1064
+ sources.append("reports/final/summary-data.json")
1065
+
1066
+ # Prefer existing summary fields when replaying old archives (golden-stable).
1067
+ if for_replay and existing:
1068
+ for key in (
1069
+ "businessGoal",
1070
+ "finalStatus",
1071
+ "finalCommit",
1072
+ "finalCommitBranch",
1073
+ "baseCommit",
1074
+ "diffStat",
1075
+ "stageStatus",
1076
+ "durations",
1077
+ "skillCalls",
1078
+ "verification",
1079
+ "timeline",
1080
+ "changedFiles",
1081
+ "artifacts",
1082
+ "reviewSummary",
1083
+ "archiveManifest",
1084
+ "uncommittedTestEvidence",
1085
+ "maintenanceNotes",
1086
+ "knownRisks",
1087
+ "manualActions",
1088
+ ):
1089
+ if key in existing:
1090
+ data[key] = _deepcopy_json(existing[key])
1091
+
1092
+ event_summary = he.build_summary(change_dir, events) if events else {
1093
+ "ok": True,
1094
+ "event_count": 0,
1095
+ "phases": {},
1096
+ "issues": [],
1097
+ }
1098
+
1099
+ # businessGoal
1100
+ if not data.get("businessGoal") or str(data.get("businessGoal")).startswith("本次"):
1101
+ if existing and existing.get("businessGoal"):
1102
+ data["businessGoal"] = existing["businessGoal"]
1103
+ else:
1104
+ data["businessGoal"] = NOT_AVAILABLE if for_replay else ""
1105
+
1106
+ # commits
1107
+ project = find_project_root(change_dir)
1108
+ if not data.get("finalCommit") or str(data.get("finalCommit")).startswith("<"):
1109
+ code, head, _ = git_run(project, "rev-parse", "HEAD")
1110
+ if code == 0 and head:
1111
+ data["finalCommit"] = head
1112
+ elif existing and existing.get("finalCommit"):
1113
+ data["finalCommit"] = existing["finalCommit"]
1114
+ else:
1115
+ data["finalCommit"] = NOT_AVAILABLE if for_replay else ""
1116
+
1117
+ if not data.get("baseCommit") or str(data.get("baseCommit")).startswith("<"):
1118
+ if ledger and ledger.get("baseCommit"):
1119
+ data["baseCommit"] = ledger["baseCommit"]
1120
+ elif existing and existing.get("baseCommit"):
1121
+ data["baseCommit"] = existing["baseCommit"]
1122
+ else:
1123
+ data["baseCommit"] = NOT_AVAILABLE if for_replay else ""
1124
+
1125
+ if not data.get("finalCommitBranch") or str(data.get("finalCommitBranch")).startswith("<"):
1126
+ code, branch, _ = git_run(project, "rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{u}")
1127
+ if code == 0 and branch:
1128
+ data["finalCommitBranch"] = branch
1129
+ elif existing and existing.get("finalCommitBranch"):
1130
+ data["finalCommitBranch"] = existing["finalCommitBranch"]
1131
+ else:
1132
+ data["finalCommitBranch"] = NOT_AVAILABLE if for_replay else ""
1133
+
1134
+ # verification
1135
+ if not for_replay or not isinstance(data.get("verification"), dict):
1136
+ unit = _ledger_unit_tests(ledger)
1137
+ api = _ledger_api_tests(ledger)
1138
+ db = _ledger_db_compat(ledger)
1139
+ if not find_test_reports(change_dir) and unit.get("run", 0) == 0:
1140
+ if "status" not in unit:
1141
+ unit["status"] = "NOT_RUN"
1142
+ if api.get("status") in {"", "OK"} and api.get("total", 0) == 0:
1143
+ api["status"] = "NOT_RUN"
1144
+ coverage = NOT_AVAILABLE
1145
+ if existing and isinstance(existing.get("verification"), dict):
1146
+ coverage = existing["verification"].get("coverageDisplay", coverage)
1147
+ data["verification"] = {
1148
+ "unitTests": unit,
1149
+ "apiTests": api,
1150
+ "dbCompatibility": db,
1151
+ "coverageDisplay": coverage,
1152
+ }
1153
+ else:
1154
+ # Ensure nested keys exist for old schema 2.1
1155
+ ver = data.setdefault("verification", {})
1156
+ ver.setdefault("unitTests", {})
1157
+ ver.setdefault("apiTests", {})
1158
+ ver.setdefault("dbCompatibility", ver.get("dbCompatibility", NOT_AVAILABLE))
1159
+ ver.setdefault("coverageDisplay", ver.get("coverageDisplay", NOT_AVAILABLE))
1160
+
1161
+ # stageStatus / finalStatus
1162
+ if not for_replay or not isinstance(data.get("stageStatus"), dict):
1163
+ data["stageStatus"] = _stage_status_from_sources(events, ledger, change_dir)
1164
+ if not for_replay or not data.get("finalStatus") or str(data.get("finalStatus")).startswith("OK |"):
1165
+ data["finalStatus"] = _compute_final_status(
1166
+ data.get("stageStatus") or {},
1167
+ data.get("verification") or {},
1168
+ )
1169
+
1170
+ # durations / skillCalls
1171
+ if events:
1172
+ data["durations"] = _durations_from_event_phases(event_summary)
1173
+ data["skillCalls"] = _skill_calls_from_stages(data["durations"].get("stages") or [])
1174
+ elif log_text and (not for_replay or not data.get("durations")):
1175
+ data["durations"] = _parse_durations_from_log(log_text)
1176
+ data["skillCalls"] = _skill_calls_from_stages(data["durations"].get("stages") or [])
1177
+ elif not data.get("durations"):
1178
+ data["durations"] = {
1179
+ "totalLabel": NOT_AVAILABLE,
1180
+ "totalMinutes": 0,
1181
+ "stages": [],
1182
+ }
1183
+ data["skillCalls"] = []
1184
+
1185
+ # diffStat / changedFiles
1186
+ if not for_replay or not data.get("changedFiles"):
1187
+ base = data.get("baseCommit")
1188
+ head = data.get("finalCommit")
1189
+ if base and head and base != NOT_AVAILABLE and head != NOT_AVAILABLE:
1190
+ diff_stat, changed = _changed_files_from_git(project, str(base), str(head))
1191
+ if changed:
1192
+ data["diffStat"] = diff_stat
1193
+ data["changedFiles"] = changed
1194
+ elif existing and existing.get("changedFiles"):
1195
+ data["changedFiles"] = existing["changedFiles"]
1196
+ data["diffStat"] = existing.get("diffStat") or diff_stat
1197
+ else:
1198
+ data["diffStat"] = diff_stat
1199
+ data["changedFiles"] = []
1200
+ elif existing and existing.get("changedFiles"):
1201
+ data["changedFiles"] = existing["changedFiles"]
1202
+ data["diffStat"] = existing.get("diffStat") or {
1203
+ "filesChanged": len(existing["changedFiles"]),
1204
+ "insertions": 0,
1205
+ "deletions": 0,
1206
+ "range": NOT_AVAILABLE,
1207
+ }
1208
+ else:
1209
+ data["diffStat"] = {
1210
+ "filesChanged": 0,
1211
+ "insertions": 0,
1212
+ "deletions": 0,
1213
+ "range": NOT_AVAILABLE if for_replay else "",
1214
+ }
1215
+ data["changedFiles"] = []
1216
+
1217
+ # artifacts (build products stay empty unless already known; reportPipeline has event artifacts)
1218
+ if not isinstance(data.get("artifacts"), list):
1219
+ data["artifacts"] = []
1220
+ if not for_replay:
1221
+ # Keep empty placeholder for build artifacts; event artifacts go to reportPipeline
1222
+ pass
1223
+
1224
+ data["reviewSummary"] = _review_summary(change_dir, existing if for_replay else None)
1225
+ data.setdefault("timeline", [])
1226
+ data.setdefault("uncommittedTestEvidence", [])
1227
+
1228
+ # Model-owned fields: leave empty placeholders on finalize; preserve on replay
1229
+ if not for_replay:
1230
+ data["maintenanceNotes"] = []
1231
+ data["knownRisks"] = []
1232
+ data["manualActions"] = []
1233
+ else:
1234
+ data.setdefault("maintenanceNotes", [])
1235
+ data.setdefault("knownRisks", [])
1236
+ data.setdefault("manualActions", [])
1237
+
1238
+ # archiveManifest
1239
+ am = {
1240
+ "movedFiles": 0,
1241
+ "generatedFiles": 0,
1242
+ "totalArchiveFiles": 0,
1243
+ "checksumStatus": "OK",
1244
+ }
1245
+ if compare_result:
1246
+ am["movedFiles"] = compare_result.get("movedFiles", 0)
1247
+ am["generatedFiles"] = compare_result.get("generatedFiles", 0)
1248
+ am["totalArchiveFiles"] = compare_result.get("totalArchiveFiles", 0)
1249
+ am["checksumStatus"] = compare_result.get("checksumStatus", "OK")
1250
+ elif after_manifest:
1251
+ am["totalArchiveFiles"] = int(after_manifest.get("fileCount") or 0)
1252
+ elif before_manifest:
1253
+ am["totalArchiveFiles"] = int(before_manifest.get("fileCount") or 0)
1254
+ elif for_replay and isinstance(data.get("archiveManifest"), dict):
1255
+ am = {**am, **data["archiveManifest"]}
1256
+ data["archiveManifest"] = am
1257
+
1258
+ # reportPipeline
1259
+ commands = _commands_from_events(events)
1260
+ if not commands and for_replay:
1261
+ # Cannot invent commands
1262
+ pass
1263
+ verification_checks = _verification_checks_from_events(events, ledger)
1264
+ pipeline_artifacts = _artifacts_from_events(events)
1265
+ if not sources:
1266
+ sources = [NOT_AVAILABLE]
1267
+
1268
+ data["reportPipeline"] = {
1269
+ "schema_version": 1,
1270
+ "generated_at": now_iso(),
1271
+ "event_count": len(events),
1272
+ "sources": sources,
1273
+ "phases": _phases_from_events_summary(event_summary),
1274
+ "commands": commands,
1275
+ "verificationChecks": verification_checks,
1276
+ "artifacts": pipeline_artifacts,
1277
+ "validationIssues": [],
1278
+ }
1279
+
1280
+ # Fill any remaining template placeholders that look like instructions
1281
+ for key in list(data.keys()):
1282
+ val = data[key]
1283
+ if isinstance(val, str) and ("|" in val and "OK" in val and len(val) < 80):
1284
+ # template enum hint left behind
1285
+ if for_replay:
1286
+ data[key] = existing.get(key, NOT_AVAILABLE) if existing else NOT_AVAILABLE
1287
+
1288
+ if write:
1289
+ out_path = change_dir / "reports" / "final" / "summary-data.json"
1290
+ write_json(out_path, data)
1291
+
1292
+ return data
1293
+
1294
+
1295
+ # ---------------------------------------------------------------------------
1296
+ # render
1297
+ # ---------------------------------------------------------------------------
1298
+
1299
+
1300
+ def resolve_node_path(project_root: Path) -> str | None:
1301
+ profile = project_root / ".harness" / "config" / "build-profile.json"
1302
+ if profile.is_file():
1303
+ try:
1304
+ data = read_json(profile)
1305
+ node = (data.get("toolPaths") or {}).get("node")
1306
+ if node and Path(str(node)).exists():
1307
+ return str(node)
1308
+ except (OSError, json.JSONDecodeError, TypeError):
1309
+ pass
1310
+ return shutil.which("node")
1311
+
1312
+
1313
+ def render_fallback_html(summary: dict[str, Any]) -> str:
1314
+ """Render escaped, deterministic HTML with every validate-required fact.
1315
+
1316
+ Used when the Node renderer is unavailable or fails. No timestamps / random
1317
+ data; all dynamic values are HTML-escaped via _html_escape.
1318
+ """
1319
+ def esc(v: Any) -> str:
1320
+ return _html_escape("" if v is None else str(v))
1321
+
1322
+ parts: list[str] = [
1323
+ "<!DOCTYPE html>",
1324
+ '<html lang="zh-CN"><head><meta charset="utf-8">',
1325
+ "<title>harness final-summary (python fallback)</title>",
1326
+ "</head><body>",
1327
+ "<h1>变更最终报告(Python fallback 渲染)</h1>",
1328
+ f'<h2 id="changeName">{esc(summary.get("changeName"))}</h2>',
1329
+ f'<p><strong>finalStatus</strong>: '
1330
+ f'<span id="finalStatus">{esc(summary.get("finalStatus"))}</span></p>',
1331
+ ]
1332
+
1333
+ pipeline = summary.get("reportPipeline") or {}
1334
+ cmds = pipeline.get("commands") or []
1335
+ parts.append("<h3>Commands</h3><ul>")
1336
+ for c in cmds:
1337
+ parts.append(
1338
+ f"<li><code>{esc(c.get('command'))}</code> "
1339
+ f"exitCode={esc(c.get('exit_code'))}</li>"
1340
+ )
1341
+ parts.append("</ul>")
1342
+
1343
+ ver = summary.get("verification") or {}
1344
+ unit = ver.get("unitTests") or {}
1345
+ api = ver.get("apiTests") or {}
1346
+ parts.append("<h3>Verification</h3>")
1347
+ parts.append(
1348
+ "<p>unitTests: run={run} failures={failures} errors={errors} "
1349
+ "skipped={skipped} passRate={passRate} status={status}</p>".format(
1350
+ run=esc(unit.get("run")),
1351
+ failures=esc(unit.get("failures")),
1352
+ errors=esc(unit.get("errors")),
1353
+ skipped=esc(unit.get("skipped")),
1354
+ passRate=esc(unit.get("passRate")),
1355
+ status=esc(unit.get("status")),
1356
+ )
1357
+ )
1358
+ parts.append(
1359
+ "<p>apiTests: status={status} total={total} passed={passed} "
1360
+ "failed={failed} blocked={blocked}</p>".format(
1361
+ status=esc(api.get("status")),
1362
+ total=esc(api.get("total")),
1363
+ passed=esc(api.get("passed")),
1364
+ failed=esc(api.get("failed")),
1365
+ blocked=esc(api.get("blocked")),
1366
+ )
1367
+ )
1368
+ parts.append(f"<p>dbCompatibility: {esc(ver.get('dbCompatibility'))}</p>")
1369
+
1370
+ parts.append("<h3>Changed Files</h3><ul>")
1371
+ for f in summary.get("changedFiles") or []:
1372
+ parts.append(
1373
+ f"<li>{esc(f.get('path'))} +{esc(f.get('insertions'))} "
1374
+ f"-{esc(f.get('deletions'))}</li>"
1375
+ )
1376
+ parts.append("</ul>")
1377
+
1378
+ am = summary.get("archiveManifest") or {}
1379
+ parts.append("<h3>Archive Manifest</h3>")
1380
+ parts.append(
1381
+ "<p>movedFiles={moved} generatedFiles={gen} totalArchiveFiles={total} "
1382
+ "checksumStatus={cs}</p>".format(
1383
+ moved=esc(am.get("movedFiles")),
1384
+ gen=esc(am.get("generatedFiles")),
1385
+ total=esc(am.get("totalArchiveFiles")),
1386
+ cs=esc(am.get("checksumStatus")),
1387
+ )
1388
+ )
1389
+
1390
+ def _list_section(title: str, items: Any) -> None:
1391
+ parts.append(f"<h3>{esc(title)}</h3><ul>")
1392
+ for it in items or []:
1393
+ parts.append(f"<li>{esc(it)}</li>")
1394
+ parts.append("</ul>")
1395
+
1396
+ _list_section("Known Risks", summary.get("knownRisks"))
1397
+ _list_section("Manual Actions", summary.get("manualActions"))
1398
+ _list_section("Maintenance Notes", summary.get("maintenanceNotes"))
1399
+
1400
+ parts.append("</body></html>")
1401
+ return "\n".join(parts) + "\n"
1402
+
1403
+
1404
+ def render_final_summary(
1405
+ change_dir: Path,
1406
+ summary_path: Path,
1407
+ ) -> dict[str, Any]:
1408
+ """Render final-summary.html via Node; fall back to a Python renderer.
1409
+
1410
+ Returns ``{ok, renderer, fallbackReason, out_path}``:
1411
+ - Node success -> renderer="node".
1412
+ - Node unavailable/timeout/non-zero/no-file -> Python fallback; success ->
1413
+ renderer="python-fallback" (fallbackReason carries the node failure cause).
1414
+ - Both fail or produce no file -> ok=False (caller must restore + exit non-0).
1415
+ """
1416
+ out_path = change_dir / "reports" / "final" / "final-summary.html"
1417
+ project = find_project_root(change_dir)
1418
+ node = resolve_node_path(project)
1419
+ fallback_reason = ""
1420
+
1421
+ if node and RENDER_SCRIPT.is_file():
1422
+ out_path.parent.mkdir(parents=True, exist_ok=True)
1423
+ try:
1424
+ proc = subprocess.run(
1425
+ [
1426
+ node,
1427
+ str(RENDER_SCRIPT),
1428
+ "--summary",
1429
+ str(summary_path),
1430
+ "--out",
1431
+ str(out_path),
1432
+ ],
1433
+ capture_output=True,
1434
+ text=True,
1435
+ encoding="utf-8",
1436
+ errors="replace",
1437
+ timeout=60,
1438
+ check=False,
1439
+ )
1440
+ if proc.returncode == 0 and out_path.is_file():
1441
+ return {
1442
+ "ok": True,
1443
+ "renderer": "node",
1444
+ "fallbackReason": "",
1445
+ "out_path": str(out_path),
1446
+ }
1447
+ fallback_reason = (
1448
+ f"node render exit {proc.returncode}: "
1449
+ f"{(proc.stderr or proc.stdout or '')[:200]}"
1450
+ )
1451
+ except (OSError, subprocess.TimeoutExpired) as exc:
1452
+ fallback_reason = f"node render failed: {exc}"
1453
+ else:
1454
+ fallback_reason = (
1455
+ "node unavailable" if not node else f"renderer missing: {RENDER_SCRIPT}"
1456
+ )
1457
+
1458
+ # Python fallback
1459
+ try:
1460
+ summary = read_json(summary_path)
1461
+ html = render_fallback_html(summary)
1462
+ out_path.parent.mkdir(parents=True, exist_ok=True)
1463
+ out_path.write_text(html, encoding="utf-8", newline="\n")
1464
+ except (OSError, json.JSONDecodeError, ValueError) as exc:
1465
+ return {
1466
+ "ok": False,
1467
+ "renderer": "none",
1468
+ "fallbackReason": f"{fallback_reason}; fallback failed: {exc}",
1469
+ "out_path": str(out_path),
1470
+ }
1471
+ if out_path.is_file():
1472
+ return {
1473
+ "ok": True,
1474
+ "renderer": "python-fallback",
1475
+ "fallbackReason": fallback_reason,
1476
+ "out_path": str(out_path),
1477
+ }
1478
+ return {
1479
+ "ok": False,
1480
+ "renderer": "none",
1481
+ "fallbackReason": f"{fallback_reason}; fallback produced no file",
1482
+ "out_path": str(out_path),
1483
+ }
1484
+
1485
+
1486
+ # ---------------------------------------------------------------------------
1487
+ # validate
1488
+ # ---------------------------------------------------------------------------
1489
+
1490
+
1491
+ def validate_summary(
1492
+ summary: dict[str, Any],
1493
+ html_path: Path | None,
1494
+ *,
1495
+ render_skipped: bool = False,
1496
+ ) -> dict[str, Any]:
1497
+ """Validate final-summary covers summary-data key facts (in-process)."""
1498
+ issues: list[dict[str, str]] = []
1499
+
1500
+ change_id = str(summary.get("changeName") or "")
1501
+ html = ""
1502
+ if html_path and html_path.is_file():
1503
+ try:
1504
+ html = html_path.read_text(encoding="utf-8-sig")
1505
+ except OSError as exc:
1506
+ issues.append(
1507
+ {
1508
+ "code": "missing-final-report",
1509
+ "severity": "error",
1510
+ "message": f"cannot read final-summary: {exc}",
1511
+ }
1512
+ )
1513
+ else:
1514
+ # Task 2 (§4.1 rule 5): 不再存在"没有 HTML 但只 warning"的分支。
1515
+ # 缺 final-summary 恒为 error;finalize 会 restore + exit 非 0。
1516
+ issues.append(
1517
+ {
1518
+ "code": "missing-final-report",
1519
+ "severity": "error",
1520
+ "message": "reports/final/final-summary.html not found",
1521
+ }
1522
+ )
1523
+
1524
+ def has_text(needle: str) -> bool:
1525
+ if not needle or needle == NOT_AVAILABLE:
1526
+ return True
1527
+ return needle in html
1528
+
1529
+ # change id
1530
+ if html and change_id and not has_text(change_id):
1531
+ issues.append(
1532
+ {
1533
+ "code": "missing-change-id",
1534
+ "severity": "error",
1535
+ "message": f"final-summary missing change id '{change_id}'",
1536
+ }
1537
+ )
1538
+
1539
+ # key commands — stock render-summary.mjs does not embed reportPipeline.commands.
1540
+ # Require commands to be present in summary-data; HTML absence is warning only.
1541
+ commands = (summary.get("reportPipeline") or {}).get("commands") or []
1542
+ if html:
1543
+ for cmd in commands[:8]:
1544
+ c = str(cmd.get("command") or "").strip()
1545
+ if not c:
1546
+ continue
1547
+ fragment = c if len(c) <= 60 else c[:60]
1548
+ token = c.split()[-1] if c.split() else c
1549
+ in_html = (
1550
+ fragment in html
1551
+ or _html_escape(fragment) in html
1552
+ or (len(token) >= 4 and (token in html or _html_escape(token) in html))
1553
+ )
1554
+ if not in_html:
1555
+ issues.append(
1556
+ {
1557
+ "code": "missing-command",
1558
+ "severity": "warning",
1559
+ "message": (
1560
+ f"final-summary HTML omits command (renderer may not "
1561
+ f"embed commands): {c}"
1562
+ ),
1563
+ }
1564
+ )
1565
+
1566
+ # verification
1567
+ ver = summary.get("verification") or {}
1568
+ unit = ver.get("unitTests") or {}
1569
+ api = ver.get("apiTests") or {}
1570
+ pass_rate = unit.get("passRate")
1571
+ if pass_rate and pass_rate != NOT_AVAILABLE and str(pass_rate) not in html:
1572
+ issues.append(
1573
+ {
1574
+ "code": "missing-verification",
1575
+ "severity": "warning",
1576
+ "message": f"unitTests.passRate '{pass_rate}' not in final-summary",
1577
+ }
1578
+ )
1579
+ api_status = str(api.get("status") or "")
1580
+ if api_status and api_status not in {"", NOT_AVAILABLE} and api_status not in html:
1581
+ # Renderer shows api status; soft warning
1582
+ if api_status in {"USER_SKIPPED", "BLOCKED_BY_DBA", "NOT_RUN", "PARTIAL"}:
1583
+ issues.append(
1584
+ {
1585
+ "code": "missing-verification",
1586
+ "severity": "warning",
1587
+ "message": f"apiTests.status '{api_status}' not visible in final-summary",
1588
+ }
1589
+ )
1590
+
1591
+ # artifacts / summary-data path hints
1592
+ am = summary.get("archiveManifest") or {}
1593
+ total = am.get("totalArchiveFiles")
1594
+ if total is not None and str(total) not in html:
1595
+ issues.append(
1596
+ {
1597
+ "code": "missing-artifact",
1598
+ "severity": "warning",
1599
+ "message": "archiveManifest.totalArchiveFiles not reflected in final-summary",
1600
+ }
1601
+ )
1602
+
1603
+ # risks / manual actions — empty arrays are OK (placeholder)
1604
+ for risk in summary.get("knownRisks") or []:
1605
+ text = str(risk)
1606
+ if text and text not in html and _html_escape(text) not in html:
1607
+ issues.append(
1608
+ {
1609
+ "code": "missing-risk",
1610
+ "severity": "warning",
1611
+ "message": f"knownRisk not in final-summary: {text[:80]}",
1612
+ }
1613
+ )
1614
+
1615
+ # status contradiction
1616
+ final_status = str(summary.get("finalStatus") or "")
1617
+ has_skip = api_status == "USER_SKIPPED" or str(
1618
+ ver.get("dbCompatibility") or ""
1619
+ ) == "BLOCKED_BY_DBA"
1620
+ has_fail_ver = int(unit.get("failures") or 0) > 0 or int(unit.get("errors") or 0) > 0
1621
+ has_fail_ver = has_fail_ver or int(api.get("failed") or 0) > 0
1622
+ stage = summary.get("stageStatus") or {}
1623
+ has_fail_stage = any(str(v).upper() == "FAIL" for v in stage.values())
1624
+
1625
+ if (has_skip or has_fail_ver or has_fail_stage) and final_status == "OK":
1626
+ issues.append(
1627
+ {
1628
+ "code": "status-contradiction",
1629
+ "severity": "error",
1630
+ "message": (
1631
+ "finalStatus is pure OK but USER_SKIPPED/BLOCKED_BY_DBA/"
1632
+ "failed verification present"
1633
+ ),
1634
+ }
1635
+ )
1636
+ if has_skip and html:
1637
+ if re.search(r">\s*OK\s*<", html) and "CONDITIONAL" not in html.upper():
1638
+ if final_status != "CONDITIONAL_OK":
1639
+ issues.append(
1640
+ {
1641
+ "code": "status-contradiction",
1642
+ "severity": "error",
1643
+ "message": "final-summary shows pure OK despite USER_SKIPPED/BLOCKED",
1644
+ }
1645
+ )
1646
+ else:
1647
+ # No HTML (and not render_skipped handled above): still check data-level status rules
1648
+ ver = summary.get("verification") or {}
1649
+ unit = ver.get("unitTests") or {}
1650
+ api = ver.get("apiTests") or {}
1651
+ api_status = str(api.get("status") or "")
1652
+ final_status = str(summary.get("finalStatus") or "")
1653
+ has_skip = api_status == "USER_SKIPPED" or str(
1654
+ ver.get("dbCompatibility") or ""
1655
+ ) == "BLOCKED_BY_DBA"
1656
+ has_fail_ver = int(unit.get("failures") or 0) > 0 or int(unit.get("errors") or 0) > 0
1657
+ has_fail_ver = has_fail_ver or int(api.get("failed") or 0) > 0
1658
+ stage = summary.get("stageStatus") or {}
1659
+ has_fail_stage = any(str(v).upper() == "FAIL" for v in stage.values())
1660
+ if (has_skip or has_fail_ver or has_fail_stage) and final_status == "OK":
1661
+ issues.append(
1662
+ {
1663
+ "code": "status-contradiction",
1664
+ "severity": "error",
1665
+ "message": (
1666
+ "finalStatus is pure OK but USER_SKIPPED/BLOCKED_BY_DBA/"
1667
+ "failed verification present"
1668
+ ),
1669
+ }
1670
+ )
1671
+
1672
+ errors = [i for i in issues if i.get("severity") == "error"]
1673
+ warnings = [i for i in issues if i.get("severity") != "error"]
1674
+ return {
1675
+ "ok": len(errors) == 0,
1676
+ "issues": issues,
1677
+ "error_count": len(errors),
1678
+ "warning_count": len(warnings),
1679
+ }
1680
+
1681
+
1682
+ def _html_escape(text: str) -> str:
1683
+ return (
1684
+ text.replace("&", "&amp;")
1685
+ .replace("<", "&lt;")
1686
+ .replace(">", "&gt;")
1687
+ .replace('"', "&quot;")
1688
+ .replace("'", "&#39;")
1689
+ )
1690
+
1691
+
1692
+ # ---------------------------------------------------------------------------
1693
+ # knowledge / service post-steps
1694
+ # ---------------------------------------------------------------------------
1695
+
1696
+
1697
+ def run_knowledge_poststeps(project_root: Path) -> dict[str, Any]:
1698
+ results: dict[str, Any] = {"ran": False, "steps": [], "warnings": []}
1699
+ if not KNOWLEDGE_SCRIPT.is_file():
1700
+ results["warnings"].append(f"harness_knowledge.py not found: {KNOWLEDGE_SCRIPT}")
1701
+ return results
1702
+ results["ran"] = True
1703
+ for cmd in ("ingest", "dedupe", "auto-supersede", "reverify-stale"):
1704
+ try:
1705
+ proc = subprocess.run(
1706
+ [
1707
+ sys.executable,
1708
+ str(KNOWLEDGE_SCRIPT),
1709
+ cmd,
1710
+ "--project",
1711
+ str(project_root),
1712
+ "--json",
1713
+ ],
1714
+ capture_output=True,
1715
+ text=True,
1716
+ encoding="utf-8",
1717
+ errors="replace",
1718
+ timeout=300,
1719
+ check=False,
1720
+ )
1721
+ step = {
1722
+ "command": cmd,
1723
+ "exit_code": proc.returncode,
1724
+ "ok": proc.returncode == 0,
1725
+ }
1726
+ if proc.returncode != 0:
1727
+ step["stderr"] = (proc.stderr or proc.stdout or "")[:500]
1728
+ results["warnings"].append(f"knowledge {cmd} exit {proc.returncode}")
1729
+ results["steps"].append(step)
1730
+ except (OSError, subprocess.TimeoutExpired) as exc:
1731
+ results["steps"].append({"command": cmd, "ok": False, "error": str(exc)})
1732
+ results["warnings"].append(f"knowledge {cmd} failed: {exc}")
1733
+ return results
1734
+
1735
+
1736
+ def enqueue_maintenance_outbox(project_root: Path, archive_dir: Path) -> dict[str, Any]:
1737
+ """§8.2: write a pending maintenance-outbox item instead of synchronously
1738
+ running the four knowledge subprocesses. Never rolls back the archive."""
1739
+ pending_dir = (
1740
+ project_root / ".harness" / "knowledge" / "maintenance-outbox" / "pending"
1741
+ )
1742
+ pending_dir.mkdir(parents=True, exist_ok=True)
1743
+ archive_id = archive_dir.name
1744
+ manifest = archive_dir / "evidence" / "archive-manifest-after.json"
1745
+ manifest_hash = "sha256:" + sha256_file(manifest) if manifest.is_file() else ""
1746
+ try:
1747
+ rel = archive_dir.resolve().relative_to(project_root.resolve()).as_posix()
1748
+ except ValueError:
1749
+ rel = str(archive_dir)
1750
+ item = {
1751
+ "schemaVersion": 1,
1752
+ "archiveId": archive_id,
1753
+ "archivePath": rel,
1754
+ "archiveManifestHash": manifest_hash,
1755
+ "status": "pending",
1756
+ "attempts": 0,
1757
+ "createdAt": now_iso(),
1758
+ "lastError": None,
1759
+ }
1760
+ item_path = pending_dir / f"{archive_id}.json"
1761
+ write_json(item_path, item)
1762
+ return {
1763
+ "queued": True,
1764
+ "outboxPath": str(item_path),
1765
+ "archiveId": archive_id,
1766
+ "status": "pending",
1767
+ }
1768
+
1769
+
1770
+ def run_service_stop(change_dir: Path) -> dict[str, Any]:
1771
+ if not SERVICE_SCRIPT.is_file():
1772
+ return {
1773
+ "ran": False,
1774
+ "skipped": True,
1775
+ "warning": f"harness_service.py not found; stop skipped",
1776
+ }
1777
+ try:
1778
+ proc = subprocess.run(
1779
+ [
1780
+ sys.executable,
1781
+ str(SERVICE_SCRIPT),
1782
+ "stop",
1783
+ "--change-dir",
1784
+ str(change_dir),
1785
+ "--if-started-by-ai",
1786
+ "--json",
1787
+ ],
1788
+ capture_output=True,
1789
+ text=True,
1790
+ encoding="utf-8",
1791
+ errors="replace",
1792
+ timeout=60,
1793
+ check=False,
1794
+ )
1795
+ return {
1796
+ "ran": True,
1797
+ "skipped": False,
1798
+ "exit_code": proc.returncode,
1799
+ "ok": proc.returncode == 0,
1800
+ "stdout": (proc.stdout or "")[:500],
1801
+ }
1802
+ except (OSError, subprocess.TimeoutExpired) as exc:
1803
+ return {"ran": True, "skipped": False, "ok": False, "warning": str(exc)}
1804
+
1805
+
1806
+ # ---------------------------------------------------------------------------
1807
+ # finalize
1808
+ # ---------------------------------------------------------------------------
1809
+
1810
+
1811
+ def cmd_finalize(
1812
+ change_dir: Path,
1813
+ archive_root: Path,
1814
+ *,
1815
+ skip_ingest: bool = False,
1816
+ ) -> tuple[int, dict[str, Any]]:
1817
+ """Execute the 9-step finalize pipeline. Returns (exit_code, payload)."""
1818
+ warnings: list[str] = []
1819
+ original_change_dir = change_dir.resolve()
1820
+ change_name = original_change_dir.name
1821
+ archive_root = archive_root.resolve()
1822
+ archive_root.mkdir(parents=True, exist_ok=True)
1823
+ archive_dir = archive_root / f"{today_date()}-{change_name}"
1824
+ project_root = find_project_root(original_change_dir)
1825
+
1826
+ payload: dict[str, Any] = {
1827
+ "ok": False,
1828
+ "action": "finalize",
1829
+ "change_dir": str(original_change_dir),
1830
+ "archive_dir": str(archive_dir),
1831
+ "change_name": change_name,
1832
+ "warnings": warnings,
1833
+ "steps": {},
1834
+ }
1835
+
1836
+ if not original_change_dir.is_dir():
1837
+ payload["error"] = f"change dir not found: {original_change_dir}"
1838
+ return 1, payload
1839
+
1840
+ if archive_dir.exists():
1841
+ payload["error"] = f"archive target already exists: {archive_dir}"
1842
+ return 1, payload
1843
+
1844
+ work_dir = original_change_dir
1845
+
1846
+ def _safe_append(**kwargs: Any) -> None:
1847
+ nonlocal work_dir
1848
+ try:
1849
+ append_event(work_dir, **kwargs)
1850
+ except OSError as exc:
1851
+ warnings.append(f"event append failed: {exc}")
1852
+
1853
+ # Step 9 starts here: phase.start
1854
+ _safe_append(phase="archive", type_="phase.start", note="finalize start")
1855
+
1856
+ # --- 1. before-manifest ---
1857
+ before_path = work_dir / "evidence" / "archive-manifest-before.json"
1858
+ try:
1859
+ before_manifest = generate_manifest(work_dir, before_path)
1860
+ payload["steps"]["before_manifest"] = {
1861
+ "ok": True,
1862
+ "path": str(before_path),
1863
+ "fileCount": before_manifest.get("fileCount"),
1864
+ }
1865
+ _safe_append(
1866
+ phase="archive",
1867
+ type_="artifact",
1868
+ path=str(before_path.relative_to(work_dir)).replace("\\", "/"),
1869
+ kind="manifest-before",
1870
+ )
1871
+ _safe_append(
1872
+ phase="archive",
1873
+ type_="command",
1874
+ command="generate_manifest(before)",
1875
+ exit_code=0,
1876
+ note="before-manifest",
1877
+ )
1878
+ except OSError as exc:
1879
+ payload["error"] = f"before-manifest failed: {exc}"
1880
+ _safe_append(
1881
+ phase="archive",
1882
+ type_="issue",
1883
+ code="before-manifest-failed",
1884
+ severity="error",
1885
+ message=str(exc),
1886
+ )
1887
+ return 1, payload
1888
+
1889
+ # --- 2. move ---
1890
+ try:
1891
+ archive_root.mkdir(parents=True, exist_ok=True)
1892
+ shutil.move(str(original_change_dir), str(archive_dir))
1893
+ work_dir = archive_dir
1894
+ payload["steps"]["move"] = {"ok": True, "to": str(archive_dir)}
1895
+ _safe_append(
1896
+ phase="archive",
1897
+ type_="command",
1898
+ command=f"move → {archive_dir.name}",
1899
+ exit_code=0,
1900
+ note="moved change dir to archive",
1901
+ )
1902
+ except OSError as exc:
1903
+ # Move failed: original intact, stop immediately
1904
+ payload["error"] = f"move failed: {exc}"
1905
+ payload["steps"]["move"] = {"ok": False, "error": str(exc)}
1906
+ payload["original_preserved"] = original_change_dir.is_dir()
1907
+ try:
1908
+ append_event(
1909
+ original_change_dir,
1910
+ phase="archive",
1911
+ type_="issue",
1912
+ code="move-failed",
1913
+ severity="error",
1914
+ message=str(exc),
1915
+ )
1916
+ except OSError:
1917
+ pass
1918
+ return 1, payload
1919
+
1920
+ # --- 3. collect ---
1921
+ try:
1922
+ summary = collect_summary_data(
1923
+ work_dir,
1924
+ before_manifest=before_manifest,
1925
+ write=True,
1926
+ for_replay=False,
1927
+ )
1928
+ summary_path = work_dir / "reports" / "final" / "summary-data.json"
1929
+ payload["steps"]["collect"] = {"ok": True, "path": str(summary_path)}
1930
+ _safe_append(
1931
+ phase="archive",
1932
+ type_="artifact",
1933
+ path="reports/final/summary-data.json",
1934
+ kind="summary-data",
1935
+ )
1936
+ except Exception as exc: # noqa: BLE001 — surface collect failures
1937
+ payload["error"] = f"collect failed: {exc}"
1938
+ payload["steps"]["collect"] = {"ok": False, "error": str(exc)}
1939
+ _restore_on_failure(archive_dir, original_change_dir, payload, warnings)
1940
+ return 1, payload
1941
+
1942
+ # --- 4. render (Node, else Python fallback) ---
1943
+ render_result = render_final_summary(work_dir, summary_path)
1944
+ payload["steps"]["render"] = render_result
1945
+ if not render_result.get("ok"):
1946
+ # Node + fallback both failed: restore + exit non-0 (§4.1 rule 4)。
1947
+ # 永不关闭一个没有 final-summary 的归档。
1948
+ msg = str(render_result.get("fallbackReason") or "render failed")
1949
+ payload["error"] = f"final-summary render failed: {msg}"
1950
+ _safe_append(
1951
+ phase="archive",
1952
+ type_="issue",
1953
+ code="render-failed",
1954
+ severity="error",
1955
+ message=msg,
1956
+ )
1957
+ _restore_on_failure(archive_dir, original_change_dir, payload, warnings)
1958
+ _restore_target = original_change_dir if original_change_dir.is_dir() else work_dir
1959
+ try:
1960
+ append_event(
1961
+ _restore_target,
1962
+ phase="archive",
1963
+ type_="phase.end",
1964
+ note="finalize aborted; render failed; original preserved",
1965
+ )
1966
+ except OSError:
1967
+ pass
1968
+ payload["warnings"] = warnings
1969
+ payload["ok"] = False
1970
+ return 1, payload
1971
+ renderer = render_result.get("renderer")
1972
+ if renderer == "python-fallback" and render_result.get("fallbackReason"):
1973
+ warnings.append(
1974
+ f"node render unavailable; used python-fallback: "
1975
+ f"{render_result.get('fallbackReason')}"
1976
+ )
1977
+ _safe_append(
1978
+ phase="archive",
1979
+ type_="artifact",
1980
+ path="reports/final/final-summary.html",
1981
+ kind="final-report",
1982
+ )
1983
+ _safe_append(
1984
+ phase="archive",
1985
+ type_="command",
1986
+ command=f"render-final-summary ({renderer})",
1987
+ exit_code=0,
1988
+ note=f"final-summary rendered by {renderer}",
1989
+ )
1990
+
1991
+ html_path = work_dir / "reports" / "final" / "final-summary.html"
1992
+
1993
+ # --- 5. validate (same process, no re-collect) ---
1994
+ # Refresh summary from disk (collect already wrote it); do not re-collect.
1995
+ try:
1996
+ summary = read_json(summary_path)
1997
+ except (OSError, json.JSONDecodeError):
1998
+ pass
1999
+ validate_result = validate_summary(
2000
+ summary,
2001
+ html_path if html_path.is_file() else None,
2002
+ )
2003
+ payload["steps"]["validate"] = validate_result
2004
+ summary.setdefault("reportPipeline", {})["validationIssues"] = validate_result.get(
2005
+ "issues"
2006
+ ) or []
2007
+ try:
2008
+ write_json(summary_path, summary)
2009
+ except OSError as exc:
2010
+ warnings.append(f"could not write validationIssues: {exc}")
2011
+
2012
+ for issue in validate_result.get("issues") or []:
2013
+ _safe_append(
2014
+ phase="archive",
2015
+ type_="issue",
2016
+ code=str(issue.get("code") or "validate"),
2017
+ severity=str(issue.get("severity") or "warning"),
2018
+ message=str(issue.get("message") or ""),
2019
+ )
2020
+
2021
+ # --- 6. after-manifest + compare ---
2022
+ after_path = work_dir / "evidence" / "archive-manifest-after.json"
2023
+ try:
2024
+ after_manifest = generate_manifest(work_dir, after_path)
2025
+ # Re-read before from archive (moved with the tree)
2026
+ before_in_archive = work_dir / "evidence" / "archive-manifest-before.json"
2027
+ if before_in_archive.is_file():
2028
+ before_manifest = read_json(before_in_archive)
2029
+ compare_result = compare_manifests(before_manifest, after_manifest)
2030
+ payload["steps"]["after_manifest"] = {
2031
+ "ok": True,
2032
+ "path": str(after_path),
2033
+ "compare": compare_result,
2034
+ }
2035
+ _safe_append(
2036
+ phase="archive",
2037
+ type_="artifact",
2038
+ path="evidence/archive-manifest-after.json",
2039
+ kind="manifest-after",
2040
+ )
2041
+ except (OSError, json.JSONDecodeError, ValueError) as exc:
2042
+ payload["error"] = f"after-manifest failed: {exc}"
2043
+ payload["steps"]["after_manifest"] = {"ok": False, "error": str(exc)}
2044
+ _restore_on_failure(archive_dir, original_change_dir, payload, warnings)
2045
+ return 1, payload
2046
+
2047
+ # Update archiveManifest in summary-data with compare stats
2048
+ summary["archiveManifest"] = {
2049
+ "movedFiles": compare_result.get("movedFiles", 0),
2050
+ "generatedFiles": compare_result.get("generatedFiles", 0),
2051
+ "totalArchiveFiles": compare_result.get("totalArchiveFiles", 0),
2052
+ "checksumStatus": compare_result.get("checksumStatus", "FAIL"),
2053
+ }
2054
+ try:
2055
+ write_json(summary_path, summary)
2056
+ except OSError as exc:
2057
+ warnings.append(f"could not update archiveManifest: {exc}")
2058
+
2059
+ # The first render is needed so the after-manifest includes a final report,
2060
+ # but it predates the final manifest statistics. Render and validate once
2061
+ # more from the now-final summary so HTML never contradicts summary-data.
2062
+ render_result = render_final_summary(work_dir, summary_path)
2063
+ if not render_result.get("ok"):
2064
+ payload["error"] = f"final summary re-render failed: {render_result.get('error')}"
2065
+ _restore_on_failure(archive_dir, original_change_dir, payload, warnings)
2066
+ return 1, payload
2067
+ validate_result = validate_summary(summary, html_path if html_path.is_file() else None)
2068
+ payload["steps"]["validate"] = validate_result
2069
+ summary.setdefault("reportPipeline", {})["validationIssues"] = validate_result.get("issues") or []
2070
+ write_json(summary_path, summary)
2071
+
2072
+ # --- 7. delete original only if validate+manifest OK ---
2073
+ # After move, "original" is gone; on failure we restore. On success, ensure
2074
+ # the changes path does not linger.
2075
+ validate_ok = bool(validate_result.get("ok"))
2076
+ manifest_ok = bool(compare_result.get("ok"))
2077
+ can_close = validate_ok and manifest_ok
2078
+
2079
+ if not can_close:
2080
+ issues_out = list(validate_result.get("issues") or [])
2081
+ if not manifest_ok:
2082
+ issues_out.append(
2083
+ {
2084
+ "code": "manifest-mismatch",
2085
+ "severity": "error",
2086
+ "message": (
2087
+ f"missing={compare_result.get('missing')} "
2088
+ f"mismatched={compare_result.get('mismatched')}"
2089
+ ),
2090
+ }
2091
+ )
2092
+ payload["issues"] = issues_out
2093
+ payload["error"] = "validate or manifest check failed; original change dir restored"
2094
+ payload["steps"]["delete_original"] = {"ok": False, "deleted": False}
2095
+ _restore_on_failure(archive_dir, original_change_dir, payload, warnings)
2096
+ _safe_append_restored = original_change_dir if original_change_dir.is_dir() else None
2097
+ if _safe_append_restored:
2098
+ try:
2099
+ append_event(
2100
+ _safe_append_restored,
2101
+ phase="archive",
2102
+ type_="phase.end",
2103
+ note="finalize aborted; original preserved",
2104
+ )
2105
+ except OSError:
2106
+ pass
2107
+ payload["warnings"] = warnings
2108
+ payload["ok"] = False
2109
+ return 1, payload
2110
+
2111
+ # Success path: original already relocated; confirm absence
2112
+ if original_change_dir.exists():
2113
+ try:
2114
+ shutil.rmtree(original_change_dir)
2115
+ payload["steps"]["delete_original"] = {"ok": True, "deleted": True}
2116
+ except OSError as exc:
2117
+ warnings.append(f"could not remove leftover change dir: {exc}")
2118
+ payload["steps"]["delete_original"] = {"ok": False, "error": str(exc)}
2119
+ else:
2120
+ payload["steps"]["delete_original"] = {
2121
+ "ok": True,
2122
+ "deleted": True,
2123
+ "note": "removed by move",
2124
+ }
2125
+
2126
+ _safe_append(
2127
+ phase="archive",
2128
+ type_="verification",
2129
+ name="archive-closure",
2130
+ status="ok",
2131
+ note="manifest+validate passed; original removed",
2132
+ )
2133
+
2134
+ # --- 8. maintenance outbox + service (§8.2: close no longer runs the four
2135
+ # knowledge subprocesses; it enqueues a pending outbox item and returns) ---
2136
+ if skip_ingest:
2137
+ payload["steps"]["knowledge"] = {"skipped": True, "reason": "--skip-ingest"}
2138
+ payload["knowledgeMaintenance"] = "SKIPPED"
2139
+ else:
2140
+ try:
2141
+ enqueue = enqueue_maintenance_outbox(project_root, work_dir)
2142
+ payload["steps"]["knowledge"] = enqueue
2143
+ payload["knowledgeMaintenance"] = "QUEUED"
2144
+ except OSError as exc:
2145
+ warnings.append(f"maintenance outbox enqueue failed: {exc}")
2146
+ payload["steps"]["knowledge"] = {"queued": False, "error": str(exc)}
2147
+ payload["knowledgeMaintenance"] = "NOT_QUEUED"
2148
+
2149
+ service_result = run_service_stop(work_dir)
2150
+ payload["steps"]["service_stop"] = service_result
2151
+ if service_result.get("warning"):
2152
+ warnings.append(str(service_result["warning"]))
2153
+
2154
+ _safe_append(phase="archive", type_="phase.end", note="finalize complete")
2155
+
2156
+ payload["ok"] = True
2157
+ payload["warnings"] = warnings
2158
+ payload["summary_data"] = str(summary_path)
2159
+ payload["final_summary"] = str(html_path) if html_path.is_file() else None
2160
+ return 0, payload
2161
+
2162
+
2163
+ def _restore_on_failure(
2164
+ archive_dir: Path,
2165
+ original_change_dir: Path,
2166
+ payload: dict[str, Any],
2167
+ warnings: list[str],
2168
+ ) -> None:
2169
+ """Move archive back to changes path so validate errors never lose the original."""
2170
+ if original_change_dir.exists():
2171
+ # Already present (e.g. move never happened)
2172
+ payload["original_preserved"] = True
2173
+ # Clean partial archive if present and distinct
2174
+ if archive_dir.exists() and archive_dir.resolve() != original_change_dir.resolve():
2175
+ warnings.append(
2176
+ f"partial archive left at {archive_dir} (original also present)"
2177
+ )
2178
+ return
2179
+ if not archive_dir.exists():
2180
+ payload["original_preserved"] = False
2181
+ warnings.append("restore failed: neither archive nor original exists")
2182
+ return
2183
+ try:
2184
+ original_change_dir.parent.mkdir(parents=True, exist_ok=True)
2185
+ shutil.move(str(archive_dir), str(original_change_dir))
2186
+ payload["original_preserved"] = True
2187
+ payload["restored_to"] = str(original_change_dir)
2188
+ except OSError as exc:
2189
+ payload["original_preserved"] = False
2190
+ warnings.append(f"restore move failed: {exc}; data at {archive_dir}")
2191
+
2192
+
2193
+ # ---------------------------------------------------------------------------
2194
+ # replay
2195
+ # ---------------------------------------------------------------------------
2196
+
2197
+
2198
+ def cmd_replay(
2199
+ archive_dir: Path,
2200
+ *,
2201
+ out_path: Path | None = None,
2202
+ ) -> tuple[int, dict[str, Any]]:
2203
+ """Read-only collect + validate. Never mutates archive contents."""
2204
+ archive_dir = archive_dir.resolve()
2205
+ if not archive_dir.is_dir():
2206
+ return 1, {"ok": False, "error": f"archive dir not found: {archive_dir}"}
2207
+
2208
+ # Collect without writing into the archive
2209
+ summary = collect_summary_data(archive_dir, write=False, for_replay=True)
2210
+
2211
+ html_path = archive_dir / "reports" / "final" / "final-summary.html"
2212
+ render_skipped = not html_path.is_file()
2213
+ validate_result = validate_summary(
2214
+ summary,
2215
+ html_path if html_path.is_file() else None,
2216
+ render_skipped=render_skipped,
2217
+ )
2218
+ summary.setdefault("reportPipeline", {})["validationIssues"] = (
2219
+ validate_result.get("issues") or []
2220
+ )
2221
+
2222
+ if out_path is not None:
2223
+ # Allowed to write outside the archive
2224
+ out_resolved = out_path.resolve()
2225
+ try:
2226
+ out_resolved.relative_to(archive_dir.resolve())
2227
+ inside = True
2228
+ except ValueError:
2229
+ inside = False
2230
+ if inside:
2231
+ return 1, {
2232
+ "ok": False,
2233
+ "error": "replay refuses to write inside archive dir (read-only)",
2234
+ }
2235
+ write_json(out_resolved, summary)
2236
+
2237
+ payload = {
2238
+ "ok": validate_result.get("ok", False),
2239
+ "action": "replay",
2240
+ "archive_dir": str(archive_dir),
2241
+ "change_name": summary.get("changeName"),
2242
+ "summary_data": summary,
2243
+ "validate": validate_result,
2244
+ "sources": (summary.get("reportPipeline") or {}).get("sources") or [],
2245
+ }
2246
+ # Replay itself is successful as an operation even if validate finds issues;
2247
+ # exit non-zero only on hard failures. Soft: ok mirrors validate.
2248
+ return (0 if payload["ok"] else 1), payload
2249
+
2250
+
2251
+ # ---------------------------------------------------------------------------
2252
+ # CLI
2253
+ # ---------------------------------------------------------------------------
2254
+
2255
+
2256
+ def cmd_status_cli(args: argparse.Namespace) -> int:
2257
+ change_dir = resolve_path(args.change_dir)
2258
+ result = check_status(change_dir)
2259
+ emit_json(result)
2260
+ # Checks completed → exit 0; archivable flag conveys the gate result.
2261
+ return 0 if result.get("ok") else 1
2262
+
2263
+
2264
+ def cmd_finalize_cli(args: argparse.Namespace) -> int:
2265
+ change_dir = resolve_path(args.change_dir)
2266
+ archive_root = resolve_path(args.archive_root)
2267
+ code, payload = cmd_finalize(
2268
+ change_dir,
2269
+ archive_root,
2270
+ skip_ingest=bool(args.skip_ingest),
2271
+ )
2272
+ emit_json(payload)
2273
+ return code
2274
+
2275
+
2276
+ def cmd_replay_cli(args: argparse.Namespace) -> int:
2277
+ archive_dir = resolve_path(args.archive_dir)
2278
+ out_path = resolve_path(args.out) if getattr(args, "out", None) else None
2279
+ code, payload = cmd_replay(archive_dir, out_path=out_path)
2280
+ emit_json(payload)
2281
+ return code
2282
+
2283
+
2284
+ def build_parser() -> argparse.ArgumentParser:
2285
+ parser = argparse.ArgumentParser(
2286
+ prog="harness_archive.py",
2287
+ description="Archive finalize / status / replay (D3)",
2288
+ )
2289
+ sub = parser.add_subparsers(dest="command_name", required=True)
2290
+
2291
+ p_status = sub.add_parser("status", help="pre-archive gate checks (read-only)")
2292
+ p_status.add_argument("--change-dir", required=True)
2293
+ p_status.add_argument("--json", action="store_true", default=True)
2294
+ p_status.set_defaults(func=cmd_status_cli)
2295
+
2296
+ p_fin = sub.add_parser("finalize", help="single-process archive finalize")
2297
+ p_fin.add_argument("--change-dir", required=True)
2298
+ p_fin.add_argument("--archive-root", required=True)
2299
+ p_fin.add_argument("--skip-ingest", action="store_true")
2300
+ p_fin.add_argument("--json", action="store_true", default=True)
2301
+ p_fin.set_defaults(func=cmd_finalize_cli)
2302
+
2303
+ p_rep = sub.add_parser("replay", help="read-only re-collect + validate")
2304
+ p_rep.add_argument("--archive-dir", required=True)
2305
+ p_rep.add_argument("--out", default=None, help="write summary-data JSON outside archive")
2306
+ p_rep.add_argument("--json", action="store_true", default=True)
2307
+ p_rep.set_defaults(func=cmd_replay_cli)
2308
+
2309
+ return parser
2310
+
2311
+
2312
+ def main(argv: list[str] | None = None) -> int:
2313
+ parser = build_parser()
2314
+ args = parser.parse_args(argv)
2315
+ return int(args.func(args))
2316
+
2317
+
2318
+ if __name__ == "__main__":
2319
+ raise SystemExit(main())