@hunter-harness/workflow-harness 0.4.0 → 0.4.1

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 (386) hide show
  1. package/harness/bundles/general/claude-code/.harness-build.json +1 -1
  2. package/harness/bundles/general/claude-code/harness-archive/SKILL.md +1 -1
  3. package/harness/bundles/general/claude-code/harness-codebase-map/SKILL.md +1 -1
  4. package/harness/bundles/general/claude-code/harness-execute/SKILL.md +1 -1
  5. package/harness/bundles/general/claude-code/harness-knowledge-ingest/SKILL.md +1 -1
  6. package/harness/bundles/general/claude-code/harness-knowledge-query/SKILL.md +1 -1
  7. package/harness/bundles/general/claude-code/harness-plan/SKILL.md +1 -1
  8. package/harness/bundles/general/claude-code/harness-pull/SKILL.md +1 -1
  9. package/harness/bundles/general/claude-code/harness-push/SKILL.md +1 -1
  10. package/harness/bundles/general/claude-code/harness-review/SKILL.md +1 -1
  11. package/harness/bundles/general/claude-code/harness-run/SKILL.md +1 -1
  12. package/harness/bundles/general/claude-code/harness-submit/SKILL.md +1 -1
  13. package/harness/bundles/general/claude-code/harness-sync/SKILL.md +1 -1
  14. package/harness/bundles/general/claude-code/harness-test/SKILL.md +1 -1
  15. package/harness/bundles/general/claude-code/scripts/harness_archive.py +29 -6
  16. package/harness/bundles/general/claude-code/scripts/harness_deploy.py +1 -1
  17. package/harness/bundles/general/claude-code/scripts/harness_knowledge_candidates.py +398 -0
  18. package/harness/bundles/general/codebuddy/.harness-build.json +1 -1
  19. package/harness/bundles/general/codebuddy/harness-archive/SKILL.md +1 -1
  20. package/harness/bundles/general/codebuddy/harness-codebase-map/SKILL.md +1 -1
  21. package/harness/bundles/general/codebuddy/harness-execute/SKILL.md +1 -1
  22. package/harness/bundles/general/codebuddy/harness-knowledge-ingest/SKILL.md +1 -1
  23. package/harness/bundles/general/codebuddy/harness-knowledge-query/SKILL.md +1 -1
  24. package/harness/bundles/general/codebuddy/harness-plan/SKILL.md +1 -1
  25. package/harness/bundles/general/codebuddy/harness-pull/SKILL.md +1 -1
  26. package/harness/bundles/general/codebuddy/harness-push/SKILL.md +1 -1
  27. package/harness/bundles/general/codebuddy/harness-review/SKILL.md +1 -1
  28. package/harness/bundles/general/codebuddy/harness-run/SKILL.md +1 -1
  29. package/harness/bundles/general/codebuddy/harness-submit/SKILL.md +1 -1
  30. package/harness/bundles/general/codebuddy/harness-sync/SKILL.md +1 -1
  31. package/harness/bundles/general/codebuddy/harness-test/SKILL.md +1 -1
  32. package/harness/bundles/general/codebuddy/scripts/harness_archive.py +29 -6
  33. package/harness/bundles/general/codebuddy/scripts/harness_deploy.py +1 -1
  34. package/harness/bundles/general/codebuddy/scripts/harness_knowledge_candidates.py +398 -0
  35. package/harness/bundles/general/codex/.harness-build.json +1 -1
  36. package/harness/bundles/general/codex/harness-archive/SKILL.md +1 -1
  37. package/harness/bundles/general/codex/harness-codebase-map/SKILL.md +1 -1
  38. package/harness/bundles/general/codex/harness-execute/SKILL.md +1 -1
  39. package/harness/bundles/general/codex/harness-knowledge-ingest/SKILL.md +1 -1
  40. package/harness/bundles/general/codex/harness-knowledge-query/SKILL.md +1 -1
  41. package/harness/bundles/general/codex/harness-plan/SKILL.md +1 -1
  42. package/harness/bundles/general/codex/harness-pull/SKILL.md +1 -1
  43. package/harness/bundles/general/codex/harness-push/SKILL.md +1 -1
  44. package/harness/bundles/general/codex/harness-review/SKILL.md +1 -1
  45. package/harness/bundles/general/codex/harness-run/SKILL.md +1 -1
  46. package/harness/bundles/general/codex/harness-submit/SKILL.md +1 -1
  47. package/harness/bundles/general/codex/harness-sync/SKILL.md +1 -1
  48. package/harness/bundles/general/codex/harness-test/SKILL.md +1 -1
  49. package/harness/bundles/general/codex/scripts/harness_archive.py +29 -6
  50. package/harness/bundles/general/codex/scripts/harness_deploy.py +1 -1
  51. package/harness/bundles/general/codex/scripts/harness_knowledge_candidates.py +398 -0
  52. package/harness/bundles/general/cursor/.harness-build.json +1 -1
  53. package/harness/bundles/general/cursor/harness-archive/SKILL.md +1 -1
  54. package/harness/bundles/general/cursor/harness-codebase-map/SKILL.md +1 -1
  55. package/harness/bundles/general/cursor/harness-execute/SKILL.md +1 -1
  56. package/harness/bundles/general/cursor/harness-knowledge-ingest/SKILL.md +1 -1
  57. package/harness/bundles/general/cursor/harness-knowledge-query/SKILL.md +1 -1
  58. package/harness/bundles/general/cursor/harness-plan/SKILL.md +1 -1
  59. package/harness/bundles/general/cursor/harness-pull/SKILL.md +1 -1
  60. package/harness/bundles/general/cursor/harness-push/SKILL.md +1 -1
  61. package/harness/bundles/general/cursor/harness-review/SKILL.md +1 -1
  62. package/harness/bundles/general/cursor/harness-run/SKILL.md +1 -1
  63. package/harness/bundles/general/cursor/harness-submit/SKILL.md +1 -1
  64. package/harness/bundles/general/cursor/harness-sync/SKILL.md +1 -1
  65. package/harness/bundles/general/cursor/harness-test/SKILL.md +1 -1
  66. package/harness/bundles/general/cursor/scripts/harness_archive.py +29 -6
  67. package/harness/bundles/general/cursor/scripts/harness_deploy.py +1 -1
  68. package/harness/bundles/general/cursor/scripts/harness_knowledge_candidates.py +398 -0
  69. package/harness/bundles/general/pi/.harness-build.json +6 -0
  70. package/harness/bundles/general/pi/CONTEXT.md +98 -0
  71. package/harness/bundles/general/pi/README.md +478 -0
  72. package/harness/bundles/general/pi/contracts/build-profile-v3.schema.json +302 -0
  73. package/harness/bundles/general/pi/contracts/ci-metrics.schema.json +36 -0
  74. package/harness/bundles/general/pi/contracts/doctor-capability.schema.json +43 -0
  75. package/harness/bundles/general/pi/contracts/external-platform-observation.schema.json +62 -0
  76. package/harness/bundles/general/pi/contracts/fixtures/managed-execution.json +341 -0
  77. package/harness/bundles/general/pi/contracts/process-identity.schema.json +132 -0
  78. package/harness/bundles/general/pi/contracts/run-session.schema.json +142 -0
  79. package/harness/bundles/general/pi/contracts/service-retirement-receipt.schema.json +85 -0
  80. package/harness/bundles/general/pi/contracts/service-session.schema.json +147 -0
  81. package/harness/bundles/general/pi/contracts/workflow-policy.json +574 -0
  82. package/harness/bundles/general/pi/harness-archive/SKILL.md +322 -0
  83. package/harness/bundles/general/pi/harness-archive/checklist.md +71 -0
  84. package/harness/bundles/general/pi/harness-archive/reference.md +150 -0
  85. package/harness/bundles/general/pi/harness-archive/scripts/gen-manifest.ps1 +31 -0
  86. package/harness/bundles/general/pi/harness-archive/templates/summary-data-template.json +218 -0
  87. package/harness/bundles/general/pi/harness-codebase-map/SKILL.md +116 -0
  88. package/harness/bundles/general/pi/harness-codebase-map/checklist.md +126 -0
  89. package/harness/bundles/general/pi/harness-codebase-map/reference.md +364 -0
  90. package/harness/bundles/general/pi/harness-codebase-map/templates/ARCHITECTURE.md +57 -0
  91. package/harness/bundles/general/pi/harness-codebase-map/templates/CONCERNS.md +49 -0
  92. package/harness/bundles/general/pi/harness-codebase-map/templates/CONVENTIONS.md +57 -0
  93. package/harness/bundles/general/pi/harness-codebase-map/templates/INTEGRATIONS.md +52 -0
  94. package/harness/bundles/general/pi/harness-codebase-map/templates/STACK.md +55 -0
  95. package/harness/bundles/general/pi/harness-codebase-map/templates/STRUCTURE.md +69 -0
  96. package/harness/bundles/general/pi/harness-codebase-map/templates/TESTING.md +64 -0
  97. package/harness/bundles/general/pi/harness-codebase-map/templates/map-manifest.schema.json +73 -0
  98. package/harness/bundles/general/pi/harness-codebase-map/templates/map-summary.md +64 -0
  99. package/harness/bundles/general/pi/harness-execute/SKILL.md +170 -0
  100. package/harness/bundles/general/pi/harness-knowledge-ingest/SKILL.md +103 -0
  101. package/harness/bundles/general/pi/harness-knowledge-query/SKILL.md +97 -0
  102. package/harness/bundles/general/pi/harness-plan/SKILL.md +158 -0
  103. package/harness/bundles/general/pi/harness-plan/checklist.md +243 -0
  104. package/harness/bundles/general/pi/harness-plan/protocols.md +133 -0
  105. package/harness/bundles/general/pi/harness-plan/reference.md +468 -0
  106. package/harness/bundles/general/pi/harness-pull/SKILL.md +55 -0
  107. package/harness/bundles/general/pi/harness-push/SKILL.md +83 -0
  108. package/harness/bundles/general/pi/harness-review/SKILL.md +175 -0
  109. package/harness/bundles/general/pi/harness-review/checklist.md +124 -0
  110. package/harness/bundles/general/pi/harness-review/protocols.md +69 -0
  111. package/harness/bundles/general/pi/harness-review/reference.md +114 -0
  112. package/harness/bundles/general/pi/harness-run/SKILL.md +188 -0
  113. package/harness/bundles/general/pi/harness-run/checklist.md +279 -0
  114. package/harness/bundles/general/pi/harness-run/protocols.md +141 -0
  115. package/harness/bundles/general/pi/harness-run/reference.md +886 -0
  116. package/harness/bundles/general/pi/harness-submit/SKILL.md +182 -0
  117. package/harness/bundles/general/pi/harness-submit/checklist.md +452 -0
  118. package/harness/bundles/general/pi/harness-submit/reference.md +152 -0
  119. package/harness/bundles/general/pi/harness-sync/SKILL.md +72 -0
  120. package/harness/bundles/general/pi/harness-sync/reference.md +176 -0
  121. package/harness/bundles/general/pi/harness-test/SKILL.md +250 -0
  122. package/harness/bundles/general/pi/harness-test/checklist.md +359 -0
  123. package/harness/bundles/general/pi/harness-test/pitfalls-java.md +14 -0
  124. package/harness/bundles/general/pi/harness-test/pitfalls.md +307 -0
  125. package/harness/bundles/general/pi/harness-test/reference.md +880 -0
  126. package/harness/bundles/general/pi/harness-test/scripts/runtime-helpers.mjs +216 -0
  127. package/harness/bundles/general/pi/protocols/archive-report-protocol.md +196 -0
  128. package/harness/bundles/general/pi/protocols/ci-layering-protocol.md +22 -0
  129. package/harness/bundles/general/pi/protocols/evidence-based-reporting-protocol.md +134 -0
  130. package/harness/bundles/general/pi/protocols/execution-session-protocol.md +52 -0
  131. package/harness/bundles/general/pi/protocols/external-platform-protocol.md +27 -0
  132. package/harness/bundles/general/pi/protocols/ledger-protocol.md +365 -0
  133. package/harness/bundles/general/pi/protocols/memory-closeout-protocol.md +21 -0
  134. package/harness/bundles/general/pi/protocols/model-routing-protocol.md +22 -0
  135. package/harness/bundles/general/pi/protocols/powershell-protocol.md +238 -0
  136. package/harness/bundles/general/pi/protocols/registry-governance-protocol.md +31 -0
  137. package/harness/bundles/general/pi/protocols/report-pipeline-protocol.md +172 -0
  138. package/harness/bundles/general/pi/protocols/sensitive-info-protocol.md +110 -0
  139. package/harness/bundles/general/pi/protocols/state-layout-protocol.md +209 -0
  140. package/harness/bundles/general/pi/protocols/submit-protocol.md +78 -0
  141. package/harness/bundles/general/pi/protocols/verification-graph-protocol.md +32 -0
  142. package/harness/bundles/general/pi/scripts/harness_adoption_metrics.py +391 -0
  143. package/harness/bundles/general/pi/scripts/harness_apidoc.py +53 -0
  144. package/harness/bundles/general/pi/scripts/harness_archive.py +11345 -0
  145. package/harness/bundles/general/pi/scripts/harness_change.py +1626 -0
  146. package/harness/bundles/general/pi/scripts/harness_check_gate.py +113 -0
  147. package/harness/bundles/general/pi/scripts/harness_context.py +1774 -0
  148. package/harness/bundles/general/pi/scripts/harness_deploy.py +968 -0
  149. package/harness/bundles/general/pi/scripts/harness_efficiency.py +443 -0
  150. package/harness/bundles/general/pi/scripts/harness_environment.py +1690 -0
  151. package/harness/bundles/general/pi/scripts/harness_events.py +2234 -0
  152. package/harness/bundles/general/pi/scripts/harness_events_sync.py +1425 -0
  153. package/harness/bundles/general/pi/scripts/harness_execution_contracts.py +596 -0
  154. package/harness/bundles/general/pi/scripts/harness_fixback.py +1975 -0
  155. package/harness/bundles/general/pi/scripts/harness_gate.py +3816 -0
  156. package/harness/bundles/general/pi/scripts/harness_headless.py +37 -0
  157. package/harness/bundles/general/pi/scripts/harness_integration.py +2564 -0
  158. package/harness/bundles/general/pi/scripts/harness_knowledge_candidates.py +796 -0
  159. package/harness/bundles/general/pi/scripts/harness_ledger.py +3556 -0
  160. package/harness/bundles/general/pi/scripts/harness_migration_head.py +220 -0
  161. package/harness/bundles/general/pi/scripts/harness_orchestration.py +344 -0
  162. package/harness/bundles/general/pi/scripts/harness_paths.py +676 -0
  163. package/harness/bundles/general/pi/scripts/harness_phase.py +1726 -0
  164. package/harness/bundles/general/pi/scripts/harness_plan_aggregate.py +249 -0
  165. package/harness/bundles/general/pi/scripts/harness_plan_finalize.py +1016 -0
  166. package/harness/bundles/general/pi/scripts/harness_preflight.py +650 -0
  167. package/harness/bundles/general/pi/scripts/harness_process.py +1065 -0
  168. package/harness/bundles/general/pi/scripts/harness_profile.py +1817 -0
  169. package/harness/bundles/general/pi/scripts/harness_report_model.py +442 -0
  170. package/harness/bundles/general/pi/scripts/harness_retry.py +175 -0
  171. package/harness/bundles/general/pi/scripts/harness_review.py +631 -0
  172. package/harness/bundles/general/pi/scripts/harness_runtime.py +2243 -0
  173. package/harness/bundles/general/pi/scripts/harness_service.py +3355 -0
  174. package/harness/bundles/general/pi/scripts/harness_state.py +365 -0
  175. package/harness/bundles/general/pi/scripts/harness_sync.py +407 -0
  176. package/harness/bundles/general/pi/scripts/harness_test_cleanup.py +175 -0
  177. package/harness/bundles/general/pi/scripts/harness_test_guard.py +1899 -0
  178. package/harness/bundles/general/pi/scripts/harness_test_runner.py +1586 -0
  179. package/harness/bundles/general/pi/scripts/harness_verification.py +593 -0
  180. package/harness/bundles/general/pi/scripts/harness_workflow_policy.py +266 -0
  181. package/harness/bundles/java/claude-code/.harness-build.json +1 -1
  182. package/harness/bundles/java/claude-code/harness-apidoc/SKILL.md +1 -1
  183. package/harness/bundles/java/claude-code/harness-archive/SKILL.md +1 -1
  184. package/harness/bundles/java/claude-code/harness-codebase-map/SKILL.md +1 -1
  185. package/harness/bundles/java/claude-code/harness-execute/SKILL.md +1 -1
  186. package/harness/bundles/java/claude-code/harness-knowledge-ingest/SKILL.md +1 -1
  187. package/harness/bundles/java/claude-code/harness-knowledge-query/SKILL.md +1 -1
  188. package/harness/bundles/java/claude-code/harness-package/SKILL.md +1 -1
  189. package/harness/bundles/java/claude-code/harness-plan/SKILL.md +1 -1
  190. package/harness/bundles/java/claude-code/harness-pull/SKILL.md +1 -1
  191. package/harness/bundles/java/claude-code/harness-push/SKILL.md +1 -1
  192. package/harness/bundles/java/claude-code/harness-review/SKILL.md +1 -1
  193. package/harness/bundles/java/claude-code/harness-run/SKILL.md +1 -1
  194. package/harness/bundles/java/claude-code/harness-submit/SKILL.md +1 -1
  195. package/harness/bundles/java/claude-code/harness-sync/SKILL.md +1 -1
  196. package/harness/bundles/java/claude-code/harness-test/SKILL.md +1 -1
  197. package/harness/bundles/java/claude-code/scripts/harness_archive.py +29 -6
  198. package/harness/bundles/java/claude-code/scripts/harness_deploy.py +1 -1
  199. package/harness/bundles/java/claude-code/scripts/harness_knowledge_candidates.py +398 -0
  200. package/harness/bundles/java/codebuddy/.harness-build.json +1 -1
  201. package/harness/bundles/java/codebuddy/harness-apidoc/SKILL.md +1 -1
  202. package/harness/bundles/java/codebuddy/harness-archive/SKILL.md +1 -1
  203. package/harness/bundles/java/codebuddy/harness-codebase-map/SKILL.md +1 -1
  204. package/harness/bundles/java/codebuddy/harness-execute/SKILL.md +1 -1
  205. package/harness/bundles/java/codebuddy/harness-knowledge-ingest/SKILL.md +1 -1
  206. package/harness/bundles/java/codebuddy/harness-knowledge-query/SKILL.md +1 -1
  207. package/harness/bundles/java/codebuddy/harness-package/SKILL.md +1 -1
  208. package/harness/bundles/java/codebuddy/harness-plan/SKILL.md +1 -1
  209. package/harness/bundles/java/codebuddy/harness-pull/SKILL.md +1 -1
  210. package/harness/bundles/java/codebuddy/harness-push/SKILL.md +1 -1
  211. package/harness/bundles/java/codebuddy/harness-review/SKILL.md +1 -1
  212. package/harness/bundles/java/codebuddy/harness-run/SKILL.md +1 -1
  213. package/harness/bundles/java/codebuddy/harness-submit/SKILL.md +1 -1
  214. package/harness/bundles/java/codebuddy/harness-sync/SKILL.md +1 -1
  215. package/harness/bundles/java/codebuddy/harness-test/SKILL.md +1 -1
  216. package/harness/bundles/java/codebuddy/scripts/harness_archive.py +29 -6
  217. package/harness/bundles/java/codebuddy/scripts/harness_deploy.py +1 -1
  218. package/harness/bundles/java/codebuddy/scripts/harness_knowledge_candidates.py +398 -0
  219. package/harness/bundles/java/codex/.harness-build.json +1 -1
  220. package/harness/bundles/java/codex/harness-apidoc/SKILL.md +1 -1
  221. package/harness/bundles/java/codex/harness-archive/SKILL.md +1 -1
  222. package/harness/bundles/java/codex/harness-codebase-map/SKILL.md +1 -1
  223. package/harness/bundles/java/codex/harness-execute/SKILL.md +1 -1
  224. package/harness/bundles/java/codex/harness-knowledge-ingest/SKILL.md +1 -1
  225. package/harness/bundles/java/codex/harness-knowledge-query/SKILL.md +1 -1
  226. package/harness/bundles/java/codex/harness-package/SKILL.md +1 -1
  227. package/harness/bundles/java/codex/harness-plan/SKILL.md +1 -1
  228. package/harness/bundles/java/codex/harness-pull/SKILL.md +1 -1
  229. package/harness/bundles/java/codex/harness-push/SKILL.md +1 -1
  230. package/harness/bundles/java/codex/harness-review/SKILL.md +1 -1
  231. package/harness/bundles/java/codex/harness-run/SKILL.md +1 -1
  232. package/harness/bundles/java/codex/harness-submit/SKILL.md +1 -1
  233. package/harness/bundles/java/codex/harness-sync/SKILL.md +1 -1
  234. package/harness/bundles/java/codex/harness-test/SKILL.md +1 -1
  235. package/harness/bundles/java/codex/scripts/harness_archive.py +29 -6
  236. package/harness/bundles/java/codex/scripts/harness_deploy.py +1 -1
  237. package/harness/bundles/java/codex/scripts/harness_knowledge_candidates.py +398 -0
  238. package/harness/bundles/java/cursor/.harness-build.json +1 -1
  239. package/harness/bundles/java/cursor/harness-apidoc/SKILL.md +1 -1
  240. package/harness/bundles/java/cursor/harness-archive/SKILL.md +1 -1
  241. package/harness/bundles/java/cursor/harness-codebase-map/SKILL.md +1 -1
  242. package/harness/bundles/java/cursor/harness-execute/SKILL.md +1 -1
  243. package/harness/bundles/java/cursor/harness-knowledge-ingest/SKILL.md +1 -1
  244. package/harness/bundles/java/cursor/harness-knowledge-query/SKILL.md +1 -1
  245. package/harness/bundles/java/cursor/harness-package/SKILL.md +1 -1
  246. package/harness/bundles/java/cursor/harness-plan/SKILL.md +1 -1
  247. package/harness/bundles/java/cursor/harness-pull/SKILL.md +1 -1
  248. package/harness/bundles/java/cursor/harness-push/SKILL.md +1 -1
  249. package/harness/bundles/java/cursor/harness-review/SKILL.md +1 -1
  250. package/harness/bundles/java/cursor/harness-run/SKILL.md +1 -1
  251. package/harness/bundles/java/cursor/harness-submit/SKILL.md +1 -1
  252. package/harness/bundles/java/cursor/harness-sync/SKILL.md +1 -1
  253. package/harness/bundles/java/cursor/harness-test/SKILL.md +1 -1
  254. package/harness/bundles/java/cursor/scripts/harness_archive.py +29 -6
  255. package/harness/bundles/java/cursor/scripts/harness_deploy.py +1 -1
  256. package/harness/bundles/java/cursor/scripts/harness_knowledge_candidates.py +398 -0
  257. package/harness/bundles/java/pi/.harness-build.json +6 -0
  258. package/harness/bundles/java/pi/CONTEXT.md +98 -0
  259. package/harness/bundles/java/pi/README.md +478 -0
  260. package/harness/bundles/java/pi/contracts/build-profile-v3.schema.json +302 -0
  261. package/harness/bundles/java/pi/contracts/ci-metrics.schema.json +36 -0
  262. package/harness/bundles/java/pi/contracts/doctor-capability.schema.json +43 -0
  263. package/harness/bundles/java/pi/contracts/external-platform-observation.schema.json +62 -0
  264. package/harness/bundles/java/pi/contracts/fixtures/managed-execution.json +341 -0
  265. package/harness/bundles/java/pi/contracts/process-identity.schema.json +132 -0
  266. package/harness/bundles/java/pi/contracts/run-session.schema.json +142 -0
  267. package/harness/bundles/java/pi/contracts/service-retirement-receipt.schema.json +85 -0
  268. package/harness/bundles/java/pi/contracts/service-session.schema.json +147 -0
  269. package/harness/bundles/java/pi/contracts/workflow-policy.json +574 -0
  270. package/harness/bundles/java/pi/harness-apidoc/SKILL.md +97 -0
  271. package/harness/bundles/java/pi/harness-apidoc/checklist.md +142 -0
  272. package/harness/bundles/java/pi/harness-apidoc/reference.md +233 -0
  273. package/harness/bundles/java/pi/harness-archive/SKILL.md +322 -0
  274. package/harness/bundles/java/pi/harness-archive/checklist.md +71 -0
  275. package/harness/bundles/java/pi/harness-archive/reference.md +150 -0
  276. package/harness/bundles/java/pi/harness-archive/scripts/gen-manifest.ps1 +31 -0
  277. package/harness/bundles/java/pi/harness-archive/templates/summary-data-template.json +218 -0
  278. package/harness/bundles/java/pi/harness-codebase-map/SKILL.md +116 -0
  279. package/harness/bundles/java/pi/harness-codebase-map/checklist.md +126 -0
  280. package/harness/bundles/java/pi/harness-codebase-map/reference.md +364 -0
  281. package/harness/bundles/java/pi/harness-codebase-map/templates/ARCHITECTURE.md +57 -0
  282. package/harness/bundles/java/pi/harness-codebase-map/templates/CONCERNS.md +49 -0
  283. package/harness/bundles/java/pi/harness-codebase-map/templates/CONVENTIONS.md +57 -0
  284. package/harness/bundles/java/pi/harness-codebase-map/templates/INTEGRATIONS.md +52 -0
  285. package/harness/bundles/java/pi/harness-codebase-map/templates/STACK.md +55 -0
  286. package/harness/bundles/java/pi/harness-codebase-map/templates/STRUCTURE.md +69 -0
  287. package/harness/bundles/java/pi/harness-codebase-map/templates/TESTING.md +64 -0
  288. package/harness/bundles/java/pi/harness-codebase-map/templates/map-manifest.schema.json +73 -0
  289. package/harness/bundles/java/pi/harness-codebase-map/templates/map-summary.md +64 -0
  290. package/harness/bundles/java/pi/harness-execute/SKILL.md +170 -0
  291. package/harness/bundles/java/pi/harness-knowledge-ingest/SKILL.md +103 -0
  292. package/harness/bundles/java/pi/harness-knowledge-query/SKILL.md +97 -0
  293. package/harness/bundles/java/pi/harness-package/SKILL.md +94 -0
  294. package/harness/bundles/java/pi/harness-package/checklist.md +322 -0
  295. package/harness/bundles/java/pi/harness-package/reference.md +210 -0
  296. package/harness/bundles/java/pi/harness-plan/SKILL.md +158 -0
  297. package/harness/bundles/java/pi/harness-plan/checklist.md +243 -0
  298. package/harness/bundles/java/pi/harness-plan/protocols.md +133 -0
  299. package/harness/bundles/java/pi/harness-plan/reference.md +468 -0
  300. package/harness/bundles/java/pi/harness-pull/SKILL.md +55 -0
  301. package/harness/bundles/java/pi/harness-push/SKILL.md +83 -0
  302. package/harness/bundles/java/pi/harness-review/SKILL.md +175 -0
  303. package/harness/bundles/java/pi/harness-review/checklist.md +124 -0
  304. package/harness/bundles/java/pi/harness-review/protocols.md +69 -0
  305. package/harness/bundles/java/pi/harness-review/reference.md +114 -0
  306. package/harness/bundles/java/pi/harness-run/SKILL.md +206 -0
  307. package/harness/bundles/java/pi/harness-run/checklist.md +252 -0
  308. package/harness/bundles/java/pi/harness-run/protocols.md +141 -0
  309. package/harness/bundles/java/pi/harness-run/reference.md +820 -0
  310. package/harness/bundles/java/pi/harness-submit/SKILL.md +189 -0
  311. package/harness/bundles/java/pi/harness-submit/checklist.md +452 -0
  312. package/harness/bundles/java/pi/harness-submit/reference.md +152 -0
  313. package/harness/bundles/java/pi/harness-sync/SKILL.md +72 -0
  314. package/harness/bundles/java/pi/harness-sync/reference.md +176 -0
  315. package/harness/bundles/java/pi/harness-test/SKILL.md +264 -0
  316. package/harness/bundles/java/pi/harness-test/checklist.md +320 -0
  317. package/harness/bundles/java/pi/harness-test/pitfalls-java.md +16 -0
  318. package/harness/bundles/java/pi/harness-test/pitfalls.md +307 -0
  319. package/harness/bundles/java/pi/harness-test/reference.md +792 -0
  320. package/harness/bundles/java/pi/harness-test/scripts/runtime-helpers.mjs +216 -0
  321. package/harness/bundles/java/pi/protocols/archive-report-protocol.md +196 -0
  322. package/harness/bundles/java/pi/protocols/ci-layering-protocol.md +22 -0
  323. package/harness/bundles/java/pi/protocols/evidence-based-reporting-protocol.md +134 -0
  324. package/harness/bundles/java/pi/protocols/execution-session-protocol.md +52 -0
  325. package/harness/bundles/java/pi/protocols/external-platform-protocol.md +27 -0
  326. package/harness/bundles/java/pi/protocols/ledger-protocol.md +365 -0
  327. package/harness/bundles/java/pi/protocols/memory-closeout-protocol.md +21 -0
  328. package/harness/bundles/java/pi/protocols/model-routing-protocol.md +22 -0
  329. package/harness/bundles/java/pi/protocols/powershell-protocol.md +238 -0
  330. package/harness/bundles/java/pi/protocols/registry-governance-protocol.md +31 -0
  331. package/harness/bundles/java/pi/protocols/report-pipeline-protocol.md +172 -0
  332. package/harness/bundles/java/pi/protocols/sensitive-info-protocol.md +110 -0
  333. package/harness/bundles/java/pi/protocols/state-layout-protocol.md +209 -0
  334. package/harness/bundles/java/pi/protocols/submit-protocol.md +78 -0
  335. package/harness/bundles/java/pi/protocols/verification-graph-protocol.md +32 -0
  336. package/harness/bundles/java/pi/scripts/harness_adoption_metrics.py +391 -0
  337. package/harness/bundles/java/pi/scripts/harness_apidoc.py +53 -0
  338. package/harness/bundles/java/pi/scripts/harness_archive.py +11345 -0
  339. package/harness/bundles/java/pi/scripts/harness_change.py +1626 -0
  340. package/harness/bundles/java/pi/scripts/harness_check_gate.py +113 -0
  341. package/harness/bundles/java/pi/scripts/harness_context.py +1774 -0
  342. package/harness/bundles/java/pi/scripts/harness_deploy.py +968 -0
  343. package/harness/bundles/java/pi/scripts/harness_efficiency.py +443 -0
  344. package/harness/bundles/java/pi/scripts/harness_environment.py +1690 -0
  345. package/harness/bundles/java/pi/scripts/harness_events.py +2234 -0
  346. package/harness/bundles/java/pi/scripts/harness_events_sync.py +1425 -0
  347. package/harness/bundles/java/pi/scripts/harness_execution_contracts.py +596 -0
  348. package/harness/bundles/java/pi/scripts/harness_fixback.py +1975 -0
  349. package/harness/bundles/java/pi/scripts/harness_gate.py +3816 -0
  350. package/harness/bundles/java/pi/scripts/harness_headless.py +37 -0
  351. package/harness/bundles/java/pi/scripts/harness_integration.py +2564 -0
  352. package/harness/bundles/java/pi/scripts/harness_knowledge_candidates.py +796 -0
  353. package/harness/bundles/java/pi/scripts/harness_ledger.py +3556 -0
  354. package/harness/bundles/java/pi/scripts/harness_migration_head.py +220 -0
  355. package/harness/bundles/java/pi/scripts/harness_orchestration.py +344 -0
  356. package/harness/bundles/java/pi/scripts/harness_paths.py +676 -0
  357. package/harness/bundles/java/pi/scripts/harness_phase.py +1726 -0
  358. package/harness/bundles/java/pi/scripts/harness_plan_aggregate.py +249 -0
  359. package/harness/bundles/java/pi/scripts/harness_plan_finalize.py +1016 -0
  360. package/harness/bundles/java/pi/scripts/harness_preflight.py +650 -0
  361. package/harness/bundles/java/pi/scripts/harness_process.py +1065 -0
  362. package/harness/bundles/java/pi/scripts/harness_profile.py +1817 -0
  363. package/harness/bundles/java/pi/scripts/harness_report_model.py +442 -0
  364. package/harness/bundles/java/pi/scripts/harness_retry.py +175 -0
  365. package/harness/bundles/java/pi/scripts/harness_review.py +631 -0
  366. package/harness/bundles/java/pi/scripts/harness_runtime.py +2243 -0
  367. package/harness/bundles/java/pi/scripts/harness_service.py +3355 -0
  368. package/harness/bundles/java/pi/scripts/harness_state.py +365 -0
  369. package/harness/bundles/java/pi/scripts/harness_sync.py +407 -0
  370. package/harness/bundles/java/pi/scripts/harness_test_cleanup.py +175 -0
  371. package/harness/bundles/java/pi/scripts/harness_test_guard.py +1899 -0
  372. package/harness/bundles/java/pi/scripts/harness_test_runner.py +1586 -0
  373. package/harness/bundles/java/pi/scripts/harness_verification.py +593 -0
  374. package/harness/bundles/java/pi/scripts/harness_workflow_policy.py +266 -0
  375. package/harness/manifests/general/claude-code.json +17 -17
  376. package/harness/manifests/general/codebuddy.json +17 -17
  377. package/harness/manifests/general/codex.json +17 -17
  378. package/harness/manifests/general/cursor.json +17 -17
  379. package/harness/manifests/general/pi.json +475 -0
  380. package/harness/manifests/java/claude-code.json +19 -19
  381. package/harness/manifests/java/codebuddy.json +19 -19
  382. package/harness/manifests/java/codex.json +19 -19
  383. package/harness/manifests/java/cursor.json +19 -19
  384. package/harness/manifests/java/pi.json +499 -0
  385. package/hunter-workflow-family.json +2 -2
  386. package/package.json +1 -1
@@ -0,0 +1,1016 @@
1
+ #!/usr/bin/env python3
2
+ """已发布计划的只读验收与 v2 清单解包(read-side only)。
3
+
4
+ legacy staging/finalize/republish 写侧已于 2026-08 移除(roadmap 14;采纳度
5
+ gating 由用户决策解除)。新 change 的发布走 `hunter-harness plan evidence-pack`
6
+ + `plan finalize`(v2);计划修订走 evidence-pack 重跑(expected_baseline
7
+ = present + 新 attempt)。本模块保留:
8
+ - `verify_plan`:已发布计划的只读验收(legacy receipt 与 v2 事务/journal 两种
9
+ 证据形状),供 `harness_gate.validate_plan_handoff` 与 `verify` 子命令使用;
10
+ - v2 manifest / checkpoints 解包与 plan/scenario 表解析:供 gate 与 ledger
11
+ 在只读侧消费(哈希校验后)。
12
+
13
+ 历史 change 的 legacy 产物必须保持可读;删除的是写入路径,不是读路径。
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ import argparse
19
+ import hashlib
20
+ import json
21
+ import os
22
+ import re
23
+ import stat
24
+ import sys
25
+ from pathlib import Path, PurePosixPath
26
+ from typing import Any
27
+
28
+ SCRIPTS_DIR = Path(__file__).resolve().parent
29
+ if str(SCRIPTS_DIR) not in sys.path:
30
+ sys.path.insert(0, str(SCRIPTS_DIR))
31
+
32
+ import harness_events # noqa: E402
33
+
34
+ if hasattr(sys.stdout, "reconfigure"):
35
+ sys.stdout.reconfigure(encoding="utf-8")
36
+ if hasattr(sys.stderr, "reconfigure"):
37
+ sys.stderr.reconfigure(encoding="utf-8")
38
+
39
+ SCHEMA_VERSION = 1
40
+ _LINK = re.compile(r"\[[^\]]+\]\(([^)]+)\)")
41
+ _BACKTICK = re.compile(r"`([^`\r\n]+)`")
42
+ _CODE_PATH_SUFFIXES = {
43
+ ".c", ".cc", ".cpp", ".cs", ".css", ".go", ".h", ".hpp", ".html",
44
+ ".java", ".js", ".jsx", ".kt", ".kts", ".mjs", ".php", ".py", ".rb",
45
+ ".rs", ".scss", ".sh", ".svelte", ".swift", ".ts", ".tsx", ".vue",
46
+ }
47
+ _BUILD_FILE_NAMES = {
48
+ "build.gradle", "build.gradle.kts", "cargo.toml", "composer.json", "go.mod",
49
+ "gradle.properties", "package-lock.json", "package.json", "pnpm-lock.yaml",
50
+ "pom.xml", "pyproject.toml", "requirements.txt", "settings.gradle",
51
+ "settings.gradle.kts", "tsconfig.json", "yarn.lock",
52
+ }
53
+
54
+ # C8: valid ownerPhase values (lifecycle phases that can own tasks).
55
+ VALID_OWNER_PHASES = {"plan", "execute", "review", "submit"}
56
+ SLICE_CANDIDATE_TYPES = {
57
+ "independent-release-candidate",
58
+ "child-of-aggregate",
59
+ "fixback-of",
60
+ "evidence-only",
61
+ }
62
+
63
+ # C9: map scenario priority to required evidence kind.
64
+ PRIORITY_EVIDENCE_KIND = {
65
+ "P0": "ledger",
66
+ "P1": "ledger",
67
+ "P2": "advisory",
68
+ }
69
+ _FILE_ATTRIBUTE_REPARSE_POINT = getattr(stat, "FILE_ATTRIBUTE_REPARSE_POINT", 0x400)
70
+
71
+
72
+ def _result_error(code: str, message: str) -> dict[str, Any]:
73
+ return {"ok": False, "code": code, "error": message}
74
+
75
+
76
+ class PlanParseError(ValueError):
77
+ def __init__(self, code: str, message: str) -> None:
78
+ super().__init__(message)
79
+ self.code = code
80
+
81
+
82
+ def _normalize_header(value: str) -> str:
83
+ return re.sub(r"[\s_-]+", "", value).lower()
84
+
85
+
86
+ def _column_index(
87
+ headers: list[str],
88
+ *aliases: str,
89
+ ) -> int | None:
90
+ normalized = {
91
+ _normalize_header(header): index for index, header in enumerate(headers)
92
+ }
93
+ for alias in aliases:
94
+ index = normalized.get(_normalize_header(alias))
95
+ if index is not None:
96
+ return index
97
+ return None
98
+
99
+
100
+ def _table_cells(line: str) -> list[str]:
101
+ return [cell.strip() for cell in line.strip().strip("|").split("|")]
102
+
103
+
104
+ def parse_test_scenarios(scenarios_path: Path) -> list[dict[str, Any]]:
105
+ """C9: parse test-scenarios.md tables, extracting scenario rows.
106
+
107
+ Returns a list of dicts with keys: id / priority / scenario / verification /
108
+ ownerPhase / requiredEvidenceKind (last one derived from priority).
109
+ """
110
+ text = Path(scenarios_path).read_text(encoding="utf-8-sig")
111
+ lines = text.splitlines()
112
+ scenarios: list[dict[str, Any]] = []
113
+ i = 0
114
+ while i < len(lines):
115
+ line = lines[i].strip()
116
+ if line.startswith("|"):
117
+ headers = _table_cells(line)
118
+ id_index = _column_index(headers, "ID", "#", "编号", "场景 ID")
119
+ scenario_index = _column_index(
120
+ headers,
121
+ "场景",
122
+ "场景描述",
123
+ "scenario",
124
+ "description",
125
+ )
126
+ if id_index is None or scenario_index is None:
127
+ i += 1
128
+ continue
129
+ priority_index = _column_index(headers, "优先级", "priority")
130
+ category_index = _column_index(headers, "分类", "category")
131
+ verification_index = _column_index(
132
+ headers,
133
+ "验证方式",
134
+ "verification",
135
+ "可复用证据",
136
+ )
137
+ owner_phase_index = _column_index(
138
+ headers,
139
+ "owner phase",
140
+ "ownerPhase",
141
+ "负责阶段",
142
+ )
143
+ execution_tier_index = _column_index(
144
+ headers,
145
+ "执行层级",
146
+ "execution tier",
147
+ "tier",
148
+ )
149
+ expected_index = _column_index(headers, "预期", "expected")
150
+ executable_id_index = _column_index(
151
+ headers,
152
+ "executable test ID",
153
+ "executableTestId",
154
+ "可执行测试 ID",
155
+ "测试 ID",
156
+ )
157
+ test_file_index = _column_index(
158
+ headers,
159
+ "test file",
160
+ "testFile",
161
+ "测试文件",
162
+ "spec path",
163
+ )
164
+ test_title_index = _column_index(
165
+ headers,
166
+ "test title",
167
+ "testTitle",
168
+ "测试标题",
169
+ "用例标题",
170
+ )
171
+ executable_mapping_declared = any(
172
+ index is not None
173
+ for index in (
174
+ executable_id_index,
175
+ test_file_index,
176
+ test_title_index,
177
+ )
178
+ )
179
+
180
+ row_index = i + 2
181
+ while row_index < len(lines):
182
+ row = lines[row_index].strip()
183
+ if not row.startswith("|"):
184
+ break
185
+ cells = _table_cells(row)
186
+ if max(id_index, scenario_index) >= len(cells):
187
+ raise PlanParseError(
188
+ "PLAN_SCENARIO_ROW_INVALID",
189
+ f"{scenarios_path.name}: line {row_index + 1} is missing scenario ID or description",
190
+ )
191
+ complete_row = len(cells) == len(headers)
192
+ scenario_id = cells[id_index].strip()
193
+ if not scenario_id or set(scenario_id) <= {"-", ":"}:
194
+ raise PlanParseError(
195
+ "PLAN_SCENARIO_ROW_INVALID",
196
+ f"{scenarios_path.name}: line {row_index + 1} has an empty scenario ID",
197
+ )
198
+ scenario_text = cells[scenario_index].strip()
199
+ if not scenario_text or set(scenario_text) <= {"-", ":"}:
200
+ raise PlanParseError(
201
+ "PLAN_SCENARIO_ROW_INVALID",
202
+ f"{scenarios_path.name}: line {row_index + 1} has an empty scenario description",
203
+ )
204
+ priority = (
205
+ cells[priority_index].strip().upper()
206
+ if priority_index is not None
207
+ and priority_index < len(cells)
208
+ and cells[priority_index].strip()
209
+ else "P1"
210
+ )
211
+ scenario = {
212
+ "id": scenario_id,
213
+ "priority": priority,
214
+ "scenario": scenario_text,
215
+ "ownerPhase": (
216
+ cells[owner_phase_index].strip()
217
+ if owner_phase_index is not None
218
+ and complete_row
219
+ and owner_phase_index < len(cells)
220
+ and cells[owner_phase_index].strip()
221
+ else "execute"
222
+ ),
223
+ "requiredEvidenceKind": PRIORITY_EVIDENCE_KIND.get(
224
+ priority,
225
+ "advisory",
226
+ ),
227
+ "executableMappingDeclared": executable_mapping_declared,
228
+ }
229
+ if category_index is not None and category_index < len(cells):
230
+ if cells[category_index].strip():
231
+ scenario["category"] = cells[category_index].strip()
232
+ for key, index in (
233
+ ("verification", verification_index),
234
+ ("executionTier", execution_tier_index),
235
+ ("expected", expected_index),
236
+ ("executableTestId", executable_id_index),
237
+ ("testFile", test_file_index),
238
+ ("testTitle", test_title_index),
239
+ ):
240
+ if (
241
+ complete_row
242
+ and index is not None
243
+ and index < len(cells)
244
+ and cells[index].strip()
245
+ ):
246
+ scenario[key] = cells[index].strip()
247
+ scenarios.append(scenario)
248
+ row_index += 1
249
+ i = row_index
250
+ continue
251
+ i += 1
252
+ return scenarios
253
+
254
+
255
+ def scenario_manifest_schema_version(scenarios: list[dict[str, Any]]) -> int:
256
+ required = [
257
+ item
258
+ for item in scenarios
259
+ if str(item.get("requiredEvidenceKind") or "") == "ledger"
260
+ ]
261
+ if required and all(
262
+ all(
263
+ str(item.get(field) or "").strip()
264
+ for field in ("executableTestId", "testFile", "testTitle")
265
+ )
266
+ for item in required
267
+ ):
268
+ return 2
269
+ return 1
270
+
271
+
272
+ # v2 场景键 → 门禁与 ledger 消费的 legacy 键。可执行三元是可选的:缺了只是把
273
+ # manifest 降到 schemaVersion 1(绑不上结构化执行收据),不算缺口。
274
+ _V2_SCENARIO_FIELD_MAP = (
275
+ ("scenario_id", "id"),
276
+ ("priority", "priority"),
277
+ ("required_evidence_kind", "requiredEvidenceKind"),
278
+ ("owner_phase", "ownerPhase"),
279
+ )
280
+ _V2_SCENARIO_OPTIONAL_FIELD_MAP = (
281
+ ("executable_test_id", "executableTestId"),
282
+ ("test_file", "testFile"),
283
+ ("test_title", "testTitle"),
284
+ )
285
+ V2_MANIFEST_UNSUPPORTED = "SCENARIO_MANIFEST_V2_UNSUPPORTED"
286
+
287
+
288
+ def is_v2_scenario_manifest(manifest: Any) -> bool:
289
+ """v2 plan finalize 派生的 scenario_manifest artifact 包装体。
290
+
291
+ 它没有顶层 schemaVersion,场景字段全在 content.scenarios 下且改了名。
292
+ """
293
+ return (
294
+ isinstance(manifest, dict)
295
+ and manifest.get("artifact_type") == "scenario_manifest"
296
+ )
297
+
298
+
299
+ def unpack_v2_scenario_manifest(manifest: Any) -> dict[str, Any] | None:
300
+ """v2 artifact 包装体 → 消费端的 legacy 形状;字段不齐时 fail-closed。
301
+
302
+ 返回 None 表示"这不是 v2 包装体",调用方按 legacy 原样处理。否则返回
303
+ ``{"ok": True, "manifest": {...}}``(已是 legacy 形状,后续判定逻辑对
304
+ v2 与 legacy 完全一致)或 ``{"ok": False, "code": V2_MANIFEST_UNSUPPORTED}``。
305
+
306
+ 这个函数住在 finalizer 里,是因为 legacy manifest 的 schema 本来就由本模块
307
+ 定义(``scenario_manifest_schema_version`` / ``PRIORITY_EVIDENCE_KIND``)。
308
+ 门禁与 ledger 都 import 它,两边不会各推一套解包规则。
309
+
310
+ 为什么必须逐场景校验、不能取键的并集:并集只要有**一条**场景带了
311
+ priority 就算"present",其余缺 priority 的场景会静默落进非必需集,
312
+ ``required_ids`` 随之缩水——那正是把证据门禁悄悄关掉的老路子。
313
+ """
314
+ if not is_v2_scenario_manifest(manifest):
315
+ return None
316
+ content = manifest.get("content")
317
+ raw = content.get("scenarios") if isinstance(content, dict) else None
318
+ if not isinstance(raw, list):
319
+ return {
320
+ "ok": False,
321
+ "code": V2_MANIFEST_UNSUPPORTED,
322
+ "message": "v2 scenario-manifest 的 content.scenarios 必须是数组",
323
+ "missingFields": [legacy for _, legacy in _V2_SCENARIO_FIELD_MAP],
324
+ "artifactType": "scenario_manifest",
325
+ }
326
+ scenarios: list[dict[str, Any]] = []
327
+ missing: set[str] = set()
328
+ for item in raw:
329
+ if not isinstance(item, dict):
330
+ missing.update(legacy for _, legacy in _V2_SCENARIO_FIELD_MAP)
331
+ continue
332
+ mapped: dict[str, Any] = {}
333
+ for source, legacy in _V2_SCENARIO_FIELD_MAP:
334
+ value = item.get(source)
335
+ if not str(value or "").strip():
336
+ missing.add(legacy)
337
+ continue
338
+ mapped[legacy] = value
339
+ for source, legacy in _V2_SCENARIO_OPTIONAL_FIELD_MAP:
340
+ value = item.get(source)
341
+ if str(value or "").strip():
342
+ mapped[legacy] = value
343
+ scenarios.append(mapped)
344
+ if missing:
345
+ return {
346
+ "ok": False,
347
+ "code": V2_MANIFEST_UNSUPPORTED,
348
+ "message": (
349
+ "meta/scenario-manifest.json 是 v2 plan artifact 包装体,"
350
+ "部分场景缺少消费端需要的字段;请在规划阶段补齐后重新发布,"
351
+ "不要手改派生产物"
352
+ ),
353
+ "missingFields": sorted(missing),
354
+ "artifactType": "scenario_manifest",
355
+ }
356
+ return {
357
+ "ok": True,
358
+ "manifest": {
359
+ # 与 legacy 同一条规则判版本,不另立一套。
360
+ "schemaVersion": scenario_manifest_schema_version(scenarios),
361
+ "scenarios": scenarios,
362
+ },
363
+ }
364
+
365
+
366
+ def unpack_v2_implementation_checkpoints(document: Any) -> dict[str, Any] | None:
367
+ """v2 的 implementation_checkpoints artifact 包装体 → 门禁消费的形状。
368
+
369
+ 返回 None 表示"这不是 v2 包装体",调用方按原样处理。
370
+
371
+ v2 的 content 是 ``{tasks, foundation_gate}``,而 ``checkpoint_status`` 找的是
372
+ ``checkpoints: [{id, status}]``——找不到就返回 "missing",``foundation_gate_blocks``
373
+ 随即放行。也就是说 foundation-gate 对**所有 v2 计划**是静默关闭的。信息本身是够
374
+ 的(``foundation_gate`` 就是那个状态),只是形状不同,所以这里解包而不是改文件名
375
+ ——与 scenario-manifest 同一模式。
376
+ """
377
+ if (
378
+ not isinstance(document, dict)
379
+ or document.get("artifact_type") != "implementation_checkpoints"
380
+ ):
381
+ return None
382
+ content = document.get("content")
383
+ content = content if isinstance(content, dict) else {}
384
+ status = str(content.get("foundation_gate") or "").strip() or "pending"
385
+ tasks = content.get("tasks")
386
+ return {
387
+ "schemaVersion": 1,
388
+ "checkpoints": [{"id": "foundation-gate", "status": status, "blocking": True}],
389
+ "tasks": tasks if isinstance(tasks, list) else [],
390
+ }
391
+
392
+
393
+ def parse_plan_tasks(plan_path: Path) -> list[dict[str, str]]:
394
+ """C8: parse plan.md task table rows, extracting optional ownerPhase/implementationDoneWhen/verificationPhase columns.
395
+
396
+ Returns a list of dicts with keys: # / 簇 / 任务 / ownerPhase / implementationDoneWhen /
397
+ verificationPhase / requiresExplicitAuthority (last four optional, only present when
398
+ the column header includes them).
399
+ """
400
+ text = Path(plan_path).read_text(encoding="utf-8-sig")
401
+ lines = text.splitlines()
402
+ tasks: list[dict[str, str]] = []
403
+ i = 0
404
+ while i < len(lines):
405
+ line = lines[i].strip()
406
+ if not line.startswith("|"):
407
+ i += 1
408
+ continue
409
+ headers = _table_cells(line)
410
+ number_index = _column_index(headers, "#", "任务编号", "task ID")
411
+ task_index = _column_index(headers, "任务", "task")
412
+ if number_index is None or task_index is None:
413
+ i += 1
414
+ continue
415
+ cluster_index = _column_index(headers, "簇", "cluster")
416
+ optional_indices = {
417
+ name: _column_index(headers, name)
418
+ for name in (
419
+ "ownerPhase",
420
+ "implementationDoneWhen",
421
+ "verificationPhase",
422
+ "requiresExplicitAuthority",
423
+ )
424
+ }
425
+ row_index = i + 2
426
+ while row_index < len(lines):
427
+ row = lines[row_index].strip()
428
+ if not row.startswith("|"):
429
+ break
430
+ cells = _table_cells(row)
431
+ if max(number_index, task_index) >= len(cells):
432
+ raise PlanParseError(
433
+ "PLAN_TASK_ROW_INVALID",
434
+ f"{plan_path.name}: line {row_index + 1} is missing task ID or description",
435
+ )
436
+ complete_row = len(cells) == len(headers)
437
+ number = cells[number_index].strip()
438
+ task_text = cells[task_index].strip()
439
+ if (
440
+ not number
441
+ or not task_text
442
+ or set(number) <= {"-", ":"}
443
+ or set(task_text) <= {"-", ":"}
444
+ ):
445
+ raise PlanParseError(
446
+ "PLAN_TASK_ROW_INVALID",
447
+ f"{plan_path.name}: line {row_index + 1} has an empty task ID or description",
448
+ )
449
+ task: dict[str, str] = {"num": number, "task": task_text}
450
+ if cluster_index is not None and cells[cluster_index].strip():
451
+ task["cluster"] = cells[cluster_index].strip()
452
+ for name, index in optional_indices.items():
453
+ if (
454
+ complete_row
455
+ and index is not None
456
+ and index < len(cells)
457
+ and cells[index].strip()
458
+ ):
459
+ task[name] = cells[index].strip()
460
+ tasks.append(task)
461
+ row_index += 1
462
+ i = row_index
463
+ return tasks
464
+
465
+
466
+
467
+
468
+ def _frontmatter(text: str) -> dict[str, str] | None:
469
+ lines = text.splitlines()
470
+ if not lines or lines[0].strip() != "---":
471
+ return None
472
+ data: dict[str, str] = {}
473
+ for line in lines[1:]:
474
+ if line.strip() == "---":
475
+ return data
476
+ if ":" not in line:
477
+ return None
478
+ key, value = line.split(":", 1)
479
+ data[key.strip()] = value.strip()
480
+ return None
481
+
482
+
483
+ def _required_artifact_names(change_name: str) -> set[str]:
484
+ return {
485
+ f"spec/{change_name}-design.md",
486
+ f"plans/{change_name}-plan.md",
487
+ f"plans/{change_name}-implementation-detail.md",
488
+ f"plans/{change_name}-test-scenarios.md",
489
+ "meta/gate-policy.json",
490
+ "meta/worktree.json",
491
+ }
492
+
493
+
494
+ def _is_link_or_reparse(path: Path) -> bool:
495
+ """Detect symlinks and Windows reparse points on every supported Python."""
496
+ try:
497
+ if path.is_symlink():
498
+ return True
499
+ attributes = getattr(os.lstat(path), "st_file_attributes", 0)
500
+ except OSError:
501
+ return False
502
+ return bool(attributes & _FILE_ATTRIBUTE_REPARSE_POINT)
503
+
504
+
505
+
506
+
507
+
508
+
509
+
510
+
511
+
512
+
513
+
514
+
515
+
516
+
517
+ def _receipt_artifact_targets(
518
+ change_dir: Path,
519
+ files_list: list[Any],
520
+ ) -> tuple[list[tuple[str, Path]] | None, dict[str, Any] | None]:
521
+ targets: list[tuple[str, Path]] = []
522
+ seen: set[str] = set()
523
+ for index, value in enumerate(files_list):
524
+ if not isinstance(value, str):
525
+ return None, _result_error(
526
+ "RECEIPT_FILE_PATH_INVALID",
527
+ f"receipt files[{index}] must be a string",
528
+ )
529
+ raw = value
530
+ segments = raw.split("/")
531
+ rel = PurePosixPath(raw)
532
+ invalid = (
533
+ not raw
534
+ or raw != raw.strip()
535
+ or "\\" in raw
536
+ or ":" in raw
537
+ or rel.is_absolute()
538
+ or any(segment in {"", ".", ".."} for segment in segments)
539
+ or any(segment.endswith((".", " ")) for segment in segments)
540
+ or not rel.parts
541
+ or rel.parts[0] not in {"spec", "plans", "meta"}
542
+ )
543
+ if invalid:
544
+ return None, _result_error(
545
+ "RECEIPT_FILE_PATH_INVALID",
546
+ f"receipt files[{index}] is not a safe artifact-relative path: {raw!r}",
547
+ )
548
+ normalized = rel.as_posix()
549
+ if normalized in seen:
550
+ return None, _result_error(
551
+ "RECEIPT_FILE_PATH_INVALID",
552
+ f"receipt contains duplicate artifact path: {normalized}",
553
+ )
554
+ seen.add(normalized)
555
+ target = change_dir.joinpath(*rel.parts)
556
+ cursor = change_dir
557
+ for part in rel.parts:
558
+ cursor = cursor / part
559
+ if _is_link_or_reparse(cursor):
560
+ return None, _result_error(
561
+ "RECEIPT_FILE_PATH_INVALID",
562
+ f"receipt artifact path traverses a link: {normalized}",
563
+ )
564
+ try:
565
+ target.resolve(strict=False).relative_to(change_dir)
566
+ except (OSError, RuntimeError, ValueError):
567
+ return None, _result_error(
568
+ "RECEIPT_FILE_PATH_INVALID",
569
+ f"receipt artifact path resolves outside the change directory: {normalized}",
570
+ )
571
+ targets.append((normalized, target))
572
+ return targets, None
573
+
574
+
575
+ def _verify_plan_v2(change_dir: Path) -> dict[str, Any] | None:
576
+ """v2 finalizer(TS)证据的只读验收。
577
+
578
+ v2 不产生 meta/plan-finalization.json;canonical 证据是
579
+ meta/plan-finalization-transactions/ + meta/publication-journals/。
580
+ 本路径只做结构验收:最新事务终态、匹配 journal 的 committed/readback、
581
+ durable targets 存在且字节哈希与 binding 一致。内容质量由 TS 质量层裁决,
582
+ Python 不重复审判。无 v2 证据时返回 None(调用方回退 RECEIPT_MISSING)。
583
+ """
584
+ transactions_dir = change_dir / "meta" / "plan-finalization-transactions"
585
+ if not transactions_dir.is_dir():
586
+ return None
587
+ transactions = sorted(
588
+ transactions_dir.glob("*.json"),
589
+ key=lambda path: path.stat().st_mtime,
590
+ )
591
+ if not transactions:
592
+ return None
593
+ latest: dict[str, Any] | None = None
594
+ for candidate in reversed(transactions):
595
+ try:
596
+ payload = json.loads(candidate.read_text(encoding="utf-8-sig"))
597
+ except (OSError, json.JSONDecodeError):
598
+ continue
599
+ if isinstance(payload, dict):
600
+ latest = payload
601
+ break
602
+ if latest is None:
603
+ return None
604
+ if latest.get("change_key") != change_dir.name:
605
+ return _result_error(
606
+ "RECEIPT_CHANGE_NAME_INVALID",
607
+ "v2 transaction change_key does not match the change directory",
608
+ )
609
+ if latest.get("status") != "publication_committed_event_complete":
610
+ return _result_error(
611
+ "RECEIPT_NOT_FINALIZED",
612
+ f"v2 transaction status is {latest.get('status')!r}, "
613
+ "expected 'publication_committed_event_complete'",
614
+ )
615
+ operation_id = str(latest.get("operation_id") or "")
616
+ journals_dir = change_dir / "meta" / "publication-journals"
617
+ journal: dict[str, Any] | None = None
618
+ if journals_dir.is_dir():
619
+ for candidate in sorted(journals_dir.glob("*.json")):
620
+ try:
621
+ payload = json.loads(candidate.read_text(encoding="utf-8-sig"))
622
+ except (OSError, json.JSONDecodeError):
623
+ continue
624
+ if isinstance(payload, dict) and payload.get("operation_id") == operation_id:
625
+ journal = payload
626
+ break
627
+ if journal is None:
628
+ return _result_error(
629
+ "PUBLICATION_JOURNAL_MISSING",
630
+ f"no publication journal matches v2 operation {operation_id!r}",
631
+ )
632
+ if journal.get("state") != "committed" or journal.get("readback") != "verified":
633
+ return _result_error(
634
+ "PUBLICATION_JOURNAL_NOT_COMMITTED",
635
+ "v2 publication journal is not committed with verified readback",
636
+ )
637
+ binding = journal.get("binding")
638
+ if not isinstance(binding, dict):
639
+ return _result_error("PUBLICATION_BINDING_INVALID", "v2 journal binding missing")
640
+ payload_hashes = binding.get("expected_payload_hashes")
641
+ ownership_paths = binding.get("ownership_paths")
642
+ if not isinstance(payload_hashes, dict) or not isinstance(ownership_paths, list):
643
+ return _result_error("PUBLICATION_BINDING_INVALID", "v2 binding fields malformed")
644
+ required = {
645
+ f"plans/{change_dir.name}-design.md",
646
+ f"plans/{change_dir.name}-plan.md",
647
+ f"plans/{change_dir.name}-implementation-detail.md",
648
+ f"plans/{change_dir.name}-test-scenarios.md",
649
+ # v2 发布的是派生视图 meta/plan-profile.json,不是 Python classify 写的
650
+ # meta/gate-policy.json——后者是 run/test 门禁的权威输入,不能被发布覆盖。
651
+ "meta/plan-profile.json",
652
+ "meta/worktree.json",
653
+ }
654
+ missing_required = sorted(required - set(ownership_paths))
655
+ if missing_required:
656
+ return _result_error(
657
+ "RECEIPT_FILES_INCOMPLETE",
658
+ "v2 journal omits required artifacts: " + ", ".join(missing_required),
659
+ )
660
+ for rel, expected_hash in payload_hashes.items():
661
+ if not isinstance(expected_hash, str) or not expected_hash.startswith("sha256:"):
662
+ return _result_error("PUBLICATION_BINDING_INVALID", f"{rel}: malformed hash")
663
+ target = change_dir / rel
664
+ try:
665
+ resolved = target.resolve()
666
+ resolved.relative_to(change_dir.resolve())
667
+ except (OSError, ValueError):
668
+ return _result_error(
669
+ "RECEIPT_FILE_PATH_INVALID",
670
+ f"v2 artifact path resolves outside the change directory: {rel}",
671
+ )
672
+ if not target.is_file():
673
+ return _result_error("ARTIFACT_MISSING", f"published artifact missing: {rel}")
674
+ actual_hash = "sha256:" + hashlib.sha256(target.read_bytes()).hexdigest()
675
+ if actual_hash != expected_hash:
676
+ return _result_error(
677
+ "ARTIFACT_HASH_DRIFT",
678
+ f"{rel}: content hash {actual_hash} != v2 binding {expected_hash}",
679
+ )
680
+ return {
681
+ "ok": True,
682
+ "code": "PLAN_V2_VERIFIED",
683
+ "v2": True,
684
+ "operationId": operation_id,
685
+ "runId": latest.get("run_id"),
686
+ "attempt": latest.get("attempt"),
687
+ "artifactCount": len(payload_hashes),
688
+ "status": latest.get("status"),
689
+ }
690
+
691
+
692
+ def verify_plan(change_dir: Path) -> dict[str, Any]:
693
+ """Read-only verification of a finalized plan (retro §5.8).
694
+
695
+ Validates the published artifacts, receipt and events stream without
696
+ requiring the original staging directory. Returns the artifact hash,
697
+ phase.end count/status, frontmatter, and consistency checks. Any parse
698
+ error or inconsistency results in ok=false with a non-zero exit code.
699
+ """
700
+ change_dir = change_dir.resolve()
701
+ receipt_path = change_dir / "meta" / "plan-finalization.json"
702
+ if not receipt_path.is_file():
703
+ # v2 finalizer 证据路径(canonical 事实源是 transactions + journals)
704
+ v2_result = _verify_plan_v2(change_dir)
705
+ if v2_result is not None:
706
+ return v2_result
707
+ return _result_error("RECEIPT_MISSING", f"receipt not found: {receipt_path}")
708
+
709
+ try:
710
+ receipt = json.loads(receipt_path.read_text(encoding="utf-8-sig"))
711
+ except (OSError, json.JSONDecodeError) as exc:
712
+ return _result_error("RECEIPT_INVALID", str(exc))
713
+ if not isinstance(receipt, dict):
714
+ return _result_error("RECEIPT_INVALID", "receipt top-level must be an object")
715
+
716
+ change_name = str(receipt.get("changeName") or "").strip()
717
+ if not change_name:
718
+ return _result_error("RECEIPT_INVALID", "receipt missing changeName")
719
+ if change_name != change_dir.name:
720
+ return _result_error(
721
+ "RECEIPT_CHANGE_NAME_INVALID",
722
+ f"receipt changeName {change_name!r} does not match {change_dir.name!r}",
723
+ )
724
+
725
+ expected_hash = str(receipt.get("artifactsHash") or "").strip()
726
+ if not expected_hash.startswith("sha256:"):
727
+ return _result_error("RECEIPT_INVALID", "receipt artifactsHash missing or malformed")
728
+
729
+ files_list = receipt.get("files")
730
+ if not isinstance(files_list, list) or not files_list:
731
+ return _result_error("RECEIPT_INVALID", "receipt files list missing or empty")
732
+ artifact_targets, path_error = _receipt_artifact_targets(change_dir, files_list)
733
+ if path_error is not None:
734
+ return path_error
735
+ assert artifact_targets is not None
736
+ artifact_name_set = {rel for rel, _ in artifact_targets}
737
+ missing_required = sorted(
738
+ _required_artifact_names(change_name) - artifact_name_set
739
+ )
740
+ if missing_required:
741
+ return _result_error(
742
+ "RECEIPT_FILES_INCOMPLETE",
743
+ "receipt omits required artifacts: " + ", ".join(missing_required),
744
+ )
745
+ if receipt.get("status") != "finalized":
746
+ return _result_error(
747
+ "RECEIPT_NOT_FINALIZED",
748
+ f"receipt status is {receipt.get('status')!r}, expected 'finalized'",
749
+ )
750
+ receipt_run_id = str(receipt.get("runId") or "").strip()
751
+ receipt_attempt = receipt.get("attempt")
752
+ if not receipt_run_id or not isinstance(receipt_attempt, int):
753
+ return _result_error(
754
+ "RECEIPT_INVALID",
755
+ "receipt runId/attempt identity is missing or malformed",
756
+ )
757
+
758
+ # Recompute artifacts hash from published files.
759
+ digest = hashlib.sha256()
760
+ artifact_names: list[str] = []
761
+ for rel, target in artifact_targets:
762
+ if not target.is_file():
763
+ return _result_error(
764
+ "ARTIFACT_MISSING", f"published artifact missing: {rel}"
765
+ )
766
+ artifact_names.append(rel)
767
+ digest.update(rel.encode("utf-8"))
768
+ digest.update(b"\0")
769
+ digest.update(target.read_bytes())
770
+ digest.update(b"\0")
771
+ actual_hash = "sha256:" + digest.hexdigest()
772
+ if actual_hash != expected_hash:
773
+ return _result_error(
774
+ "ARTIFACT_HASH_DRIFT",
775
+ f"published artifacts hash {actual_hash} != receipt {expected_hash}",
776
+ )
777
+
778
+ # Validate frontmatter on markdown artifacts.
779
+ frontmatter_results: dict[str, Any] = {}
780
+ for rel in artifact_names:
781
+ if not rel.endswith(".md"):
782
+ continue
783
+ target = change_dir / rel
784
+ try:
785
+ text = target.read_text(encoding="utf-8-sig")
786
+ except OSError as exc:
787
+ return _result_error("ARTIFACT_UNREADABLE", f"{rel}: {exc}")
788
+ fm = _frontmatter(text)
789
+ if fm is None:
790
+ return _result_error(
791
+ "ARTIFACT_FRONTMATTER_MISSING", f"{rel}: no frontmatter"
792
+ )
793
+ if fm.get("change-name") != change_name:
794
+ return _result_error(
795
+ "ARTIFACT_FRONTMATTER_INVALID",
796
+ f"{rel}: change-name '{fm.get('change-name')}' != '{change_name}'",
797
+ )
798
+ frontmatter_results[rel] = {
799
+ "change-name": fm.get("change-name"),
800
+ "status": fm.get("status"),
801
+ }
802
+
803
+ plan_path = change_dir / "plans" / f"{change_name}-plan.md"
804
+ try:
805
+ expected_tasks = parse_plan_tasks(plan_path)
806
+ except PlanParseError as exc:
807
+ return _result_error(exc.code, str(exc))
808
+ if not expected_tasks:
809
+ return _result_error(
810
+ "PLAN_TASKS_EMPTY",
811
+ f"no task rows parsed from {plan_path.relative_to(change_dir).as_posix()}",
812
+ )
813
+ checkpoints_path = change_dir / "meta" / "implementation-checkpoints.json"
814
+ if not checkpoints_path.is_file():
815
+ return _result_error(
816
+ "IMPLEMENTATION_CHECKPOINTS_MISSING",
817
+ f"derived task metadata missing: {checkpoints_path}",
818
+ )
819
+ try:
820
+ checkpoints = json.loads(
821
+ checkpoints_path.read_text(encoding="utf-8-sig")
822
+ )
823
+ except (OSError, json.JSONDecodeError) as exc:
824
+ return _result_error("IMPLEMENTATION_CHECKPOINTS_INVALID", str(exc))
825
+ expected_checkpoints = {
826
+ "schemaVersion": 1,
827
+ "changeName": change_name,
828
+ "tasks": expected_tasks,
829
+ "foundationGate": "approved",
830
+ }
831
+ if checkpoints != expected_checkpoints:
832
+ return _result_error(
833
+ "IMPLEMENTATION_CHECKPOINTS_DRIFT",
834
+ "derived implementation checkpoints do not match the finalized plan",
835
+ )
836
+
837
+ scenarios_path = (
838
+ change_dir / "plans" / f"{change_name}-test-scenarios.md"
839
+ )
840
+ try:
841
+ expected_scenarios = parse_test_scenarios(scenarios_path)
842
+ except PlanParseError as exc:
843
+ return _result_error(exc.code, str(exc))
844
+ if not expected_scenarios:
845
+ return _result_error(
846
+ "PLAN_SCENARIOS_EMPTY",
847
+ f"no scenario rows parsed from {scenarios_path.relative_to(change_dir).as_posix()}",
848
+ )
849
+ scenario_manifest_path = change_dir / "meta" / "scenario-manifest.json"
850
+ if not scenario_manifest_path.is_file():
851
+ return _result_error(
852
+ "SCENARIO_MANIFEST_MISSING",
853
+ f"derived scenario metadata missing: {scenario_manifest_path}",
854
+ )
855
+ try:
856
+ manifest = json.loads(
857
+ scenario_manifest_path.read_text(encoding="utf-8-sig")
858
+ )
859
+ except (OSError, json.JSONDecodeError) as exc:
860
+ return _result_error("SCENARIO_MANIFEST_INVALID", str(exc))
861
+ actual_scenarios = manifest.get("scenarios") if isinstance(manifest, dict) else None
862
+ if not isinstance(actual_scenarios, list) or not actual_scenarios:
863
+ return _result_error(
864
+ "SCENARIO_MANIFEST_EMPTY",
865
+ "scenario-manifest.json must contain at least one scenario",
866
+ )
867
+
868
+ expected_manifest = {
869
+ "schemaVersion": scenario_manifest_schema_version(expected_scenarios),
870
+ "changeName": change_name,
871
+ "scenarios": [
872
+ {
873
+ key: value
874
+ for key, value in scenario.items()
875
+ if key != "executableMappingDeclared"
876
+ }
877
+ for scenario in expected_scenarios
878
+ ],
879
+ }
880
+ if manifest != expected_manifest:
881
+ return _result_error(
882
+ "SCENARIO_MANIFEST_DRIFT",
883
+ "derived scenario manifest does not match the finalized scenario table",
884
+ )
885
+
886
+ # Validate gate-policy.json is parseable JSON.
887
+ gate_policy_path = change_dir / "meta" / "gate-policy.json"
888
+ if not gate_policy_path.is_file():
889
+ return _result_error(
890
+ "GATE_POLICY_MISSING",
891
+ f"gate policy missing: {gate_policy_path}",
892
+ )
893
+ try:
894
+ json.loads(gate_policy_path.read_text(encoding="utf-8-sig"))
895
+ except (OSError, json.JSONDecodeError) as exc:
896
+ return _result_error("GATE_POLICY_INVALID", str(exc))
897
+ gate_policy_consistent = True
898
+
899
+ # Validate events.ndjson: require one matching start/end lifecycle.
900
+ events_path = harness_events.events_path(change_dir)
901
+ phase_start_count = 0
902
+ phase_end_count = 0
903
+ phase_end_status: str | None = None
904
+ events_parse_errors: list[str] = []
905
+ if events_path.is_file():
906
+ for line_no, line in enumerate(events_path.read_text(encoding="utf-8").splitlines(), 1):
907
+ if not line.strip():
908
+ continue
909
+ try:
910
+ event = json.loads(line)
911
+ except json.JSONDecodeError as exc:
912
+ events_parse_errors.append(f"line {line_no}: {exc}")
913
+ continue
914
+ if not isinstance(event, dict):
915
+ events_parse_errors.append(f"line {line_no}: not an object")
916
+ continue
917
+ if event.get("phase") != "plan":
918
+ continue
919
+ if (
920
+ str(event.get("run_id") or "") != receipt_run_id
921
+ or event.get("attempt") != receipt_attempt
922
+ ):
923
+ continue
924
+ if event.get("type") == "phase.start":
925
+ phase_start_count += 1
926
+ elif event.get("type") == "phase.end":
927
+ phase_end_count += 1
928
+ phase_end_status = str(event.get("status") or "").upper()
929
+ else:
930
+ events_parse_errors.append("events.ndjson missing")
931
+
932
+ if events_parse_errors:
933
+ return _result_error(
934
+ "EVENTS_PARSE_ERROR", "; ".join(events_parse_errors[:5])
935
+ )
936
+
937
+ if phase_start_count == 0:
938
+ return _result_error(
939
+ "PHASE_START_MISSING",
940
+ "no matching plan phase.start event found",
941
+ )
942
+ if phase_start_count > 1:
943
+ return _result_error(
944
+ "PHASE_START_DUPLICATE",
945
+ f"found {phase_start_count} matching plan phase.start events",
946
+ )
947
+ if phase_end_count == 0:
948
+ return _result_error("PHASE_END_MISSING", "no plan phase.end event found")
949
+ if phase_end_count > 1:
950
+ return _result_error(
951
+ "PHASE_END_DUPLICATE", f"found {phase_end_count} phase.end events"
952
+ )
953
+ if phase_end_status != "OK":
954
+ return _result_error(
955
+ "PHASE_END_NOT_OK",
956
+ f"plan phase.end status is {phase_end_status!r}, expected 'OK'",
957
+ )
958
+
959
+ return {
960
+ "ok": True,
961
+ "action": "verify",
962
+ "changeDir": str(change_dir),
963
+ "changeName": change_name,
964
+ "artifactsHash": actual_hash,
965
+ "phaseStartCount": phase_start_count,
966
+ "phaseEndCount": phase_end_count,
967
+ "phaseEndStatus": phase_end_status,
968
+ "frontmatter": frontmatter_results,
969
+ "gatePolicyConsistent": gate_policy_consistent,
970
+ "receiptConsistent": True,
971
+ "taskCount": len(expected_tasks),
972
+ "scenarioCount": len(expected_scenarios),
973
+ "files": artifact_names,
974
+ }
975
+
976
+
977
+
978
+
979
+
980
+
981
+
982
+
983
+
984
+
985
+
986
+
987
+
988
+
989
+
990
+
991
+
992
+
993
+
994
+
995
+ def build_parser() -> argparse.ArgumentParser:
996
+ parser = argparse.ArgumentParser(prog="harness_plan_finalize.py")
997
+ sub = parser.add_subparsers(dest="command", required=True)
998
+ verify = sub.add_parser("verify")
999
+ verify.add_argument("--change-dir", required=True)
1000
+ verify.add_argument("--json", action="store_true")
1001
+ return parser
1002
+
1003
+
1004
+ def main(argv: list[str] | None = None) -> int:
1005
+ args = build_parser().parse_args(argv)
1006
+ result = verify_plan(Path(args.change_dir))
1007
+ stream = sys.stdout if result["ok"] else sys.stderr
1008
+ if args.json:
1009
+ stream.write(json.dumps(result, ensure_ascii=False, indent=2) + "\n")
1010
+ else:
1011
+ stream.write((result.get("action") or result.get("code") or "error") + "\n")
1012
+ return 0 if result["ok"] else 1
1013
+
1014
+
1015
+ if __name__ == "__main__":
1016
+ raise SystemExit(main())