@hunter-harness/workflow-harness 0.2.9 → 0.2.10

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 (282) 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-archive/reference.md +12 -2
  4. package/harness/bundles/general/claude-code/harness-archive/templates/summary-data-template.json +6 -2
  5. package/harness/bundles/general/claude-code/harness-codebase-map/SKILL.md +1 -1
  6. package/harness/bundles/general/claude-code/harness-knowledge-ingest/SKILL.md +3 -1
  7. package/harness/bundles/general/claude-code/harness-knowledge-ingest/reference.md +7 -0
  8. package/harness/bundles/general/claude-code/harness-knowledge-ingest/scripts/harness_knowledge.py +117 -0
  9. package/harness/bundles/general/claude-code/harness-knowledge-query/SKILL.md +1 -1
  10. package/harness/bundles/general/claude-code/harness-plan/SKILL.md +1 -1
  11. package/harness/bundles/general/claude-code/harness-review/SKILL.md +3 -3
  12. package/harness/bundles/general/claude-code/harness-run/SKILL.md +1 -1
  13. package/harness/bundles/general/claude-code/harness-run/reference.md +2 -0
  14. package/harness/bundles/general/claude-code/harness-submit/SKILL.md +15 -10
  15. package/harness/bundles/general/claude-code/harness-submit/checklist.md +40 -76
  16. package/harness/bundles/general/claude-code/harness-sync/SKILL.md +17 -2
  17. package/harness/bundles/general/claude-code/harness-sync/reference.md +2 -2
  18. package/harness/bundles/general/claude-code/harness-test/SKILL.md +1 -1
  19. package/harness/bundles/general/claude-code/harness-test/reference.md +2 -0
  20. package/harness/bundles/general/claude-code/protocols/archive-report-protocol.md +24 -4
  21. package/harness/bundles/general/claude-code/protocols/ledger-protocol.md +36 -0
  22. package/harness/bundles/general/claude-code/protocols/state-layout-protocol.md +15 -0
  23. package/harness/bundles/general/claude-code/scripts/harness_archive.py +803 -274
  24. package/harness/bundles/general/claude-code/scripts/harness_change.py +18 -0
  25. package/harness/bundles/general/claude-code/scripts/harness_deploy.py +1 -44
  26. package/harness/bundles/general/claude-code/scripts/harness_events.py +104 -6
  27. package/harness/bundles/general/claude-code/scripts/harness_gate.py +25 -4
  28. package/harness/bundles/general/claude-code/scripts/harness_integration.py +878 -0
  29. package/harness/bundles/general/claude-code/scripts/harness_ledger.py +440 -10
  30. package/harness/bundles/general/claude-code/scripts/harness_paths.py +315 -0
  31. package/harness/bundles/general/claude-code/scripts/harness_profile.py +45 -0
  32. package/harness/bundles/general/claude-code/scripts/harness_review.py +326 -0
  33. package/harness/bundles/general/claude-code/scripts/harness_sync.py +407 -0
  34. package/harness/bundles/general/claude-code/scripts/harness_test_guard.py +244 -49
  35. package/harness/bundles/general/codebuddy/.harness-build.json +1 -1
  36. package/harness/bundles/general/codebuddy/harness-archive/SKILL.md +1 -1
  37. package/harness/bundles/general/codebuddy/harness-archive/reference.md +12 -2
  38. package/harness/bundles/general/codebuddy/harness-archive/templates/summary-data-template.json +6 -2
  39. package/harness/bundles/general/codebuddy/harness-codebase-map/SKILL.md +1 -1
  40. package/harness/bundles/general/codebuddy/harness-knowledge-ingest/SKILL.md +3 -1
  41. package/harness/bundles/general/codebuddy/harness-knowledge-ingest/reference.md +7 -0
  42. package/harness/bundles/general/codebuddy/harness-knowledge-ingest/scripts/harness_knowledge.py +117 -0
  43. package/harness/bundles/general/codebuddy/harness-knowledge-query/SKILL.md +1 -1
  44. package/harness/bundles/general/codebuddy/harness-plan/SKILL.md +1 -1
  45. package/harness/bundles/general/codebuddy/harness-review/SKILL.md +3 -3
  46. package/harness/bundles/general/codebuddy/harness-run/SKILL.md +1 -1
  47. package/harness/bundles/general/codebuddy/harness-run/reference.md +2 -0
  48. package/harness/bundles/general/codebuddy/harness-submit/SKILL.md +15 -10
  49. package/harness/bundles/general/codebuddy/harness-submit/checklist.md +40 -76
  50. package/harness/bundles/general/codebuddy/harness-sync/SKILL.md +17 -2
  51. package/harness/bundles/general/codebuddy/harness-sync/reference.md +2 -2
  52. package/harness/bundles/general/codebuddy/harness-test/SKILL.md +1 -1
  53. package/harness/bundles/general/codebuddy/harness-test/reference.md +2 -0
  54. package/harness/bundles/general/codebuddy/protocols/archive-report-protocol.md +24 -4
  55. package/harness/bundles/general/codebuddy/protocols/ledger-protocol.md +36 -0
  56. package/harness/bundles/general/codebuddy/protocols/state-layout-protocol.md +15 -0
  57. package/harness/bundles/general/codebuddy/scripts/harness_archive.py +803 -274
  58. package/harness/bundles/general/codebuddy/scripts/harness_change.py +18 -0
  59. package/harness/bundles/general/codebuddy/scripts/harness_deploy.py +1 -44
  60. package/harness/bundles/general/codebuddy/scripts/harness_events.py +104 -6
  61. package/harness/bundles/general/codebuddy/scripts/harness_gate.py +25 -4
  62. package/harness/bundles/general/codebuddy/scripts/harness_integration.py +878 -0
  63. package/harness/bundles/general/codebuddy/scripts/harness_ledger.py +440 -10
  64. package/harness/bundles/general/codebuddy/scripts/harness_paths.py +315 -0
  65. package/harness/bundles/general/codebuddy/scripts/harness_profile.py +45 -0
  66. package/harness/bundles/general/codebuddy/scripts/harness_review.py +326 -0
  67. package/harness/bundles/general/codebuddy/scripts/harness_sync.py +407 -0
  68. package/harness/bundles/general/codebuddy/scripts/harness_test_guard.py +244 -49
  69. package/harness/bundles/general/codex/.harness-build.json +1 -1
  70. package/harness/bundles/general/codex/harness-archive/SKILL.md +1 -1
  71. package/harness/bundles/general/codex/harness-archive/reference.md +12 -2
  72. package/harness/bundles/general/codex/harness-archive/templates/summary-data-template.json +6 -2
  73. package/harness/bundles/general/codex/harness-codebase-map/SKILL.md +1 -1
  74. package/harness/bundles/general/codex/harness-knowledge-ingest/SKILL.md +3 -1
  75. package/harness/bundles/general/codex/harness-knowledge-ingest/reference.md +7 -0
  76. package/harness/bundles/general/codex/harness-knowledge-ingest/scripts/harness_knowledge.py +117 -0
  77. package/harness/bundles/general/codex/harness-knowledge-query/SKILL.md +1 -1
  78. package/harness/bundles/general/codex/harness-plan/SKILL.md +1 -1
  79. package/harness/bundles/general/codex/harness-review/SKILL.md +1 -1
  80. package/harness/bundles/general/codex/harness-run/SKILL.md +1 -1
  81. package/harness/bundles/general/codex/harness-run/reference.md +2 -0
  82. package/harness/bundles/general/codex/harness-submit/SKILL.md +15 -10
  83. package/harness/bundles/general/codex/harness-submit/checklist.md +40 -76
  84. package/harness/bundles/general/codex/harness-sync/SKILL.md +17 -2
  85. package/harness/bundles/general/codex/harness-sync/reference.md +2 -2
  86. package/harness/bundles/general/codex/harness-test/SKILL.md +1 -1
  87. package/harness/bundles/general/codex/harness-test/reference.md +2 -0
  88. package/harness/bundles/general/codex/protocols/archive-report-protocol.md +24 -4
  89. package/harness/bundles/general/codex/protocols/ledger-protocol.md +36 -0
  90. package/harness/bundles/general/codex/protocols/state-layout-protocol.md +15 -0
  91. package/harness/bundles/general/codex/scripts/harness_archive.py +803 -274
  92. package/harness/bundles/general/codex/scripts/harness_change.py +18 -0
  93. package/harness/bundles/general/codex/scripts/harness_deploy.py +1 -44
  94. package/harness/bundles/general/codex/scripts/harness_events.py +104 -6
  95. package/harness/bundles/general/codex/scripts/harness_gate.py +25 -4
  96. package/harness/bundles/general/codex/scripts/harness_integration.py +878 -0
  97. package/harness/bundles/general/codex/scripts/harness_ledger.py +440 -10
  98. package/harness/bundles/general/codex/scripts/harness_paths.py +315 -0
  99. package/harness/bundles/general/codex/scripts/harness_profile.py +45 -0
  100. package/harness/bundles/general/codex/scripts/harness_review.py +326 -0
  101. package/harness/bundles/general/codex/scripts/harness_sync.py +407 -0
  102. package/harness/bundles/general/codex/scripts/harness_test_guard.py +244 -49
  103. package/harness/bundles/general/cursor/.harness-build.json +1 -1
  104. package/harness/bundles/general/cursor/harness-archive/SKILL.md +1 -1
  105. package/harness/bundles/general/cursor/harness-archive/reference.md +12 -2
  106. package/harness/bundles/general/cursor/harness-archive/templates/summary-data-template.json +6 -2
  107. package/harness/bundles/general/cursor/harness-codebase-map/SKILL.md +1 -1
  108. package/harness/bundles/general/cursor/harness-knowledge-ingest/SKILL.md +3 -1
  109. package/harness/bundles/general/cursor/harness-knowledge-ingest/reference.md +7 -0
  110. package/harness/bundles/general/cursor/harness-knowledge-ingest/scripts/harness_knowledge.py +117 -0
  111. package/harness/bundles/general/cursor/harness-knowledge-query/SKILL.md +1 -1
  112. package/harness/bundles/general/cursor/harness-plan/SKILL.md +1 -1
  113. package/harness/bundles/general/cursor/harness-review/SKILL.md +1 -1
  114. package/harness/bundles/general/cursor/harness-run/SKILL.md +1 -1
  115. package/harness/bundles/general/cursor/harness-run/reference.md +2 -0
  116. package/harness/bundles/general/cursor/harness-submit/SKILL.md +15 -10
  117. package/harness/bundles/general/cursor/harness-submit/checklist.md +40 -76
  118. package/harness/bundles/general/cursor/harness-sync/SKILL.md +17 -2
  119. package/harness/bundles/general/cursor/harness-sync/reference.md +2 -2
  120. package/harness/bundles/general/cursor/harness-test/SKILL.md +1 -1
  121. package/harness/bundles/general/cursor/harness-test/reference.md +2 -0
  122. package/harness/bundles/general/cursor/protocols/archive-report-protocol.md +24 -4
  123. package/harness/bundles/general/cursor/protocols/ledger-protocol.md +36 -0
  124. package/harness/bundles/general/cursor/protocols/state-layout-protocol.md +15 -0
  125. package/harness/bundles/general/cursor/scripts/harness_archive.py +803 -274
  126. package/harness/bundles/general/cursor/scripts/harness_change.py +18 -0
  127. package/harness/bundles/general/cursor/scripts/harness_deploy.py +1 -44
  128. package/harness/bundles/general/cursor/scripts/harness_events.py +104 -6
  129. package/harness/bundles/general/cursor/scripts/harness_gate.py +25 -4
  130. package/harness/bundles/general/cursor/scripts/harness_integration.py +878 -0
  131. package/harness/bundles/general/cursor/scripts/harness_ledger.py +440 -10
  132. package/harness/bundles/general/cursor/scripts/harness_paths.py +315 -0
  133. package/harness/bundles/general/cursor/scripts/harness_profile.py +45 -0
  134. package/harness/bundles/general/cursor/scripts/harness_review.py +326 -0
  135. package/harness/bundles/general/cursor/scripts/harness_sync.py +407 -0
  136. package/harness/bundles/general/cursor/scripts/harness_test_guard.py +244 -49
  137. package/harness/bundles/java/claude-code/.harness-build.json +1 -1
  138. package/harness/bundles/java/claude-code/harness-apidoc/SKILL.md +1 -1
  139. package/harness/bundles/java/claude-code/harness-archive/SKILL.md +1 -1
  140. package/harness/bundles/java/claude-code/harness-archive/reference.md +12 -2
  141. package/harness/bundles/java/claude-code/harness-archive/templates/summary-data-template.json +6 -2
  142. package/harness/bundles/java/claude-code/harness-codebase-map/SKILL.md +1 -1
  143. package/harness/bundles/java/claude-code/harness-knowledge-ingest/SKILL.md +3 -1
  144. package/harness/bundles/java/claude-code/harness-knowledge-ingest/reference.md +7 -0
  145. package/harness/bundles/java/claude-code/harness-knowledge-ingest/scripts/harness_knowledge.py +117 -0
  146. package/harness/bundles/java/claude-code/harness-knowledge-query/SKILL.md +1 -1
  147. package/harness/bundles/java/claude-code/harness-package/SKILL.md +1 -1
  148. package/harness/bundles/java/claude-code/harness-plan/SKILL.md +1 -1
  149. package/harness/bundles/java/claude-code/harness-review/SKILL.md +3 -3
  150. package/harness/bundles/java/claude-code/harness-run/SKILL.md +1 -1
  151. package/harness/bundles/java/claude-code/harness-submit/SKILL.md +15 -10
  152. package/harness/bundles/java/claude-code/harness-submit/checklist.md +40 -76
  153. package/harness/bundles/java/claude-code/harness-sync/SKILL.md +17 -2
  154. package/harness/bundles/java/claude-code/harness-sync/reference.md +2 -2
  155. package/harness/bundles/java/claude-code/harness-test/SKILL.md +1 -1
  156. package/harness/bundles/java/claude-code/protocols/archive-report-protocol.md +24 -4
  157. package/harness/bundles/java/claude-code/protocols/ledger-protocol.md +36 -0
  158. package/harness/bundles/java/claude-code/protocols/state-layout-protocol.md +15 -0
  159. package/harness/bundles/java/claude-code/scripts/harness_archive.py +803 -274
  160. package/harness/bundles/java/claude-code/scripts/harness_change.py +18 -0
  161. package/harness/bundles/java/claude-code/scripts/harness_deploy.py +1 -44
  162. package/harness/bundles/java/claude-code/scripts/harness_events.py +104 -6
  163. package/harness/bundles/java/claude-code/scripts/harness_gate.py +25 -4
  164. package/harness/bundles/java/claude-code/scripts/harness_integration.py +878 -0
  165. package/harness/bundles/java/claude-code/scripts/harness_ledger.py +440 -10
  166. package/harness/bundles/java/claude-code/scripts/harness_paths.py +315 -0
  167. package/harness/bundles/java/claude-code/scripts/harness_profile.py +45 -0
  168. package/harness/bundles/java/claude-code/scripts/harness_review.py +326 -0
  169. package/harness/bundles/java/claude-code/scripts/harness_sync.py +407 -0
  170. package/harness/bundles/java/claude-code/scripts/harness_test_guard.py +244 -49
  171. package/harness/bundles/java/codebuddy/.harness-build.json +1 -1
  172. package/harness/bundles/java/codebuddy/harness-apidoc/SKILL.md +1 -1
  173. package/harness/bundles/java/codebuddy/harness-archive/SKILL.md +1 -1
  174. package/harness/bundles/java/codebuddy/harness-archive/reference.md +12 -2
  175. package/harness/bundles/java/codebuddy/harness-archive/templates/summary-data-template.json +6 -2
  176. package/harness/bundles/java/codebuddy/harness-codebase-map/SKILL.md +1 -1
  177. package/harness/bundles/java/codebuddy/harness-knowledge-ingest/SKILL.md +3 -1
  178. package/harness/bundles/java/codebuddy/harness-knowledge-ingest/reference.md +7 -0
  179. package/harness/bundles/java/codebuddy/harness-knowledge-ingest/scripts/harness_knowledge.py +117 -0
  180. package/harness/bundles/java/codebuddy/harness-knowledge-query/SKILL.md +1 -1
  181. package/harness/bundles/java/codebuddy/harness-package/SKILL.md +1 -1
  182. package/harness/bundles/java/codebuddy/harness-plan/SKILL.md +1 -1
  183. package/harness/bundles/java/codebuddy/harness-review/SKILL.md +3 -3
  184. package/harness/bundles/java/codebuddy/harness-run/SKILL.md +1 -1
  185. package/harness/bundles/java/codebuddy/harness-submit/SKILL.md +15 -10
  186. package/harness/bundles/java/codebuddy/harness-submit/checklist.md +40 -76
  187. package/harness/bundles/java/codebuddy/harness-sync/SKILL.md +17 -2
  188. package/harness/bundles/java/codebuddy/harness-sync/reference.md +2 -2
  189. package/harness/bundles/java/codebuddy/harness-test/SKILL.md +1 -1
  190. package/harness/bundles/java/codebuddy/protocols/archive-report-protocol.md +24 -4
  191. package/harness/bundles/java/codebuddy/protocols/ledger-protocol.md +36 -0
  192. package/harness/bundles/java/codebuddy/protocols/state-layout-protocol.md +15 -0
  193. package/harness/bundles/java/codebuddy/scripts/harness_archive.py +803 -274
  194. package/harness/bundles/java/codebuddy/scripts/harness_change.py +18 -0
  195. package/harness/bundles/java/codebuddy/scripts/harness_deploy.py +1 -44
  196. package/harness/bundles/java/codebuddy/scripts/harness_events.py +104 -6
  197. package/harness/bundles/java/codebuddy/scripts/harness_gate.py +25 -4
  198. package/harness/bundles/java/codebuddy/scripts/harness_integration.py +878 -0
  199. package/harness/bundles/java/codebuddy/scripts/harness_ledger.py +440 -10
  200. package/harness/bundles/java/codebuddy/scripts/harness_paths.py +315 -0
  201. package/harness/bundles/java/codebuddy/scripts/harness_profile.py +45 -0
  202. package/harness/bundles/java/codebuddy/scripts/harness_review.py +326 -0
  203. package/harness/bundles/java/codebuddy/scripts/harness_sync.py +407 -0
  204. package/harness/bundles/java/codebuddy/scripts/harness_test_guard.py +244 -49
  205. package/harness/bundles/java/codex/.harness-build.json +1 -1
  206. package/harness/bundles/java/codex/harness-apidoc/SKILL.md +1 -1
  207. package/harness/bundles/java/codex/harness-archive/SKILL.md +1 -1
  208. package/harness/bundles/java/codex/harness-archive/reference.md +12 -2
  209. package/harness/bundles/java/codex/harness-archive/templates/summary-data-template.json +6 -2
  210. package/harness/bundles/java/codex/harness-codebase-map/SKILL.md +1 -1
  211. package/harness/bundles/java/codex/harness-knowledge-ingest/SKILL.md +3 -1
  212. package/harness/bundles/java/codex/harness-knowledge-ingest/reference.md +7 -0
  213. package/harness/bundles/java/codex/harness-knowledge-ingest/scripts/harness_knowledge.py +117 -0
  214. package/harness/bundles/java/codex/harness-knowledge-query/SKILL.md +1 -1
  215. package/harness/bundles/java/codex/harness-package/SKILL.md +1 -1
  216. package/harness/bundles/java/codex/harness-plan/SKILL.md +1 -1
  217. package/harness/bundles/java/codex/harness-review/SKILL.md +1 -1
  218. package/harness/bundles/java/codex/harness-run/SKILL.md +1 -1
  219. package/harness/bundles/java/codex/harness-submit/SKILL.md +15 -10
  220. package/harness/bundles/java/codex/harness-submit/checklist.md +40 -76
  221. package/harness/bundles/java/codex/harness-sync/SKILL.md +17 -2
  222. package/harness/bundles/java/codex/harness-sync/reference.md +2 -2
  223. package/harness/bundles/java/codex/harness-test/SKILL.md +1 -1
  224. package/harness/bundles/java/codex/protocols/archive-report-protocol.md +24 -4
  225. package/harness/bundles/java/codex/protocols/ledger-protocol.md +36 -0
  226. package/harness/bundles/java/codex/protocols/state-layout-protocol.md +15 -0
  227. package/harness/bundles/java/codex/scripts/harness_archive.py +803 -274
  228. package/harness/bundles/java/codex/scripts/harness_change.py +18 -0
  229. package/harness/bundles/java/codex/scripts/harness_deploy.py +1 -44
  230. package/harness/bundles/java/codex/scripts/harness_events.py +104 -6
  231. package/harness/bundles/java/codex/scripts/harness_gate.py +25 -4
  232. package/harness/bundles/java/codex/scripts/harness_integration.py +878 -0
  233. package/harness/bundles/java/codex/scripts/harness_ledger.py +440 -10
  234. package/harness/bundles/java/codex/scripts/harness_paths.py +315 -0
  235. package/harness/bundles/java/codex/scripts/harness_profile.py +45 -0
  236. package/harness/bundles/java/codex/scripts/harness_review.py +326 -0
  237. package/harness/bundles/java/codex/scripts/harness_sync.py +407 -0
  238. package/harness/bundles/java/codex/scripts/harness_test_guard.py +244 -49
  239. package/harness/bundles/java/cursor/.harness-build.json +1 -1
  240. package/harness/bundles/java/cursor/harness-apidoc/SKILL.md +1 -1
  241. package/harness/bundles/java/cursor/harness-archive/SKILL.md +1 -1
  242. package/harness/bundles/java/cursor/harness-archive/reference.md +12 -2
  243. package/harness/bundles/java/cursor/harness-archive/templates/summary-data-template.json +6 -2
  244. package/harness/bundles/java/cursor/harness-codebase-map/SKILL.md +1 -1
  245. package/harness/bundles/java/cursor/harness-knowledge-ingest/SKILL.md +3 -1
  246. package/harness/bundles/java/cursor/harness-knowledge-ingest/reference.md +7 -0
  247. package/harness/bundles/java/cursor/harness-knowledge-ingest/scripts/harness_knowledge.py +117 -0
  248. package/harness/bundles/java/cursor/harness-knowledge-query/SKILL.md +1 -1
  249. package/harness/bundles/java/cursor/harness-package/SKILL.md +1 -1
  250. package/harness/bundles/java/cursor/harness-plan/SKILL.md +1 -1
  251. package/harness/bundles/java/cursor/harness-review/SKILL.md +1 -1
  252. package/harness/bundles/java/cursor/harness-run/SKILL.md +1 -1
  253. package/harness/bundles/java/cursor/harness-submit/SKILL.md +15 -10
  254. package/harness/bundles/java/cursor/harness-submit/checklist.md +40 -76
  255. package/harness/bundles/java/cursor/harness-sync/SKILL.md +17 -2
  256. package/harness/bundles/java/cursor/harness-sync/reference.md +2 -2
  257. package/harness/bundles/java/cursor/harness-test/SKILL.md +1 -1
  258. package/harness/bundles/java/cursor/protocols/archive-report-protocol.md +24 -4
  259. package/harness/bundles/java/cursor/protocols/ledger-protocol.md +36 -0
  260. package/harness/bundles/java/cursor/protocols/state-layout-protocol.md +15 -0
  261. package/harness/bundles/java/cursor/scripts/harness_archive.py +803 -274
  262. package/harness/bundles/java/cursor/scripts/harness_change.py +18 -0
  263. package/harness/bundles/java/cursor/scripts/harness_deploy.py +1 -44
  264. package/harness/bundles/java/cursor/scripts/harness_events.py +104 -6
  265. package/harness/bundles/java/cursor/scripts/harness_gate.py +25 -4
  266. package/harness/bundles/java/cursor/scripts/harness_integration.py +878 -0
  267. package/harness/bundles/java/cursor/scripts/harness_ledger.py +440 -10
  268. package/harness/bundles/java/cursor/scripts/harness_paths.py +315 -0
  269. package/harness/bundles/java/cursor/scripts/harness_profile.py +45 -0
  270. package/harness/bundles/java/cursor/scripts/harness_review.py +326 -0
  271. package/harness/bundles/java/cursor/scripts/harness_sync.py +407 -0
  272. package/harness/bundles/java/cursor/scripts/harness_test_guard.py +244 -49
  273. package/harness/manifests/general/claude-code.json +47 -31
  274. package/harness/manifests/general/codebuddy.json +47 -31
  275. package/harness/manifests/general/codex.json +47 -31
  276. package/harness/manifests/general/cursor.json +47 -31
  277. package/harness/manifests/java/claude-code.json +47 -31
  278. package/harness/manifests/java/codebuddy.json +47 -31
  279. package/harness/manifests/java/codex.json +47 -31
  280. package/harness/manifests/java/cursor.json +47 -31
  281. package/hunter-workflow-family.json +2 -2
  282. package/package.json +1 -1
@@ -0,0 +1,315 @@
1
+ #!/usr/bin/env python3
2
+ """Harness path and identity resolution.
3
+
4
+ Single owner for:
5
+ - repository identity (stable across worktrees of one repo);
6
+ - Change contract loading (``meta/change-context.json``);
7
+ - dual-root layout resolution (static contract root vs dynamic state root);
8
+ - path boundary assertions used by cleanup/integration code.
9
+
10
+ Python 3.10+, stdlib only.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import argparse
16
+ import hashlib
17
+ import json
18
+ import os
19
+ import re
20
+ import subprocess
21
+ import sys
22
+ from pathlib import Path
23
+ from typing import Any
24
+
25
+ SCRIPTS_DIR = Path(__file__).resolve().parent
26
+ if str(SCRIPTS_DIR) not in sys.path:
27
+ sys.path.insert(0, str(SCRIPTS_DIR))
28
+
29
+ if hasattr(sys.stdout, "reconfigure"):
30
+ sys.stdout.reconfigure(encoding="utf-8")
31
+ if hasattr(sys.stderr, "reconfigure"):
32
+ sys.stderr.reconfigure(encoding="utf-8")
33
+
34
+ LIFECYCLE_STATUSES = {"draft", "active", "superseded", "archived", "cancelled"}
35
+ CHANGE_CONTEXT_REL = Path("meta") / "change-context.json"
36
+
37
+
38
+ def _git_text(cwd: Path, *args: str) -> str | None:
39
+ proc = subprocess.run(
40
+ ["git", *args],
41
+ cwd=str(cwd),
42
+ capture_output=True,
43
+ text=True,
44
+ encoding="utf-8",
45
+ errors="replace",
46
+ check=False,
47
+ )
48
+ if proc.returncode != 0:
49
+ return None
50
+ return proc.stdout.strip()
51
+
52
+
53
+ def resolve_main_project_root(cwd: Path | None = None) -> Path:
54
+ """Locate the main project root from a worktree or the main checkout."""
55
+ start = (cwd or Path.cwd()).resolve()
56
+ common_raw = _git_text(start, "rev-parse", "--git-common-dir")
57
+ if not common_raw:
58
+ return start
59
+ common = Path(common_raw)
60
+ if not common.is_absolute():
61
+ common = (start / common).resolve()
62
+ if common.name == ".git":
63
+ return common.parent
64
+ return start
65
+
66
+
67
+ def resolve_worktree_root(cwd: Path | None = None) -> Path:
68
+ """Top-level of the checkout containing ``cwd`` (worktree-aware)."""
69
+ start = (cwd or Path.cwd()).resolve()
70
+ top = _git_text(start, "rev-parse", "--show-toplevel")
71
+ if not top:
72
+ return start
73
+ return Path(top).resolve()
74
+
75
+
76
+ def _git_common_dir(cwd: Path) -> Path | None:
77
+ raw = _git_text(cwd, "rev-parse", "--git-common-dir")
78
+ if not raw:
79
+ return None
80
+ common = Path(raw)
81
+ if not common.is_absolute():
82
+ common = (cwd / common).resolve()
83
+ return common
84
+
85
+
86
+ def _root_commit(cwd: Path) -> str | None:
87
+ raw = _git_text(cwd, "rev-list", "--max-parents=0", "HEAD")
88
+ if not raw:
89
+ return None
90
+ return raw.splitlines()[0].strip()
91
+
92
+
93
+ def _normalize_remote(url: str) -> str:
94
+ """Normalize a git remote URL to ``host/path`` lowercase form."""
95
+ text = url.strip()
96
+ text = re.sub(r"^git@", "", text)
97
+ text = text.replace(":", "/", 1) if "://" not in text else text
98
+ text = re.sub(r"^[a-zA-Z]+://", "", text)
99
+ text = re.sub(r"^[^/@]+@", "", text) # strip userinfo
100
+ text = text.lower()
101
+ if text.endswith(".git"):
102
+ text = text[: -len(".git")]
103
+ return text.rstrip("/")
104
+
105
+
106
+ def _primary_remote_url(cwd: Path) -> str | None:
107
+ url = _git_text(cwd, "config", "--get", "remote.origin.url")
108
+ if url:
109
+ return url
110
+ remotes = _git_text(cwd, "remote")
111
+ if not remotes:
112
+ return None
113
+ first = remotes.splitlines()[0].strip()
114
+ return _git_text(cwd, "config", "--get", f"remote.{first}.url")
115
+
116
+
117
+ def repository_identity(cwd: Path) -> str:
118
+ """Stable repository identity.
119
+
120
+ Preferred: normalized primary remote identity + root commit.
121
+ Fallback (no remote): resolved git common-dir identity + root commit.
122
+ The identity never embeds a worktree absolute path, so linked worktrees of
123
+ one repository share the same id (RET-09).
124
+ """
125
+ cwd = Path(cwd).resolve()
126
+ root_commit = _root_commit(cwd) or "no-root-commit"
127
+ remote = _primary_remote_url(cwd)
128
+ if remote:
129
+ basis = f"remote:{_normalize_remote(remote)}\n{root_commit}"
130
+ else:
131
+ common = _git_common_dir(cwd)
132
+ basis = f"local:{common}\n{root_commit}"
133
+ return "sha256:" + hashlib.sha256(basis.encode("utf-8")).hexdigest()
134
+
135
+
136
+ def load_change_contract(contract_dir: Path) -> dict[str, Any]:
137
+ """Load and validate ``meta/change-context.json`` from a contract dir."""
138
+ contract_dir = Path(contract_dir)
139
+ context_path = contract_dir / CHANGE_CONTEXT_REL
140
+ if not context_path.is_file():
141
+ raise FileNotFoundError(f"change context not found: {context_path}")
142
+ data = json.loads(context_path.read_text(encoding="utf-8-sig"))
143
+ if not isinstance(data, dict):
144
+ raise ValueError(f"change context must be an object: {context_path}")
145
+ lifecycle = data.get("lifecycle")
146
+ if isinstance(lifecycle, dict):
147
+ status = lifecycle.get("status")
148
+ if status is not None and status not in LIFECYCLE_STATUSES:
149
+ raise ValueError(
150
+ f"invalid lifecycle.status {status!r}; expected one of "
151
+ f"{sorted(LIFECYCLE_STATUSES)}"
152
+ )
153
+ return data
154
+
155
+
156
+ def contract_layout_kind(contract: dict[str, Any]) -> str:
157
+ """``split-v1`` when the contract declares a separate runtime root."""
158
+ ownership = contract.get("stateOwnership")
159
+ if isinstance(ownership, dict) and ownership.get("runtimeRoot"):
160
+ return "split-v1"
161
+ return "legacy-colocated"
162
+
163
+
164
+ def _resolve_runtime_root(main_root: Path, change_name: str, runtime_root: Any) -> Path:
165
+ raw = Path(str(runtime_root or ""))
166
+ if not str(runtime_root or "").strip() or raw.is_absolute():
167
+ raise ValueError("stateOwnership.runtimeRoot must be a project-relative path")
168
+ state_parent = (main_root / ".harness" / "state" / "changes").resolve()
169
+ resolved = (main_root / raw).resolve()
170
+ assert_path_within(resolved, state_parent)
171
+ expected = (state_parent / change_name).resolve()
172
+ if resolved != expected:
173
+ raise ValueError(
174
+ "stateOwnership.runtimeRoot must equal "
175
+ f".harness/state/changes/{change_name}"
176
+ )
177
+ return resolved
178
+
179
+
180
+ def resolve_change_layout(
181
+ cwd_hint: Path, change_id_or_dir: str | Path
182
+ ) -> dict[str, Any]:
183
+ """Resolve the static/dynamic roots for a change.
184
+
185
+ ``cwd_hint`` is any directory inside the checkout (main or linked
186
+ worktree); state always anchors at the main project root. Resolution is
187
+ read-only: legacy files are never moved here.
188
+ """
189
+ hint = Path(cwd_hint).resolve()
190
+ main_root = resolve_main_project_root(hint)
191
+ worktree_root = resolve_worktree_root(hint)
192
+
193
+ if isinstance(change_id_or_dir, Path):
194
+ contract_dir = change_id_or_dir.resolve()
195
+ change_name = contract_dir.name
196
+ if not contract_dir.is_dir():
197
+ raise FileNotFoundError(f"change contract dir not found: {contract_dir}")
198
+ else:
199
+ change_name = str(change_id_or_dir)
200
+ contract_dir = (main_root / ".harness" / "changes" / change_name).resolve()
201
+ if not contract_dir.is_dir():
202
+ raise FileNotFoundError(f"change not found: {change_name}")
203
+
204
+ contract: dict[str, Any] = {}
205
+ if (contract_dir / CHANGE_CONTEXT_REL).is_file():
206
+ contract = load_change_contract(contract_dir)
207
+
208
+ layout_kind = contract_layout_kind(contract)
209
+ if layout_kind == "split-v1":
210
+ runtime_rel = contract["stateOwnership"]["runtimeRoot"]
211
+ state_root = _resolve_runtime_root(main_root, change_name, runtime_rel)
212
+ else:
213
+ state_root = contract_dir
214
+
215
+ return {
216
+ "schemaVersion": 1,
217
+ "changeName": change_name,
218
+ "repositoryId": repository_identity(main_root),
219
+ "projectRoot": str(main_root),
220
+ "worktreeRoot": str(worktree_root),
221
+ "contractRoot": str(contract_dir),
222
+ "stateRoot": str(state_root),
223
+ "layout": layout_kind,
224
+ }
225
+
226
+
227
+ def resolve_state_dir_for_contract(
228
+ contract_dir: Path, cwd: Path | None = None
229
+ ) -> Path:
230
+ """Return the dynamic-state root for a change contract dir.
231
+
232
+ split-v1 contracts resolve to ``.harness/state/changes/<id>`` under the
233
+ main project root. Legacy contracts, missing/invalid context files and
234
+ non-standard locations return the contract dir unchanged, so colocated
235
+ readers observe zero behaviour change.
236
+ """
237
+ contract_dir = Path(contract_dir).resolve()
238
+ try:
239
+ contract = load_change_contract(contract_dir)
240
+ except (FileNotFoundError, ValueError, json.JSONDecodeError, OSError):
241
+ return contract_dir
242
+ if contract_layout_kind(contract) != "split-v1":
243
+ return contract_dir
244
+ main_root = resolve_main_project_root(cwd or contract_dir)
245
+ expected = (main_root / ".harness" / "changes" / contract_dir.name).resolve()
246
+ if contract_dir != expected:
247
+ return contract_dir
248
+ return _resolve_runtime_root(
249
+ main_root, contract_dir.name, contract["stateOwnership"]["runtimeRoot"]
250
+ )
251
+
252
+
253
+ def assert_path_within(
254
+ path: Path, allowed_root: Path, *, allow_root: bool = False
255
+ ) -> Path:
256
+ """Resolve ``path`` and require it to stay inside ``allowed_root``.
257
+
258
+ Rejects empty paths, parents, sibling-prefix attacks and symlink escapes.
259
+ Returns the resolved path on success.
260
+ """
261
+ raw = str(path)
262
+ if not raw or not raw.strip():
263
+ raise ValueError("empty path is not allowed")
264
+ root = Path(allowed_root).resolve()
265
+ resolved = Path(path).resolve()
266
+ if resolved == root:
267
+ if allow_root:
268
+ return resolved
269
+ raise ValueError(f"path is the allowed root itself: {resolved}")
270
+ if not resolved.is_relative_to(root):
271
+ raise ValueError(f"path escapes allowed root: {resolved} not within {root}")
272
+ return resolved
273
+
274
+
275
+ def cmd_resolve_layout(args: argparse.Namespace) -> int:
276
+ layout = resolve_change_layout(Path.cwd(), args.change)
277
+ sys.stdout.write(json.dumps(layout, ensure_ascii=False, indent=2) + "\n")
278
+ return 0
279
+
280
+
281
+ def cmd_repository_id(args: argparse.Namespace) -> int:
282
+ identity = repository_identity(Path.cwd())
283
+ if args.json:
284
+ sys.stdout.write(
285
+ json.dumps({"ok": True, "repositoryId": identity}, ensure_ascii=False)
286
+ + "\n"
287
+ )
288
+ else:
289
+ sys.stdout.write(identity + "\n")
290
+ return 0
291
+
292
+
293
+ def build_parser() -> argparse.ArgumentParser:
294
+ parser = argparse.ArgumentParser(prog="harness_paths.py")
295
+ sub = parser.add_subparsers(dest="command_name", required=True)
296
+
297
+ p_layout = sub.add_parser("resolve-layout")
298
+ p_layout.add_argument("--change", required=True)
299
+ p_layout.set_defaults(func=cmd_resolve_layout)
300
+
301
+ p_id = sub.add_parser("repository-id")
302
+ p_id.add_argument("--json", action="store_true")
303
+ p_id.set_defaults(func=cmd_repository_id)
304
+
305
+ return parser
306
+
307
+
308
+ def main(argv: list[str] | None = None) -> int:
309
+ parser = build_parser()
310
+ args = parser.parse_args(argv)
311
+ return int(args.func(args))
312
+
313
+
314
+ if __name__ == "__main__":
315
+ raise SystemExit(main())
@@ -727,6 +727,23 @@ def build_parser() -> argparse.ArgumentParser:
727
727
  p_validate.add_argument("--project", required=True, type=Path)
728
728
  p_validate.add_argument("--json", action="store_true")
729
729
 
730
+ p_resolve = sub.add_parser(
731
+ "resolve", help="resolve command template by key (runtime placeholder substitution)"
732
+ )
733
+ p_resolve.add_argument("--project", required=True, type=Path)
734
+ p_resolve.add_argument("--key", required=True, help="command key in profile.commands")
735
+ p_resolve.add_argument(
736
+ "--test-classes",
737
+ default=None,
738
+ help="comma-separated test classes substituting {testClasses}",
739
+ )
740
+ p_resolve.add_argument(
741
+ "--modules",
742
+ default=None,
743
+ help="comma-separated modules substituting {modules}",
744
+ )
745
+ p_resolve.add_argument("--json", action="store_true")
746
+
730
747
  p_migrate = sub.add_parser("migrate", help="migrate v1 → v2")
731
748
  p_migrate.add_argument("--project", required=True, type=Path)
732
749
  p_migrate.add_argument("--apply", action="store_true", help="apply (default: dry-run)")
@@ -757,6 +774,34 @@ def main(argv: list[str] | None = None) -> int:
757
774
  payload = {"ok": not issues, "issues": issues}
758
775
  sys.stdout.write(json.dumps(payload, ensure_ascii=False, indent=2) + "\n")
759
776
  return 0 if payload["ok"] else 1
777
+ if args.command == "resolve":
778
+ project = args.project.resolve()
779
+ profile = load_profile(project)
780
+ if profile is None:
781
+ sys.stdout.write(
782
+ json.dumps({"ok": False, "error": "profile missing"}, ensure_ascii=False) + "\n"
783
+ )
784
+ return 1
785
+ test_classes = (
786
+ [item for item in args.test_classes.split(",") if item]
787
+ if args.test_classes
788
+ else None
789
+ )
790
+ modules = (
791
+ [item for item in args.modules.split(",") if item] if args.modules else None
792
+ )
793
+ try:
794
+ resolved = resolve_command(
795
+ profile, args.key, test_classes=test_classes, modules=modules
796
+ )
797
+ except KeyError as exc:
798
+ sys.stdout.write(
799
+ json.dumps({"ok": False, "error": str(exc)}, ensure_ascii=False) + "\n"
800
+ )
801
+ return 1
802
+ payload = {"ok": True, "key": args.key, **resolved}
803
+ sys.stdout.write(json.dumps(payload, ensure_ascii=False, indent=2) + "\n")
804
+ return 0
760
805
  if args.command == "migrate":
761
806
  result = migrate(args.project, dry_run=not args.apply)
762
807
  sys.stdout.write(json.dumps(result, ensure_ascii=False, indent=2) + "\n")
@@ -0,0 +1,326 @@
1
+ #!/usr/bin/env python3
2
+ """Harness review findings / fixback dispositions sidecar.
3
+
4
+ Structured source of truth for review output; Markdown reports are a human
5
+ projection of these sidecars, never the counting source.
6
+
7
+ Files (under the change state root):
8
+ reports/review/review-findings.json
9
+ reports/review/fixback-dispositions.json
10
+
11
+ Python 3.10+, stdlib only.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import argparse
17
+ import hashlib
18
+ import json
19
+ import os
20
+ import re
21
+ import sys
22
+ import uuid
23
+ from pathlib import Path
24
+ from typing import Any
25
+
26
+ SCRIPTS_DIR = Path(__file__).resolve().parent
27
+ if str(SCRIPTS_DIR) not in sys.path:
28
+ sys.path.insert(0, str(SCRIPTS_DIR))
29
+
30
+ import harness_paths # noqa: E402
31
+
32
+ if hasattr(sys.stdout, "reconfigure"):
33
+ sys.stdout.reconfigure(encoding="utf-8")
34
+ if hasattr(sys.stderr, "reconfigure"):
35
+ sys.stderr.reconfigure(encoding="utf-8")
36
+
37
+ SEVERITIES = {"RED", "YELLOW", "OK"}
38
+ DISPOSITIONS = {"OPEN", "FIXED", "ACCEPTED_RISK", "DEFERRED", "UNKNOWN"}
39
+ FINDINGS_REL = Path("reports") / "review" / "review-findings.json"
40
+ DISPOSITIONS_REL = Path("reports") / "review" / "fixback-dispositions.json"
41
+ _REQUIRED_FINDING_FIELDS = ("dimension", "severity", "path", "line", "title")
42
+
43
+
44
+ def _write_json_atomic(path: Path, data: Any) -> None:
45
+ path.parent.mkdir(parents=True, exist_ok=True)
46
+ text = json.dumps(data, ensure_ascii=False, indent=2) + "\n"
47
+ tmp = path.with_name(f".{path.name}.{os.getpid()}.{uuid.uuid4().hex}.tmp")
48
+ try:
49
+ tmp.write_text(text, encoding="utf-8", newline="\n")
50
+ os.replace(tmp, path)
51
+ except BaseException:
52
+ tmp.unlink(missing_ok=True)
53
+ raise
54
+
55
+
56
+ def _read_json(path: Path) -> Any:
57
+ return json.loads(path.read_text(encoding="utf-8-sig"))
58
+
59
+
60
+ def _state_dir(change_dir: Path) -> Path:
61
+ return Path(harness_paths.resolve_state_dir_for_contract(change_dir))
62
+
63
+
64
+ def findings_path(change_dir: Path) -> Path:
65
+ return _state_dir(change_dir) / FINDINGS_REL
66
+
67
+
68
+ def dispositions_path(change_dir: Path) -> Path:
69
+ return _state_dir(change_dir) / DISPOSITIONS_REL
70
+
71
+
72
+ def _normalize_title(title: str) -> str:
73
+ return re.sub(r"\s+", " ", title.strip().lower())
74
+
75
+
76
+ def stable_finding_id(
77
+ run_id: str, dimension: str, path: str, line: int, title: str
78
+ ) -> str:
79
+ """Stable finding identity (run + dimension + canonical path + line + title)."""
80
+ canonical_path = str(path).replace("\\", "/").strip("/").lower()
81
+ basis = (
82
+ f"{run_id}|{dimension.strip().lower()}|{canonical_path}|{int(line)}|"
83
+ f"{_normalize_title(title)}"
84
+ )
85
+ return "f-" + hashlib.sha256(basis.encode("utf-8")).hexdigest()[:16]
86
+
87
+
88
+ def validate_findings(doc: Any) -> list[str]:
89
+ problems: list[str] = []
90
+ if not isinstance(doc, dict):
91
+ return ["findings document must be an object"]
92
+ if not isinstance(doc.get("runId"), str) or not doc["runId"].strip():
93
+ problems.append("runId is required")
94
+ findings = doc.get("findings")
95
+ if not isinstance(findings, list):
96
+ problems.append("findings must be a list")
97
+ return problems
98
+ for index, finding in enumerate(findings):
99
+ if not isinstance(finding, dict):
100
+ problems.append(f"findings[{index}] must be an object")
101
+ continue
102
+ for field in _REQUIRED_FINDING_FIELDS:
103
+ if field not in finding:
104
+ problems.append(f"findings[{index}].{field} is required")
105
+ severity = finding.get("severity")
106
+ if severity is not None and severity not in SEVERITIES:
107
+ problems.append(
108
+ f"findings[{index}].severity must be one of {sorted(SEVERITIES)}"
109
+ )
110
+ line = finding.get("line")
111
+ if line is not None and (not isinstance(line, int) or line < 0):
112
+ problems.append(f"findings[{index}].line must be a non-negative int")
113
+ return problems
114
+
115
+
116
+ def write_findings(change_dir: Path, doc: dict[str, Any]) -> dict[str, Any]:
117
+ problems = validate_findings(doc)
118
+ if problems:
119
+ return {"ok": False, "code": "FINDINGS_INVALID", "problems": problems}
120
+ run_id = doc["runId"]
121
+ assigned: list[dict[str, Any]] = []
122
+ seen: set[str] = set()
123
+ for finding in doc["findings"]:
124
+ fid = stable_finding_id(
125
+ run_id,
126
+ finding["dimension"],
127
+ finding["path"],
128
+ finding["line"],
129
+ finding["title"],
130
+ )
131
+ suffix = 2
132
+ unique = fid
133
+ while unique in seen:
134
+ unique = f"{fid}-{suffix}"
135
+ suffix += 1
136
+ seen.add(unique)
137
+ entry = dict(finding)
138
+ entry["id"] = unique
139
+ assigned.append(entry)
140
+ payload = {
141
+ "schemaVersion": 1,
142
+ "runId": run_id,
143
+ "changeName": doc.get("changeName") or Path(change_dir).name,
144
+ "findings": assigned,
145
+ }
146
+ out = findings_path(change_dir)
147
+ _write_json_atomic(out, payload)
148
+ return {"ok": True, "code": "FINDINGS_WRITTEN", "path": str(out),
149
+ "count": len(assigned)}
150
+
151
+
152
+ def _load_findings(change_dir: Path) -> dict[str, Any] | None:
153
+ path = findings_path(change_dir)
154
+ if not path.is_file():
155
+ return None
156
+ try:
157
+ data = _read_json(path)
158
+ except (OSError, json.JSONDecodeError):
159
+ return None
160
+ return data if isinstance(data, dict) else None
161
+
162
+
163
+ def validate_dispositions(
164
+ doc: Any, known_ids: set[str]
165
+ ) -> list[str]:
166
+ problems: list[str] = []
167
+ if not isinstance(doc, dict):
168
+ return ["dispositions document must be an object"]
169
+ dispositions = doc.get("dispositions")
170
+ if not isinstance(dispositions, list):
171
+ return ["dispositions must be a list"]
172
+ for index, item in enumerate(dispositions):
173
+ if not isinstance(item, dict):
174
+ problems.append(f"dispositions[{index}] must be an object")
175
+ continue
176
+ fid = item.get("findingId")
177
+ if not isinstance(fid, str) or not fid.strip():
178
+ problems.append(f"dispositions[{index}].findingId is required")
179
+ elif fid not in known_ids:
180
+ problems.append(f"dispositions[{index}].findingId unknown: {fid}")
181
+ value = item.get("disposition")
182
+ if value not in DISPOSITIONS:
183
+ problems.append(
184
+ f"dispositions[{index}].disposition must be one of "
185
+ f"{sorted(DISPOSITIONS)}"
186
+ )
187
+ return problems
188
+
189
+
190
+ def write_dispositions(change_dir: Path, doc: dict[str, Any]) -> dict[str, Any]:
191
+ findings_doc = _load_findings(change_dir)
192
+ known_ids = {
193
+ f.get("id") for f in (findings_doc or {}).get("findings", [])
194
+ if isinstance(f, dict)
195
+ }
196
+ problems = validate_dispositions(doc, known_ids)
197
+ if problems:
198
+ return {"ok": False, "code": "DISPOSITIONS_INVALID", "problems": problems}
199
+ payload = {
200
+ "schemaVersion": 1,
201
+ "runId": doc.get("runId"),
202
+ "dispositions": doc["dispositions"],
203
+ }
204
+ out = dispositions_path(change_dir)
205
+ _write_json_atomic(out, payload)
206
+ return {"ok": True, "code": "DISPOSITIONS_WRITTEN", "path": str(out)}
207
+
208
+
209
+ def status(change_dir: Path) -> dict[str, Any]:
210
+ findings_doc = _load_findings(change_dir)
211
+ if findings_doc is None:
212
+ return {
213
+ "ok": True,
214
+ "code": "NO_FINDINGS",
215
+ "counts": {"RED": 0, "YELLOW": 0, "OK": 0},
216
+ "dispositions": {},
217
+ "items": [],
218
+ }
219
+ dispositions_doc: dict[str, Any] = {}
220
+ dpath = dispositions_path(change_dir)
221
+ if dpath.is_file():
222
+ try:
223
+ loaded = _read_json(dpath)
224
+ if isinstance(loaded, dict):
225
+ dispositions_doc = loaded
226
+ except (OSError, json.JSONDecodeError):
227
+ dispositions_doc = {}
228
+ by_id = {
229
+ item.get("findingId"): item
230
+ for item in dispositions_doc.get("dispositions", [])
231
+ if isinstance(item, dict)
232
+ }
233
+ counts = {"RED": 0, "YELLOW": 0, "OK": 0}
234
+ disposition_counts: dict[str, int] = {}
235
+ items: list[dict[str, Any]] = []
236
+ for finding in findings_doc.get("findings", []):
237
+ severity = finding.get("severity")
238
+ if severity in counts:
239
+ counts[severity] += 1
240
+ entry = by_id.get(finding.get("id"))
241
+ disposition = entry.get("disposition") if entry else None
242
+ if disposition not in DISPOSITIONS:
243
+ disposition = "UNKNOWN"
244
+ disposition_counts[disposition] = disposition_counts.get(disposition, 0) + 1
245
+ items.append(
246
+ {
247
+ "id": finding.get("id"),
248
+ "severity": severity,
249
+ "path": finding.get("path"),
250
+ "line": finding.get("line"),
251
+ "title": finding.get("title"),
252
+ "disposition": disposition,
253
+ }
254
+ )
255
+ return {
256
+ "ok": True,
257
+ "code": "STATUS",
258
+ "runId": findings_doc.get("runId"),
259
+ "counts": counts,
260
+ "dispositions": disposition_counts,
261
+ "items": items,
262
+ }
263
+
264
+
265
+ # ------------------------------------------------------------------- CLI
266
+
267
+
268
+ def _emit(payload: Any, *, as_json: bool) -> int:
269
+ sys.stdout.write(json.dumps(payload, ensure_ascii=False, indent=2) + "\n")
270
+ return 0 if payload.get("ok") else 1
271
+
272
+
273
+ def cmd_validate_findings(args: argparse.Namespace) -> int:
274
+ doc = _read_json(Path(args.input))
275
+ problems = validate_findings(doc)
276
+ payload = {"ok": not problems, "problems": problems}
277
+ return _emit(payload, as_json=True)
278
+
279
+
280
+ def cmd_write_findings(args: argparse.Namespace) -> int:
281
+ doc = _read_json(Path(args.input))
282
+ return _emit(write_findings(Path(args.change_dir), doc), as_json=True)
283
+
284
+
285
+ def cmd_write_dispositions(args: argparse.Namespace) -> int:
286
+ doc = _read_json(Path(args.input))
287
+ return _emit(write_dispositions(Path(args.change_dir), doc), as_json=True)
288
+
289
+
290
+ def cmd_status(args: argparse.Namespace) -> int:
291
+ return _emit(status(Path(args.change_dir)), as_json=True)
292
+
293
+
294
+ def build_parser() -> argparse.ArgumentParser:
295
+ parser = argparse.ArgumentParser(prog="harness_review.py")
296
+ sub = parser.add_subparsers(dest="command_name", required=True)
297
+
298
+ p_validate = sub.add_parser("validate-findings")
299
+ p_validate.add_argument("--input", required=True)
300
+ p_validate.set_defaults(func=cmd_validate_findings)
301
+
302
+ p_findings = sub.add_parser("write-findings")
303
+ p_findings.add_argument("--change-dir", required=True)
304
+ p_findings.add_argument("--input", required=True)
305
+ p_findings.set_defaults(func=cmd_write_findings)
306
+
307
+ p_dispositions = sub.add_parser("write-dispositions")
308
+ p_dispositions.add_argument("--change-dir", required=True)
309
+ p_dispositions.add_argument("--input", required=True)
310
+ p_dispositions.set_defaults(func=cmd_write_dispositions)
311
+
312
+ p_status = sub.add_parser("status")
313
+ p_status.add_argument("--change-dir", required=True)
314
+ p_status.set_defaults(func=cmd_status)
315
+
316
+ return parser
317
+
318
+
319
+ def main(argv: list[str] | None = None) -> int:
320
+ parser = build_parser()
321
+ args = parser.parse_args(argv)
322
+ return int(args.func(args))
323
+
324
+
325
+ if __name__ == "__main__":
326
+ raise SystemExit(main())