@hunter-harness/workflow-harness 0.2.0 → 0.2.2

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 (278) 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 +2 -8
  4. package/harness/bundles/general/claude-code/harness-codebase-map/SKILL.md +2 -1
  5. package/harness/bundles/general/claude-code/harness-knowledge-ingest/SKILL.md +35 -21
  6. package/harness/bundles/general/claude-code/harness-knowledge-ingest/reference.md +53 -7
  7. package/harness/bundles/general/claude-code/harness-knowledge-ingest/scripts/harness_knowledge.py +521 -125
  8. package/harness/bundles/general/claude-code/harness-knowledge-ingest/scripts/harness_knowledge_mcp.py +2 -2
  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 +2 -1
  11. package/harness/bundles/general/claude-code/harness-review/SKILL.md +5 -1
  12. package/harness/bundles/general/claude-code/harness-run/SKILL.md +1 -1
  13. package/harness/bundles/general/claude-code/harness-run/checklist.md +1 -1
  14. package/harness/bundles/general/claude-code/harness-submit/SKILL.md +2 -2
  15. package/harness/bundles/general/claude-code/harness-submit/checklist.md +30 -6
  16. package/harness/bundles/general/claude-code/harness-sync/SKILL.md +2 -2
  17. package/harness/bundles/general/claude-code/harness-test/SKILL.md +2 -2
  18. package/harness/bundles/general/claude-code/harness-test/checklist.md +1 -1
  19. package/harness/bundles/general/claude-code/harness-test/pitfalls.md +16 -0
  20. package/harness/bundles/general/claude-code/harness-test/scripts/runtime-helpers.mjs +216 -0
  21. package/harness/bundles/general/claude-code/protocols/archive-report-protocol.md +1 -1
  22. package/harness/bundles/general/claude-code/protocols/ledger-protocol.md +55 -2
  23. package/harness/bundles/general/claude-code/protocols/report-pipeline-protocol.md +1 -8
  24. package/harness/bundles/general/claude-code/protocols/sensitive-info-protocol.md +13 -0
  25. package/harness/bundles/general/claude-code/protocols/state-layout-protocol.md +24 -0
  26. package/harness/bundles/general/claude-code/scripts/harness_archive.py +31 -14
  27. package/harness/bundles/general/claude-code/scripts/harness_check_gate.py +103 -0
  28. package/harness/bundles/general/claude-code/scripts/harness_deploy.py +179 -4
  29. package/harness/bundles/general/claude-code/scripts/harness_ledger.py +269 -3
  30. package/harness/bundles/general/claude-code/scripts/harness_preflight.py +72 -382
  31. package/harness/bundles/general/claude-code/scripts/harness_profile.py +742 -0
  32. package/harness/bundles/general/claude-code/scripts/harness_service.py +99 -2
  33. package/harness/bundles/general/claude-code/scripts/harness_state.py +179 -0
  34. package/harness/bundles/general/codebuddy/.harness-build.json +1 -1
  35. package/harness/bundles/general/codebuddy/harness-archive/SKILL.md +1 -1
  36. package/harness/bundles/general/codebuddy/harness-archive/reference.md +2 -8
  37. package/harness/bundles/general/codebuddy/harness-codebase-map/SKILL.md +2 -1
  38. package/harness/bundles/general/codebuddy/harness-knowledge-ingest/SKILL.md +35 -21
  39. package/harness/bundles/general/codebuddy/harness-knowledge-ingest/reference.md +53 -7
  40. package/harness/bundles/general/codebuddy/harness-knowledge-ingest/scripts/harness_knowledge.py +521 -125
  41. package/harness/bundles/general/codebuddy/harness-knowledge-ingest/scripts/harness_knowledge_mcp.py +2 -2
  42. package/harness/bundles/general/codebuddy/harness-knowledge-query/SKILL.md +1 -1
  43. package/harness/bundles/general/codebuddy/harness-plan/SKILL.md +2 -1
  44. package/harness/bundles/general/codebuddy/harness-review/SKILL.md +5 -1
  45. package/harness/bundles/general/codebuddy/harness-run/SKILL.md +1 -1
  46. package/harness/bundles/general/codebuddy/harness-run/checklist.md +1 -1
  47. package/harness/bundles/general/codebuddy/harness-submit/SKILL.md +2 -2
  48. package/harness/bundles/general/codebuddy/harness-submit/checklist.md +30 -6
  49. package/harness/bundles/general/codebuddy/harness-sync/SKILL.md +2 -2
  50. package/harness/bundles/general/codebuddy/harness-test/SKILL.md +2 -2
  51. package/harness/bundles/general/codebuddy/harness-test/checklist.md +1 -1
  52. package/harness/bundles/general/codebuddy/harness-test/pitfalls.md +16 -0
  53. package/harness/bundles/general/codebuddy/harness-test/scripts/runtime-helpers.mjs +216 -0
  54. package/harness/bundles/general/codebuddy/protocols/archive-report-protocol.md +1 -1
  55. package/harness/bundles/general/codebuddy/protocols/ledger-protocol.md +55 -2
  56. package/harness/bundles/general/codebuddy/protocols/report-pipeline-protocol.md +1 -8
  57. package/harness/bundles/general/codebuddy/protocols/sensitive-info-protocol.md +13 -0
  58. package/harness/bundles/general/codebuddy/protocols/state-layout-protocol.md +24 -0
  59. package/harness/bundles/general/codebuddy/scripts/harness_archive.py +31 -14
  60. package/harness/bundles/general/codebuddy/scripts/harness_check_gate.py +103 -0
  61. package/harness/bundles/general/codebuddy/scripts/harness_deploy.py +179 -4
  62. package/harness/bundles/general/codebuddy/scripts/harness_ledger.py +269 -3
  63. package/harness/bundles/general/codebuddy/scripts/harness_preflight.py +72 -382
  64. package/harness/bundles/general/codebuddy/scripts/harness_profile.py +742 -0
  65. package/harness/bundles/general/codebuddy/scripts/harness_service.py +99 -2
  66. package/harness/bundles/general/codebuddy/scripts/harness_state.py +179 -0
  67. package/harness/bundles/general/codex/.harness-build.json +1 -1
  68. package/harness/bundles/general/codex/harness-archive/SKILL.md +1 -1
  69. package/harness/bundles/general/codex/harness-archive/reference.md +2 -8
  70. package/harness/bundles/general/codex/harness-codebase-map/SKILL.md +2 -1
  71. package/harness/bundles/general/codex/harness-knowledge-ingest/SKILL.md +35 -21
  72. package/harness/bundles/general/codex/harness-knowledge-ingest/reference.md +53 -7
  73. package/harness/bundles/general/codex/harness-knowledge-ingest/scripts/harness_knowledge.py +521 -125
  74. package/harness/bundles/general/codex/harness-knowledge-ingest/scripts/harness_knowledge_mcp.py +2 -2
  75. package/harness/bundles/general/codex/harness-knowledge-query/SKILL.md +1 -1
  76. package/harness/bundles/general/codex/harness-plan/SKILL.md +2 -1
  77. package/harness/bundles/general/codex/harness-review/SKILL.md +5 -1
  78. package/harness/bundles/general/codex/harness-run/SKILL.md +1 -1
  79. package/harness/bundles/general/codex/harness-run/checklist.md +1 -1
  80. package/harness/bundles/general/codex/harness-submit/SKILL.md +2 -2
  81. package/harness/bundles/general/codex/harness-submit/checklist.md +30 -6
  82. package/harness/bundles/general/codex/harness-sync/SKILL.md +2 -2
  83. package/harness/bundles/general/codex/harness-test/SKILL.md +2 -2
  84. package/harness/bundles/general/codex/harness-test/checklist.md +1 -1
  85. package/harness/bundles/general/codex/harness-test/pitfalls.md +16 -0
  86. package/harness/bundles/general/codex/harness-test/scripts/runtime-helpers.mjs +216 -0
  87. package/harness/bundles/general/codex/protocols/archive-report-protocol.md +1 -1
  88. package/harness/bundles/general/codex/protocols/ledger-protocol.md +55 -2
  89. package/harness/bundles/general/codex/protocols/report-pipeline-protocol.md +1 -8
  90. package/harness/bundles/general/codex/protocols/sensitive-info-protocol.md +13 -0
  91. package/harness/bundles/general/codex/protocols/state-layout-protocol.md +24 -0
  92. package/harness/bundles/general/codex/scripts/harness_archive.py +31 -14
  93. package/harness/bundles/general/codex/scripts/harness_check_gate.py +103 -0
  94. package/harness/bundles/general/codex/scripts/harness_deploy.py +179 -4
  95. package/harness/bundles/general/codex/scripts/harness_ledger.py +269 -3
  96. package/harness/bundles/general/codex/scripts/harness_preflight.py +72 -382
  97. package/harness/bundles/general/codex/scripts/harness_profile.py +742 -0
  98. package/harness/bundles/general/codex/scripts/harness_service.py +99 -2
  99. package/harness/bundles/general/codex/scripts/harness_state.py +179 -0
  100. package/harness/bundles/general/cursor/.harness-build.json +1 -1
  101. package/harness/bundles/general/cursor/harness-archive/SKILL.md +1 -1
  102. package/harness/bundles/general/cursor/harness-archive/reference.md +2 -8
  103. package/harness/bundles/general/cursor/harness-codebase-map/SKILL.md +2 -1
  104. package/harness/bundles/general/cursor/harness-knowledge-ingest/SKILL.md +35 -21
  105. package/harness/bundles/general/cursor/harness-knowledge-ingest/reference.md +53 -7
  106. package/harness/bundles/general/cursor/harness-knowledge-ingest/scripts/harness_knowledge.py +521 -125
  107. package/harness/bundles/general/cursor/harness-knowledge-ingest/scripts/harness_knowledge_mcp.py +2 -2
  108. package/harness/bundles/general/cursor/harness-knowledge-query/SKILL.md +1 -1
  109. package/harness/bundles/general/cursor/harness-plan/SKILL.md +2 -1
  110. package/harness/bundles/general/cursor/harness-review/SKILL.md +5 -1
  111. package/harness/bundles/general/cursor/harness-run/SKILL.md +1 -1
  112. package/harness/bundles/general/cursor/harness-run/checklist.md +1 -1
  113. package/harness/bundles/general/cursor/harness-submit/SKILL.md +2 -2
  114. package/harness/bundles/general/cursor/harness-submit/checklist.md +30 -6
  115. package/harness/bundles/general/cursor/harness-sync/SKILL.md +2 -2
  116. package/harness/bundles/general/cursor/harness-test/SKILL.md +2 -2
  117. package/harness/bundles/general/cursor/harness-test/checklist.md +1 -1
  118. package/harness/bundles/general/cursor/harness-test/pitfalls.md +16 -0
  119. package/harness/bundles/general/cursor/harness-test/scripts/runtime-helpers.mjs +216 -0
  120. package/harness/bundles/general/cursor/protocols/archive-report-protocol.md +1 -1
  121. package/harness/bundles/general/cursor/protocols/ledger-protocol.md +55 -2
  122. package/harness/bundles/general/cursor/protocols/report-pipeline-protocol.md +1 -8
  123. package/harness/bundles/general/cursor/protocols/sensitive-info-protocol.md +13 -0
  124. package/harness/bundles/general/cursor/protocols/state-layout-protocol.md +24 -0
  125. package/harness/bundles/general/cursor/scripts/harness_archive.py +31 -14
  126. package/harness/bundles/general/cursor/scripts/harness_check_gate.py +103 -0
  127. package/harness/bundles/general/cursor/scripts/harness_deploy.py +179 -4
  128. package/harness/bundles/general/cursor/scripts/harness_ledger.py +269 -3
  129. package/harness/bundles/general/cursor/scripts/harness_preflight.py +72 -382
  130. package/harness/bundles/general/cursor/scripts/harness_profile.py +742 -0
  131. package/harness/bundles/general/cursor/scripts/harness_service.py +99 -2
  132. package/harness/bundles/general/cursor/scripts/harness_state.py +179 -0
  133. package/harness/bundles/java/claude-code/.harness-build.json +1 -1
  134. package/harness/bundles/java/claude-code/harness-apidoc/SKILL.md +1 -1
  135. package/harness/bundles/java/claude-code/harness-archive/SKILL.md +1 -1
  136. package/harness/bundles/java/claude-code/harness-archive/reference.md +2 -8
  137. package/harness/bundles/java/claude-code/harness-codebase-map/SKILL.md +2 -1
  138. package/harness/bundles/java/claude-code/harness-knowledge-ingest/SKILL.md +35 -21
  139. package/harness/bundles/java/claude-code/harness-knowledge-ingest/reference.md +53 -7
  140. package/harness/bundles/java/claude-code/harness-knowledge-ingest/scripts/harness_knowledge.py +521 -125
  141. package/harness/bundles/java/claude-code/harness-knowledge-ingest/scripts/harness_knowledge_mcp.py +2 -2
  142. package/harness/bundles/java/claude-code/harness-knowledge-query/SKILL.md +1 -1
  143. package/harness/bundles/java/claude-code/harness-package/SKILL.md +3 -3
  144. package/harness/bundles/java/claude-code/harness-plan/SKILL.md +2 -1
  145. package/harness/bundles/java/claude-code/harness-review/SKILL.md +5 -1
  146. package/harness/bundles/java/claude-code/harness-run/SKILL.md +4 -4
  147. package/harness/bundles/java/claude-code/harness-run/checklist.md +1 -1
  148. package/harness/bundles/java/claude-code/harness-submit/SKILL.md +3 -3
  149. package/harness/bundles/java/claude-code/harness-submit/checklist.md +30 -6
  150. package/harness/bundles/java/claude-code/harness-sync/SKILL.md +2 -2
  151. package/harness/bundles/java/claude-code/harness-test/SKILL.md +5 -5
  152. package/harness/bundles/java/claude-code/harness-test/pitfalls.md +16 -0
  153. package/harness/bundles/java/claude-code/harness-test/scripts/runtime-helpers.mjs +216 -0
  154. package/harness/bundles/java/claude-code/protocols/archive-report-protocol.md +1 -1
  155. package/harness/bundles/java/claude-code/protocols/ledger-protocol.md +55 -2
  156. package/harness/bundles/java/claude-code/protocols/report-pipeline-protocol.md +1 -8
  157. package/harness/bundles/java/claude-code/protocols/sensitive-info-protocol.md +13 -0
  158. package/harness/bundles/java/claude-code/protocols/state-layout-protocol.md +24 -0
  159. package/harness/bundles/java/claude-code/scripts/harness_archive.py +31 -14
  160. package/harness/bundles/java/claude-code/scripts/harness_check_gate.py +103 -0
  161. package/harness/bundles/java/claude-code/scripts/harness_deploy.py +179 -4
  162. package/harness/bundles/java/claude-code/scripts/harness_ledger.py +269 -3
  163. package/harness/bundles/java/claude-code/scripts/harness_preflight.py +72 -382
  164. package/harness/bundles/java/claude-code/scripts/harness_profile.py +742 -0
  165. package/harness/bundles/java/claude-code/scripts/harness_service.py +99 -2
  166. package/harness/bundles/java/claude-code/scripts/harness_state.py +179 -0
  167. package/harness/bundles/java/codebuddy/.harness-build.json +1 -1
  168. package/harness/bundles/java/codebuddy/harness-apidoc/SKILL.md +1 -1
  169. package/harness/bundles/java/codebuddy/harness-archive/SKILL.md +1 -1
  170. package/harness/bundles/java/codebuddy/harness-archive/reference.md +2 -8
  171. package/harness/bundles/java/codebuddy/harness-codebase-map/SKILL.md +2 -1
  172. package/harness/bundles/java/codebuddy/harness-knowledge-ingest/SKILL.md +35 -21
  173. package/harness/bundles/java/codebuddy/harness-knowledge-ingest/reference.md +53 -7
  174. package/harness/bundles/java/codebuddy/harness-knowledge-ingest/scripts/harness_knowledge.py +521 -125
  175. package/harness/bundles/java/codebuddy/harness-knowledge-ingest/scripts/harness_knowledge_mcp.py +2 -2
  176. package/harness/bundles/java/codebuddy/harness-knowledge-query/SKILL.md +1 -1
  177. package/harness/bundles/java/codebuddy/harness-package/SKILL.md +3 -3
  178. package/harness/bundles/java/codebuddy/harness-plan/SKILL.md +2 -1
  179. package/harness/bundles/java/codebuddy/harness-review/SKILL.md +5 -1
  180. package/harness/bundles/java/codebuddy/harness-run/SKILL.md +4 -4
  181. package/harness/bundles/java/codebuddy/harness-run/checklist.md +1 -1
  182. package/harness/bundles/java/codebuddy/harness-submit/SKILL.md +3 -3
  183. package/harness/bundles/java/codebuddy/harness-submit/checklist.md +30 -6
  184. package/harness/bundles/java/codebuddy/harness-sync/SKILL.md +2 -2
  185. package/harness/bundles/java/codebuddy/harness-test/SKILL.md +5 -5
  186. package/harness/bundles/java/codebuddy/harness-test/pitfalls.md +16 -0
  187. package/harness/bundles/java/codebuddy/harness-test/scripts/runtime-helpers.mjs +216 -0
  188. package/harness/bundles/java/codebuddy/protocols/archive-report-protocol.md +1 -1
  189. package/harness/bundles/java/codebuddy/protocols/ledger-protocol.md +55 -2
  190. package/harness/bundles/java/codebuddy/protocols/report-pipeline-protocol.md +1 -8
  191. package/harness/bundles/java/codebuddy/protocols/sensitive-info-protocol.md +13 -0
  192. package/harness/bundles/java/codebuddy/protocols/state-layout-protocol.md +24 -0
  193. package/harness/bundles/java/codebuddy/scripts/harness_archive.py +31 -14
  194. package/harness/bundles/java/codebuddy/scripts/harness_check_gate.py +103 -0
  195. package/harness/bundles/java/codebuddy/scripts/harness_deploy.py +179 -4
  196. package/harness/bundles/java/codebuddy/scripts/harness_ledger.py +269 -3
  197. package/harness/bundles/java/codebuddy/scripts/harness_preflight.py +72 -382
  198. package/harness/bundles/java/codebuddy/scripts/harness_profile.py +742 -0
  199. package/harness/bundles/java/codebuddy/scripts/harness_service.py +99 -2
  200. package/harness/bundles/java/codebuddy/scripts/harness_state.py +179 -0
  201. package/harness/bundles/java/codex/.harness-build.json +1 -1
  202. package/harness/bundles/java/codex/harness-apidoc/SKILL.md +1 -1
  203. package/harness/bundles/java/codex/harness-archive/SKILL.md +1 -1
  204. package/harness/bundles/java/codex/harness-archive/reference.md +2 -8
  205. package/harness/bundles/java/codex/harness-codebase-map/SKILL.md +2 -1
  206. package/harness/bundles/java/codex/harness-knowledge-ingest/SKILL.md +35 -21
  207. package/harness/bundles/java/codex/harness-knowledge-ingest/reference.md +53 -7
  208. package/harness/bundles/java/codex/harness-knowledge-ingest/scripts/harness_knowledge.py +521 -125
  209. package/harness/bundles/java/codex/harness-knowledge-ingest/scripts/harness_knowledge_mcp.py +2 -2
  210. package/harness/bundles/java/codex/harness-knowledge-query/SKILL.md +1 -1
  211. package/harness/bundles/java/codex/harness-package/SKILL.md +3 -3
  212. package/harness/bundles/java/codex/harness-plan/SKILL.md +2 -1
  213. package/harness/bundles/java/codex/harness-review/SKILL.md +5 -1
  214. package/harness/bundles/java/codex/harness-run/SKILL.md +4 -4
  215. package/harness/bundles/java/codex/harness-run/checklist.md +1 -1
  216. package/harness/bundles/java/codex/harness-submit/SKILL.md +3 -3
  217. package/harness/bundles/java/codex/harness-submit/checklist.md +30 -6
  218. package/harness/bundles/java/codex/harness-sync/SKILL.md +2 -2
  219. package/harness/bundles/java/codex/harness-test/SKILL.md +5 -5
  220. package/harness/bundles/java/codex/harness-test/pitfalls.md +16 -0
  221. package/harness/bundles/java/codex/harness-test/scripts/runtime-helpers.mjs +216 -0
  222. package/harness/bundles/java/codex/protocols/archive-report-protocol.md +1 -1
  223. package/harness/bundles/java/codex/protocols/ledger-protocol.md +55 -2
  224. package/harness/bundles/java/codex/protocols/report-pipeline-protocol.md +1 -8
  225. package/harness/bundles/java/codex/protocols/sensitive-info-protocol.md +13 -0
  226. package/harness/bundles/java/codex/protocols/state-layout-protocol.md +24 -0
  227. package/harness/bundles/java/codex/scripts/harness_archive.py +31 -14
  228. package/harness/bundles/java/codex/scripts/harness_check_gate.py +103 -0
  229. package/harness/bundles/java/codex/scripts/harness_deploy.py +179 -4
  230. package/harness/bundles/java/codex/scripts/harness_ledger.py +269 -3
  231. package/harness/bundles/java/codex/scripts/harness_preflight.py +72 -382
  232. package/harness/bundles/java/codex/scripts/harness_profile.py +742 -0
  233. package/harness/bundles/java/codex/scripts/harness_service.py +99 -2
  234. package/harness/bundles/java/codex/scripts/harness_state.py +179 -0
  235. package/harness/bundles/java/cursor/.harness-build.json +1 -1
  236. package/harness/bundles/java/cursor/harness-apidoc/SKILL.md +1 -1
  237. package/harness/bundles/java/cursor/harness-archive/SKILL.md +1 -1
  238. package/harness/bundles/java/cursor/harness-archive/reference.md +2 -8
  239. package/harness/bundles/java/cursor/harness-codebase-map/SKILL.md +2 -1
  240. package/harness/bundles/java/cursor/harness-knowledge-ingest/SKILL.md +35 -21
  241. package/harness/bundles/java/cursor/harness-knowledge-ingest/reference.md +53 -7
  242. package/harness/bundles/java/cursor/harness-knowledge-ingest/scripts/harness_knowledge.py +521 -125
  243. package/harness/bundles/java/cursor/harness-knowledge-ingest/scripts/harness_knowledge_mcp.py +2 -2
  244. package/harness/bundles/java/cursor/harness-knowledge-query/SKILL.md +1 -1
  245. package/harness/bundles/java/cursor/harness-package/SKILL.md +3 -3
  246. package/harness/bundles/java/cursor/harness-plan/SKILL.md +2 -1
  247. package/harness/bundles/java/cursor/harness-review/SKILL.md +5 -1
  248. package/harness/bundles/java/cursor/harness-run/SKILL.md +4 -4
  249. package/harness/bundles/java/cursor/harness-run/checklist.md +1 -1
  250. package/harness/bundles/java/cursor/harness-submit/SKILL.md +3 -3
  251. package/harness/bundles/java/cursor/harness-submit/checklist.md +30 -6
  252. package/harness/bundles/java/cursor/harness-sync/SKILL.md +2 -2
  253. package/harness/bundles/java/cursor/harness-test/SKILL.md +5 -5
  254. package/harness/bundles/java/cursor/harness-test/pitfalls.md +16 -0
  255. package/harness/bundles/java/cursor/harness-test/scripts/runtime-helpers.mjs +216 -0
  256. package/harness/bundles/java/cursor/protocols/archive-report-protocol.md +1 -1
  257. package/harness/bundles/java/cursor/protocols/ledger-protocol.md +55 -2
  258. package/harness/bundles/java/cursor/protocols/report-pipeline-protocol.md +1 -8
  259. package/harness/bundles/java/cursor/protocols/sensitive-info-protocol.md +13 -0
  260. package/harness/bundles/java/cursor/protocols/state-layout-protocol.md +24 -0
  261. package/harness/bundles/java/cursor/scripts/harness_archive.py +31 -14
  262. package/harness/bundles/java/cursor/scripts/harness_check_gate.py +103 -0
  263. package/harness/bundles/java/cursor/scripts/harness_deploy.py +179 -4
  264. package/harness/bundles/java/cursor/scripts/harness_ledger.py +269 -3
  265. package/harness/bundles/java/cursor/scripts/harness_preflight.py +72 -382
  266. package/harness/bundles/java/cursor/scripts/harness_profile.py +742 -0
  267. package/harness/bundles/java/cursor/scripts/harness_service.py +99 -2
  268. package/harness/bundles/java/cursor/scripts/harness_state.py +179 -0
  269. package/harness/manifests/general/claude-code.json +46 -30
  270. package/harness/manifests/general/codebuddy.json +46 -30
  271. package/harness/manifests/general/codex.json +46 -30
  272. package/harness/manifests/general/cursor.json +46 -30
  273. package/harness/manifests/java/claude-code.json +47 -31
  274. package/harness/manifests/java/codebuddy.json +47 -31
  275. package/harness/manifests/java/codex.json +47 -31
  276. package/harness/manifests/java/cursor.json +47 -31
  277. package/hunter-workflow-family.json +2 -2
  278. package/package.json +1 -1
@@ -58,6 +58,20 @@ DEFAULT_AUTO_KNOWLEDGE_CONFIG = {
58
58
  "supersededPenalty": 0.8,
59
59
  "conflictPenalty": 0.8,
60
60
  },
61
+ "activeLifecycle": {
62
+ "autoDemote": True,
63
+ "targetStatus": "stale",
64
+ },
65
+ "knowledgeValidation": {
66
+ "enabled": True,
67
+ "autoDemoteActive": True,
68
+ "defaultTargetStatus": "stale",
69
+ "allowCommandValidators": False,
70
+ "commandTimeoutSeconds": 60,
71
+ },
72
+ "judge": {
73
+ "maxCandidatesPerRun": 100,
74
+ },
61
75
  }
62
76
 
63
77
 
@@ -83,6 +97,29 @@ def write_text(path: Path, text: str) -> None:
83
97
  path.write_text(text, encoding="utf-8")
84
98
 
85
99
 
100
+ def _serialize_json_bytes(data: Any) -> bytes:
101
+ return (json.dumps(data, ensure_ascii=False, indent=2) + "\n").encode("utf-8")
102
+
103
+
104
+ def write_json_if_changed(path: Path, data: Any) -> bool:
105
+ """Write only when byte content would differ; returns True iff a write occurred."""
106
+ path.parent.mkdir(parents=True, exist_ok=True)
107
+ payload = _serialize_json_bytes(data)
108
+ if path.exists() and path.read_bytes() == payload:
109
+ return False
110
+ path.write_bytes(payload)
111
+ return True
112
+
113
+
114
+ def write_text_if_changed(path: Path, text: str) -> bool:
115
+ path.parent.mkdir(parents=True, exist_ok=True)
116
+ payload = text.encode("utf-8")
117
+ if path.exists() and path.read_bytes() == payload:
118
+ return False
119
+ path.write_bytes(payload)
120
+ return True
121
+
122
+
86
123
  def json_clone(value: Any) -> Any:
87
124
  return json.loads(json.dumps(value, ensure_ascii=False))
88
125
 
@@ -106,6 +143,9 @@ def ensure_auto_knowledge_config(knowledge: Path) -> dict[str, Any]:
106
143
  "path": str(config_path),
107
144
  "created": False,
108
145
  "autoPromoteEnabled": auto_promote_config(config)["enabled"],
146
+ "autoDemoteEnabled": active_lifecycle_config(config)["autoDemote"],
147
+ "autoDemoteActiveEnabled": knowledge_validation_config(config)["autoDemoteActive"],
148
+ "maxCandidatesPerRun": judge_config(config)["maxCandidatesPerRun"],
109
149
  "appliedBy": "existing",
110
150
  "candidateAutoPromoted": None,
111
151
  }
@@ -115,6 +155,9 @@ def ensure_auto_knowledge_config(knowledge: Path) -> dict[str, Any]:
115
155
  "path": str(config_path),
116
156
  "created": True,
117
157
  "autoPromoteEnabled": True,
158
+ "autoDemoteEnabled": True,
159
+ "autoDemoteActiveEnabled": True,
160
+ "maxCandidatesPerRun": judge_config(config)["maxCandidatesPerRun"],
118
161
  "appliedBy": None,
119
162
  "candidateAutoPromoted": None,
120
163
  }
@@ -185,6 +228,16 @@ def auto_promote_config(config: dict[str, Any]) -> dict[str, Any]:
185
228
  }
186
229
 
187
230
 
231
+ def judge_config(config: dict[str, Any]) -> dict[str, Any]:
232
+ raw = config.get("judge")
233
+ if not isinstance(raw, dict):
234
+ raw = {}
235
+ max_candidates = int(raw.get("maxCandidatesPerRun") or 100)
236
+ return {
237
+ "maxCandidatesPerRun": max(1, max_candidates),
238
+ }
239
+
240
+
188
241
  def sha256_file(path: Path) -> str:
189
242
  h = hashlib.sha256()
190
243
  with path.open("rb") as f:
@@ -591,21 +644,7 @@ def looks_like_contract_path(path: str) -> bool:
591
644
  return any(part in lower for part in ["openapi", "contract", "schema", "protocol", "api"])
592
645
 
593
646
 
594
- def reset_generated_knowledge(knowledge: Path) -> None:
595
- for sub in ["entries/candidate", "entries/stale", "entries/conflicted", "entries/superseded"]:
596
- path = knowledge / sub
597
- if path.exists():
598
- if sub in {"entries/candidate", "entries/stale"}:
599
- for entry_path in sorted(path.glob("*.json")):
600
- try:
601
- entry = read_json(entry_path)
602
- except (OSError, json.JSONDecodeError):
603
- entry_path.unlink(missing_ok=True)
604
- continue
605
- if not isinstance(entry, dict) or not entry.get("lifecycle", {}).get("demotedAt"):
606
- entry_path.unlink(missing_ok=True)
607
- else:
608
- shutil.rmtree(path)
647
+ def ensure_knowledge_dirs(knowledge: Path) -> None:
609
648
  for sub in [
610
649
  "entries/candidate",
611
650
  "entries/active",
@@ -620,6 +659,50 @@ def reset_generated_knowledge(knowledge: Path) -> None:
620
659
  (knowledge / sub).mkdir(parents=True, exist_ok=True)
621
660
 
622
661
 
662
+ def prune_generated_entries(knowledge: Path, current_entries: list[dict[str, Any]]) -> int:
663
+ """Remove generated entry files that disappeared or moved to another status dir.
664
+
665
+ Replaces the old reset-and-rewrite sweep. Preserved entries (active, and
666
+ candidate/stale carrying a manual ``demotedAt``) are never removed, so the
667
+ write-if-changed persist step can detect unchanged entries by content.
668
+ Returns the number of files removed.
669
+ """
670
+ current_by_id: dict[str, dict[str, Any]] = {}
671
+ for entry in current_entries:
672
+ if isinstance(entry, dict) and entry.get("id"):
673
+ current_by_id[str(entry["id"])] = entry
674
+ preserved_ids = {entry["id"] for entry in load_preserved_entries(knowledge)}
675
+ removed = 0
676
+ for status in ["candidate", "stale", "superseded", "conflicted"]:
677
+ status_dir = knowledge / "entries" / status
678
+ if not status_dir.exists():
679
+ continue
680
+ for path in sorted(status_dir.glob("*.json")):
681
+ try:
682
+ entry = read_json(path)
683
+ except (OSError, json.JSONDecodeError):
684
+ path.unlink(missing_ok=True)
685
+ removed += 1
686
+ continue
687
+ eid = entry.get("id") if isinstance(entry, dict) else None
688
+ if eid is None:
689
+ path.unlink(missing_ok=True)
690
+ removed += 1
691
+ continue
692
+ if eid in preserved_ids:
693
+ continue
694
+ replacement = current_by_id.get(str(eid))
695
+ if replacement is None:
696
+ path.unlink(missing_ok=True)
697
+ removed += 1
698
+ elif str(replacement.get("status")) != status:
699
+ # entry moved to another status dir; the new file was already
700
+ # written by the persist step, so remove the stale old file.
701
+ path.unlink(missing_ok=True)
702
+ removed += 1
703
+ return removed
704
+
705
+
623
706
  def load_entries_from_dir(path: Path) -> list[dict[str, Any]]:
624
707
  entries: list[dict[str, Any]] = []
625
708
  if not path.exists():
@@ -646,7 +729,6 @@ def load_cached_archive_entries(
646
729
  *,
647
730
  summary_path: str,
648
731
  summary_hash: str,
649
- head_commit: str | None,
650
732
  ) -> list[dict[str, Any]] | None:
651
733
  if not cache_path.exists():
652
734
  return None
@@ -662,8 +744,9 @@ def load_cached_archive_entries(
662
744
  return None
663
745
  if payload.get("summarySha256") != summary_hash:
664
746
  return None
665
- if payload.get("headCommit") != head_commit:
666
- return None
747
+ # headCommit is intentionally ignored: archive extraction depends only on the
748
+ # summary-data.json content, so an unrelated HEAD change must not invalidate
749
+ # the extraction cache (design §3.5, cluster 6 — true incremental).
667
750
  entries = payload.get("entries")
668
751
  if not isinstance(entries, list):
669
752
  return None
@@ -677,7 +760,6 @@ def write_cached_archive_entries(
677
760
  *,
678
761
  summary_path: str,
679
762
  summary_hash: str,
680
- head_commit: str | None,
681
763
  entries: list[dict[str, Any]],
682
764
  ) -> None:
683
765
  write_json(
@@ -687,7 +769,6 @@ def write_cached_archive_entries(
687
769
  "generatedAt": now_iso(),
688
770
  "summaryData": summary_path,
689
771
  "summarySha256": summary_hash,
690
- "headCommit": head_commit,
691
772
  "entries": entries,
692
773
  },
693
774
  )
@@ -833,7 +914,25 @@ def calculate_confidence(entry: dict[str, Any], config: dict[str, Any]) -> dict[
833
914
 
834
915
  def apply_confidence_scores(entries: list[dict[str, Any]], config: dict[str, Any]) -> None:
835
916
  for entry in entries:
836
- entry["confidence"] = calculate_confidence(entry, config)
917
+ fresh = calculate_confidence(entry, config)
918
+ existing = entry.get("confidence") if isinstance(entry.get("confidence"), dict) else None
919
+ if existing is not None and (
920
+ existing.get("score") == fresh["score"]
921
+ and existing.get("level") == fresh["level"]
922
+ and existing.get("signals") == fresh["signals"]
923
+ ):
924
+ # The inputs that determine confidence are unchanged; keep the previous
925
+ # lastCalculatedAt so the entry content (and mtime) does not churn on
926
+ # every ingest. This is what makes write-if-changed effective for
927
+ # unchanged entries.
928
+ entry["confidence"] = {
929
+ "score": fresh["score"],
930
+ "level": fresh["level"],
931
+ "signals": fresh["signals"],
932
+ "lastCalculatedAt": existing.get("lastCalculatedAt"),
933
+ }
934
+ else:
935
+ entry["confidence"] = fresh
837
936
 
838
937
 
839
938
  def should_auto_promote(entry: dict[str, Any], policy: dict[str, Any]) -> bool:
@@ -896,14 +995,47 @@ def apply_auto_promote_policy(entries: list[dict[str, Any]], config: dict[str, A
896
995
  return actions
897
996
 
898
997
 
899
- def persist_entry_updates(knowledge: Path, entries: list[dict[str, Any]]) -> None:
998
+ def _preserve_confidence_timestamp(target: Path, entry: dict[str, Any]) -> None:
999
+ """Keep the on-disk ``lastCalculatedAt`` when confidence is otherwise unchanged.
1000
+
1001
+ Extraction cache stores raw entries whose ``confidence`` is still the legacy
1002
+ string, so the first ``apply_confidence_scores`` of every build would mint a
1003
+ fresh ``lastCalculatedAt`` and churn unchanged entries. By re-reading the
1004
+ on-disk entry just before writing, an unchanged entry keeps its previous
1005
+ timestamp and ``write_json_if_changed`` becomes a true no-op (cluster 6).
1006
+ """
1007
+ if not target.exists():
1008
+ return
1009
+ try:
1010
+ disk = read_json(target)
1011
+ except (OSError, json.JSONDecodeError):
1012
+ return
1013
+ if not isinstance(disk, dict) or disk.get("id") != entry.get("id"):
1014
+ return
1015
+ disk_conf = disk.get("confidence") if isinstance(disk.get("confidence"), dict) else None
1016
+ new_conf = entry.get("confidence") if isinstance(entry.get("confidence"), dict) else None
1017
+ if not (disk_conf and new_conf):
1018
+ return
1019
+ if (
1020
+ disk_conf.get("score") == new_conf.get("score")
1021
+ and disk_conf.get("level") == new_conf.get("level")
1022
+ and disk_conf.get("signals") == new_conf.get("signals")
1023
+ ):
1024
+ entry["confidence"]["lastCalculatedAt"] = disk_conf.get("lastCalculatedAt")
1025
+
1026
+
1027
+ def persist_entry_updates(knowledge: Path, entries: list[dict[str, Any]]) -> int:
1028
+ written = 0
900
1029
  for entry in entries:
901
1030
  status = str(entry.get("status") or "")
902
1031
  if status not in {"candidate", "active", "stale", "superseded", "conflicted"}:
903
1032
  continue
904
1033
  path = knowledge / "entries" / status / entry_filename(entry)
905
1034
  if path.exists():
906
- write_json(path, entry)
1035
+ _preserve_confidence_timestamp(path, entry)
1036
+ if write_json_if_changed(path, entry):
1037
+ written += 1
1038
+ return written
907
1039
 
908
1040
 
909
1041
  def normalized_entry_text(entry: dict[str, Any]) -> str:
@@ -1584,21 +1716,196 @@ def apply_ttl_stale(entries: list[dict[str, Any]], config: dict[str, Any]) -> No
1584
1716
  lifecycle["lastCheckedAt"] = now_iso()
1585
1717
 
1586
1718
 
1587
- def build_index(project: Path, incremental: bool = True) -> dict[str, Any]:
1719
+ SQLITE_SCHEMA_VERSION = 1
1720
+
1721
+
1722
+ def compute_inputs_hash(
1723
+ records: list[dict[str, Any]], config: dict[str, Any], knowledge: Path
1724
+ ) -> str:
1725
+ """Stable hash of the inputs that determine knowledge index content.
1726
+
1727
+ Excludes HEAD on purpose: a business-code commit that does not touch any
1728
+ archive must not invalidate the index (design §3.5, cluster 6 — true
1729
+ incremental). Inputs are archive checksums + knowledge config + schema
1730
+ versions + preserved entries (active / manually demoted), so promote/demote
1731
+ correctly invalidate the no-op fast path.
1732
+ """
1733
+ archive_fingerprint = json.dumps(
1734
+ sorted(
1735
+ (
1736
+ {"path": r.get("summaryData"), "sha256": r.get("summarySha256")}
1737
+ for r in records
1738
+ ),
1739
+ key=lambda item: item["path"] or "",
1740
+ ),
1741
+ ensure_ascii=False,
1742
+ sort_keys=True,
1743
+ )
1744
+ config_fingerprint = json.dumps(config, ensure_ascii=False, sort_keys=True)
1745
+ schema_fingerprint = json.dumps(
1746
+ {
1747
+ "entrySchemaVersion": 1,
1748
+ "indexSchemaVersion": 1,
1749
+ "sqliteSchemaVersion": SQLITE_SCHEMA_VERSION,
1750
+ },
1751
+ sort_keys=True,
1752
+ )
1753
+ preserved = load_preserved_entries(knowledge)
1754
+ preserved_fingerprint = json.dumps(
1755
+ sorted(
1756
+ (
1757
+ {
1758
+ "id": e.get("id"),
1759
+ "status": e.get("status"),
1760
+ "lifecycle": e.get("lifecycle", {}),
1761
+ }
1762
+ for e in preserved
1763
+ ),
1764
+ key=lambda item: item["id"] or "",
1765
+ ),
1766
+ ensure_ascii=False,
1767
+ sort_keys=True,
1768
+ )
1769
+ digest = hashlib.sha256()
1770
+ digest.update(archive_fingerprint.encode("utf-8"))
1771
+ digest.update(b"|config|")
1772
+ digest.update(config_fingerprint.encode("utf-8"))
1773
+ digest.update(b"|schema|")
1774
+ digest.update(schema_fingerprint.encode("utf-8"))
1775
+ digest.update(b"|preserved|")
1776
+ digest.update(preserved_fingerprint.encode("utf-8"))
1777
+ return digest.hexdigest()
1778
+
1779
+
1780
+ class KnowledgeSnapshot:
1781
+ """Shared per-invocation snapshot (design §3.5 / cluster 6 要点2).
1782
+
1783
+ Loads config + archive records + inputs_hash once so auto/maintain/sync/query
1784
+ can pass it through instead of independently recomputing inputs_hash, reloading
1785
+ config, or rescanning archives. HEAD is intentionally not part of the snapshot
1786
+ -- an unrelated business-code commit must not invalidate the index.
1787
+ """
1788
+
1789
+ __slots__ = (
1790
+ "project",
1791
+ "knowledge",
1792
+ "pname",
1793
+ "config",
1794
+ "summary_paths",
1795
+ "archive_records",
1796
+ "inputs_hash",
1797
+ )
1798
+
1799
+ def __init__(
1800
+ self,
1801
+ project: Path,
1802
+ knowledge: Path,
1803
+ pname: str,
1804
+ config: dict[str, Any],
1805
+ summary_paths: list[Path],
1806
+ archive_records: list[dict[str, Any]],
1807
+ inputs_hash: str,
1808
+ ) -> None:
1809
+ self.project = project
1810
+ self.knowledge = knowledge
1811
+ self.pname = pname
1812
+ self.config = config
1813
+ self.summary_paths = summary_paths
1814
+ self.archive_records = archive_records
1815
+ self.inputs_hash = inputs_hash
1816
+
1817
+
1818
+ def build_snapshot(project: Path) -> KnowledgeSnapshot:
1819
+ """Load config + archive records + inputs_hash exactly once for one invocation."""
1588
1820
  project = project.resolve()
1589
- harness = project / ".harness"
1590
- archive_root = harness / "archive"
1591
- knowledge = harness / "knowledge"
1821
+ knowledge = project / ".harness" / "knowledge"
1592
1822
  pname = project_id(project)
1593
1823
  config = load_config(knowledge)
1594
- current_head = git_head(project)
1595
- reset_generated_knowledge(knowledge)
1824
+ summary_paths = sorted(
1825
+ (project / ".harness" / "archive").glob("*/reports/final/summary-data.json")
1826
+ )
1827
+ archive_records = archive_summary_records(project, summary_paths)
1828
+ inputs_hash = compute_inputs_hash(archive_records, config, knowledge)
1829
+ return KnowledgeSnapshot(
1830
+ project, knowledge, pname, config, summary_paths, archive_records, inputs_hash
1831
+ )
1596
1832
 
1597
- summary_paths = sorted(archive_root.glob("*/reports/final/summary-data.json"))
1833
+
1834
+ def build_index(
1835
+ project: Path,
1836
+ incremental: bool = True,
1837
+ *,
1838
+ snapshot: KnowledgeSnapshot | None = None,
1839
+ ) -> dict[str, Any]:
1840
+ project = project.resolve()
1841
+ if snapshot is not None:
1842
+ # Reuse the single-invocation snapshot: no recomputation of
1843
+ # config / archive records / inputs_hash (design §3.5, cluster 6 要点2).
1844
+ knowledge = snapshot.knowledge
1845
+ pname = snapshot.pname
1846
+ config = snapshot.config
1847
+ summary_paths = snapshot.summary_paths
1848
+ archive_records = snapshot.archive_records
1849
+ inputs_hash = snapshot.inputs_hash
1850
+ else:
1851
+ knowledge = project / ".harness" / "knowledge"
1852
+ pname = project_id(project)
1853
+ config = load_config(knowledge)
1854
+ summary_paths = sorted(
1855
+ (project / ".harness" / "archive").glob("*/reports/final/summary-data.json")
1856
+ )
1857
+ archive_records = archive_summary_records(project, summary_paths)
1858
+ inputs_hash = compute_inputs_hash(archive_records, config, knowledge)
1859
+ ensure_knowledge_dirs(knowledge)
1860
+
1861
+ # No-op fast path: inputs (archive checksums + config + schema) are unchanged.
1862
+ # Write nothing — entries, sqlite, index and views all stay byte-identical,
1863
+ # so a repeated ingest is a true no-op (design §3.5, cluster 6, UT-025).
1864
+ # Also require index.sqlite to exist so a query never sees a stale index.json
1865
+ # pointing at a missing sqlite (API-009 single ensure-current).
1866
+ if incremental:
1867
+ old_index: dict[str, Any] | None = None
1868
+ index_path = knowledge / "index.json"
1869
+ if index_path.exists():
1870
+ try:
1871
+ old_index = read_json(index_path)
1872
+ except (OSError, json.JSONDecodeError):
1873
+ old_index = None
1874
+ if (
1875
+ isinstance(old_index, dict)
1876
+ and old_index.get("inputsHash") == inputs_hash
1877
+ and (knowledge / "index.sqlite").exists()
1878
+ ):
1879
+ stale_mode = dict(old_index.get("ingestMode", {}))
1880
+ stale_mode.update(
1881
+ {
1882
+ "mode": "no-op",
1883
+ "incremental": incremental,
1884
+ "inputsHash": inputs_hash,
1885
+ "entriesWritten": 0,
1886
+ "entriesPruned": 0,
1887
+ "archivesExtracted": 0,
1888
+ "archivesReused": len(summary_paths),
1889
+ "cacheWrites": 0,
1890
+ "sqliteRebuild": 0,
1891
+ "sqliteUpsert": 0,
1892
+ "sqliteDelete": 0,
1893
+ }
1894
+ )
1895
+ result = dict(old_index)
1896
+ result["ingestMode"] = stale_mode
1897
+ return result
1898
+
1899
+ mode = "cold" if not (knowledge / "index.json").exists() else "warm"
1900
+ current_head = git_head(project) # recorded in manifest only; not an invalidation key
1598
1901
  entries: list[dict[str, Any]] = []
1599
1902
  failures: list[dict[str, str]] = []
1600
1903
  ingest_mode: dict[str, Any] = {
1601
1904
  "incremental": incremental,
1905
+ "mode": mode,
1906
+ "inputsHash": inputs_hash,
1907
+ "entriesWritten": 0,
1908
+ "entriesPruned": 0,
1602
1909
  "archivesExtracted": 0,
1603
1910
  "archivesReused": 0,
1604
1911
  "cacheWrites": 0,
@@ -1608,6 +1915,9 @@ def build_index(project: Path, incremental: bool = True) -> dict[str, Any]:
1608
1915
  "validationChecked": 0,
1609
1916
  "validationFailed": 0,
1610
1917
  "validationAutoDemoted": 0,
1918
+ "sqliteRebuild": 0,
1919
+ "sqliteUpsert": 0,
1920
+ "sqliteDelete": 0,
1611
1921
  }
1612
1922
 
1613
1923
  for summary_path in summary_paths:
@@ -1621,7 +1931,6 @@ def build_index(project: Path, incremental: bool = True) -> dict[str, Any]:
1621
1931
  cache_path,
1622
1932
  summary_path=summary_rel,
1623
1933
  summary_hash=summary_hash,
1624
- head_commit=current_head,
1625
1934
  )
1626
1935
  if archive_entries is None:
1627
1936
  archive_entries = extract_entries(project, pname, summary_path)
@@ -1631,7 +1940,6 @@ def build_index(project: Path, incremental: bool = True) -> dict[str, Any]:
1631
1940
  cache_path,
1632
1941
  summary_path=summary_rel,
1633
1942
  summary_hash=summary_hash,
1634
- head_commit=current_head,
1635
1943
  entries=archive_entries,
1636
1944
  )
1637
1945
  ingest_mode["cacheWrites"] += 1
@@ -1686,11 +1994,13 @@ def build_index(project: Path, incremental: bool = True) -> dict[str, Any]:
1686
1994
  failures.append({"id": entry["id"], "reason": "filename collision",
1687
1995
  "path": str(target), "conflictsWith": existing.get("id")})
1688
1996
  continue
1689
- write_json(target, entry)
1997
+ _preserve_confidence_timestamp(target, entry)
1998
+ if write_json_if_changed(target, entry):
1999
+ ingest_mode["entriesWritten"] += 1
1690
2000
 
1691
2001
  indexed_entries = combine_generated_with_preserved(knowledge, deduped)
1692
2002
  apply_confidence_scores(indexed_entries, config)
1693
- persist_entry_updates(knowledge, indexed_entries)
2003
+ ingest_mode["entriesWritten"] += persist_entry_updates(knowledge, indexed_entries)
1694
2004
  auto_demotions = apply_active_lifecycle_policy(knowledge, indexed_entries, config)
1695
2005
  ingest_mode["activeAutoDemoted"] = len(auto_demotions)
1696
2006
  validation = apply_knowledge_validation(project, knowledge, config)
@@ -1702,10 +2012,12 @@ def build_index(project: Path, incremental: bool = True) -> dict[str, Any]:
1702
2012
  if validation["checked"]:
1703
2013
  indexed_entries = [entry for _, entry in load_entry_files(knowledge)]
1704
2014
  apply_confidence_scores(indexed_entries, config)
1705
- persist_entry_updates(knowledge, indexed_entries)
2015
+ ingest_mode["entriesWritten"] += persist_entry_updates(knowledge, indexed_entries)
1706
2016
  ingest_mode["confidenceScored"] = len(indexed_entries)
1707
- archive_records = archive_summary_records(project, summary_paths)
1708
- write_sqlite(knowledge / "index.sqlite", indexed_entries)
2017
+ ingest_mode["entriesPruned"] = prune_generated_entries(knowledge, indexed_entries)
2018
+
2019
+ sqlite_stats = write_sqlite(knowledge / "index.sqlite", indexed_entries)
2020
+ ingest_mode.update(sqlite_stats)
1709
2021
  index = make_manifest(
1710
2022
  project,
1711
2023
  pname,
@@ -1716,7 +2028,7 @@ def build_index(project: Path, incremental: bool = True) -> dict[str, Any]:
1716
2028
  duplicates,
1717
2029
  ingest_mode,
1718
2030
  )
1719
- write_json(knowledge / "index.json", index)
2031
+ write_json_if_changed(knowledge / "index.json", index)
1720
2032
  write_views(knowledge, index, indexed_entries)
1721
2033
  write_ingest_report(knowledge, index, failures, duplicates)
1722
2034
  return index
@@ -1979,6 +2291,7 @@ def make_manifest(
1979
2291
  "projectId": pname,
1980
2292
  "projectRoot": str(project),
1981
2293
  "headCommit": git_head(project),
2294
+ "inputsHash": (ingest_mode or {}).get("inputsHash"),
1982
2295
  "archives": {
1983
2296
  "scanned": len(summary_paths),
1984
2297
  "indexed": len(summary_paths) - len(failures),
@@ -2005,90 +2318,153 @@ def make_manifest(
2005
2318
  }
2006
2319
 
2007
2320
 
2008
- def write_sqlite(path: Path, entries: list[dict[str, Any]]) -> None:
2321
+ def write_sqlite(path: Path, entries: list[dict[str, Any]]) -> dict[str, Any]:
2322
+ """Persist entries to SQLite using a transactional dirty-set.
2323
+
2324
+ Reads existing rows, then upserts only changed entries and deletes removed
2325
+ ones within a single transaction. When the schema version changes or the
2326
+ table is missing, performs a full rebuild. If nothing changed the file is
2327
+ not touched (cluster 6, design §3.5 — true incremental).
2328
+ """
2009
2329
  path.parent.mkdir(parents=True, exist_ok=True)
2010
- if path.exists():
2011
- path.unlink()
2330
+ stats: dict[str, Any] = {
2331
+ "sqliteRebuild": 0,
2332
+ "sqliteUpsert": 0,
2333
+ "sqliteDelete": 0,
2334
+ "sqliteUnchanged": 0,
2335
+ }
2012
2336
  con = sqlite3.connect(path)
2013
2337
  try:
2014
2338
  con.execute("pragma journal_mode=wal")
2015
- con.execute(
2016
- """
2017
- create table entries (
2018
- id text primary key,
2019
- project_id text not null,
2020
- type text not null,
2021
- status text not null,
2022
- title text not null,
2023
- summary text not null,
2024
- body text not null,
2025
- source_archive text not null,
2026
- source_commit text,
2027
- source_files_json text not null,
2028
- keywords_json text not null,
2029
- entry_json text not null
2030
- )
2031
- """
2032
- )
2033
- con.execute(
2034
- """
2035
- create table entry_files (
2036
- entry_id text not null,
2037
- source_file text not null,
2038
- primary key (entry_id, source_file),
2039
- foreign key (entry_id) references entries(id)
2040
- )
2041
- """
2339
+ table_exists = con.execute(
2340
+ "select name from sqlite_master where type='table' and name='entries'"
2341
+ ).fetchone()
2342
+ existing_version = (
2343
+ con.execute("pragma user_version").fetchone()[0] if table_exists else 0
2042
2344
  )
2043
- con.execute("create virtual table entries_fts using fts5(id, title, summary, body, keywords)")
2044
- con.execute("create index idx_entries_status on entries(status)")
2045
- con.execute("create index idx_entries_type on entries(type)")
2046
- con.execute("create index idx_entries_source_archive on entries(source_archive)")
2047
- con.execute("create index idx_entry_files_source_file on entry_files(source_file)")
2345
+ need_rebuild = (not table_exists) or existing_version != SQLITE_SCHEMA_VERSION
2346
+
2347
+ existing_map: dict[str, str] = {}
2348
+ if not need_rebuild:
2349
+ rows = con.execute("select id, entry_json from entries").fetchall()
2350
+ existing_map = {str(row[0]): str(row[1]) for row in rows}
2351
+
2352
+ new_map: dict[str, str] = {}
2048
2353
  for entry in entries:
2049
- source_files_json = json.dumps(entry["scope"]["sourceFiles"], ensure_ascii=False)
2050
- keywords_json = json.dumps(entry["keywords"], ensure_ascii=False)
2051
- entry_json = json.dumps(entry, ensure_ascii=False)
2354
+ if isinstance(entry, dict) and entry.get("id"):
2355
+ new_map[str(entry["id"])] = json.dumps(entry, ensure_ascii=False)
2356
+
2357
+ to_upsert = [eid for eid in new_map if new_map[eid] != existing_map.get(eid)]
2358
+ to_delete = [eid for eid in existing_map if eid not in new_map]
2359
+ stats["sqliteUnchanged"] = len(new_map) - len(to_upsert)
2360
+
2361
+ if not need_rebuild and not to_upsert and not to_delete:
2362
+ # nothing to do; leave the file (and its mtime) untouched
2363
+ return stats
2364
+
2365
+ if need_rebuild:
2366
+ con.execute("drop table if exists entries")
2367
+ con.execute("drop table if exists entry_files")
2368
+ con.execute("drop table if exists entries_fts")
2052
2369
  con.execute(
2053
2370
  """
2054
- insert into entries (
2055
- id, project_id, type, status, title, summary, body, source_archive,
2056
- source_commit, source_files_json, keywords_json, entry_json
2057
- ) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
2058
- """,
2059
- (
2060
- entry["id"],
2061
- entry["projectId"],
2062
- entry["type"],
2063
- entry["status"],
2064
- entry["title"],
2065
- entry["summary"],
2066
- entry["body"],
2067
- entry["source"]["archive"],
2068
- entry["source"]["sourceCommit"],
2069
- source_files_json,
2070
- keywords_json,
2071
- entry_json,
2072
- ),
2371
+ create table entries (
2372
+ id text primary key,
2373
+ project_id text not null,
2374
+ type text not null,
2375
+ status text not null,
2376
+ title text not null,
2377
+ summary text not null,
2378
+ body text not null,
2379
+ source_archive text not null,
2380
+ source_commit text,
2381
+ source_files_json text not null,
2382
+ keywords_json text not null,
2383
+ entry_json text not null
2384
+ )
2385
+ """
2073
2386
  )
2074
2387
  con.execute(
2075
- "insert into entries_fts (id, title, summary, body, keywords) values (?, ?, ?, ?, ?)",
2076
- (
2077
- entry["id"],
2078
- entry["title"],
2079
- entry["summary"],
2080
- entry["body"],
2081
- " ".join(entry["keywords"]),
2082
- ),
2388
+ """
2389
+ create table entry_files (
2390
+ entry_id text not null,
2391
+ source_file text not null,
2392
+ primary key (entry_id, source_file),
2393
+ foreign key (entry_id) references entries(id)
2394
+ )
2395
+ """
2083
2396
  )
2084
- for source_file in entry["scope"]["sourceFiles"]:
2397
+ con.execute("create virtual table entries_fts using fts5(id, title, summary, body, keywords)")
2398
+ con.execute("create index idx_entries_status on entries(status)")
2399
+ con.execute("create index idx_entries_type on entries(type)")
2400
+ con.execute("create index idx_entries_source_archive on entries(source_archive)")
2401
+ con.execute("create index idx_entry_files_source_file on entry_files(source_file)")
2402
+ con.execute(f"pragma user_version = {SQLITE_SCHEMA_VERSION}")
2403
+ stats["sqliteRebuild"] = 1
2404
+ # full rebuild => every entry must be (re)inserted
2405
+ to_upsert = list(new_map.keys())
2406
+ to_delete = []
2407
+
2408
+ con.execute("begin")
2409
+ try:
2410
+ for eid in to_delete:
2411
+ con.execute("delete from entries where id=?", (eid,))
2412
+ con.execute("delete from entries_fts where id=?", (eid,))
2413
+ con.execute("delete from entry_files where entry_id=?", (eid,))
2414
+ for entry in entries:
2415
+ eid = str(entry["id"])
2416
+ if eid not in to_upsert:
2417
+ continue
2418
+ source_files_json = json.dumps(entry["scope"]["sourceFiles"], ensure_ascii=False)
2419
+ keywords_json = json.dumps(entry["keywords"], ensure_ascii=False)
2085
2420
  con.execute(
2086
- "insert or ignore into entry_files (entry_id, source_file) values (?, ?)",
2087
- (entry["id"], source_file),
2421
+ """
2422
+ insert or replace into entries (
2423
+ id, project_id, type, status, title, summary, body, source_archive,
2424
+ source_commit, source_files_json, keywords_json, entry_json
2425
+ ) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
2426
+ """,
2427
+ (
2428
+ entry["id"],
2429
+ entry["projectId"],
2430
+ entry["type"],
2431
+ entry["status"],
2432
+ entry["title"],
2433
+ entry["summary"],
2434
+ entry["body"],
2435
+ entry["source"]["archive"],
2436
+ entry["source"]["sourceCommit"],
2437
+ source_files_json,
2438
+ keywords_json,
2439
+ new_map[eid],
2440
+ ),
2441
+ )
2442
+ con.execute("delete from entries_fts where id=?", (eid,))
2443
+ con.execute(
2444
+ "insert into entries_fts (id, title, summary, body, keywords) values (?, ?, ?, ?, ?)",
2445
+ (
2446
+ entry["id"],
2447
+ entry["title"],
2448
+ entry["summary"],
2449
+ entry["body"],
2450
+ " ".join(entry["keywords"]),
2451
+ ),
2088
2452
  )
2089
- con.commit()
2453
+ con.execute("delete from entry_files where entry_id=?", (eid,))
2454
+ for source_file in entry["scope"]["sourceFiles"]:
2455
+ con.execute(
2456
+ "insert or ignore into entry_files (entry_id, source_file) values (?, ?)",
2457
+ (eid, source_file),
2458
+ )
2459
+ stats["sqliteUpsert"] = len(to_upsert)
2460
+ stats["sqliteDelete"] = len(to_delete)
2461
+ con.commit()
2462
+ except Exception:
2463
+ con.rollback()
2464
+ raise
2090
2465
  finally:
2091
2466
  con.close()
2467
+ return stats
2092
2468
 
2093
2469
 
2094
2470
  def write_views(knowledge: Path, index: dict[str, Any], entries: list[dict[str, Any]]) -> None:
@@ -2111,7 +2487,7 @@ def write_views(knowledge: Path, index: dict[str, Any], entries: list[dict[str,
2111
2487
  for entry in entries[:50]:
2112
2488
  dashboard.append(f"- **{entry['type']}** `{entry['status']}` {entry['title']}")
2113
2489
  dashboard.append(f" - source: `{entry['source']['archive']}`")
2114
- write_text(knowledge / "views" / "knowledge-dashboard.md", "\n".join(dashboard) + "\n")
2490
+ write_text_if_changed(knowledge / "views" / "knowledge-dashboard.md", "\n".join(dashboard) + "\n")
2115
2491
 
2116
2492
  by_file: dict[str, list[dict[str, Any]]] = {}
2117
2493
  for entry in entries:
@@ -2123,7 +2499,7 @@ def write_views(knowledge: Path, index: dict[str, Any], entries: list[dict[str,
2123
2499
  for entry in by_file[source_file]:
2124
2500
  lines.append(f"- **{entry['type']}** `{entry['status']}` {entry['title']}")
2125
2501
  lines.append("")
2126
- write_text(knowledge / "views" / "by-file.md", "\n".join(lines))
2502
+ write_text_if_changed(knowledge / "views" / "by-file.md", "\n".join(lines))
2127
2503
 
2128
2504
  stale = [entry for entry in entries if entry["status"] == "stale"]
2129
2505
  lines = ["# Harness Stale Knowledge", ""]
@@ -2133,7 +2509,7 @@ def write_views(knowledge: Path, index: dict[str, Any], entries: list[dict[str,
2133
2509
  lines.append(f"- **{entry['type']}** {entry['title']}")
2134
2510
  for reason in entry["lifecycle"]["staleReasons"]:
2135
2511
  lines.append(f" - {reason}")
2136
- write_text(knowledge / "views" / "stale-items.md", "\n".join(lines) + "\n")
2512
+ write_text_if_changed(knowledge / "views" / "stale-items.md", "\n".join(lines) + "\n")
2137
2513
 
2138
2514
  superseded = [entry for entry in entries if entry["status"] == "superseded"]
2139
2515
  lines = ["# Harness Superseded Knowledge", ""]
@@ -2146,7 +2522,7 @@ def write_views(knowledge: Path, index: dict[str, Any], entries: list[dict[str,
2146
2522
  lines.append(f" - supersededBy: `{superseded_by}`")
2147
2523
  for reason in entry["lifecycle"].get("staleReasons") or []:
2148
2524
  lines.append(f" - {reason}")
2149
- write_text(knowledge / "views" / "superseded-items.md", "\n".join(lines) + "\n")
2525
+ write_text_if_changed(knowledge / "views" / "superseded-items.md", "\n".join(lines) + "\n")
2150
2526
 
2151
2527
  conflicted = [entry for entry in entries if entry["status"] == "conflicted"]
2152
2528
  lines = ["# Harness Conflicted Knowledge", ""]
@@ -2158,7 +2534,7 @@ def write_views(knowledge: Path, index: dict[str, Any], entries: list[dict[str,
2158
2534
  lines.append(f" - conflictsWith: `{conflict_id}`")
2159
2535
  for reason in entry["lifecycle"].get("staleReasons") or []:
2160
2536
  lines.append(f" - {reason}")
2161
- write_text(knowledge / "views" / "conflicted-items.md", "\n".join(lines) + "\n")
2537
+ write_text_if_changed(knowledge / "views" / "conflicted-items.md", "\n".join(lines) + "\n")
2162
2538
 
2163
2539
  active_review = active_review_items(entries)
2164
2540
  lines = ["# Harness Active Review", ""]
@@ -2169,7 +2545,7 @@ def write_views(knowledge: Path, index: dict[str, Any], entries: list[dict[str,
2169
2545
  lines.append(f" - id: `{entry['id']}`")
2170
2546
  for reason in entry.get("reviewReasons") or []:
2171
2547
  lines.append(f" - {reason}")
2172
- write_text(knowledge / "views" / "active-review.md", "\n".join(lines) + "\n")
2548
+ write_text_if_changed(knowledge / "views" / "active-review.md", "\n".join(lines) + "\n")
2173
2549
 
2174
2550
  base = [
2175
2551
  'filters:',
@@ -2204,7 +2580,7 @@ def write_views(knowledge: Path, index: dict[str, Any], entries: list[dict[str,
2204
2580
  ' - file.folder',
2205
2581
  ' - file.mtime',
2206
2582
  ]
2207
- write_text(knowledge / "views" / "knowledge.base", "\n".join(base) + "\n")
2583
+ write_text_if_changed(knowledge / "views" / "knowledge.base", "\n".join(base) + "\n")
2208
2584
 
2209
2585
 
2210
2586
  def write_ingest_report(
@@ -2513,7 +2889,7 @@ def auto_knowledge(
2513
2889
  *,
2514
2890
  limit: int = 20,
2515
2891
  suggest_statuses: list[str] | None = None,
2516
- apply_suggestions: bool = False,
2892
+ apply_suggestions: bool = True,
2517
2893
  incremental: bool = True,
2518
2894
  audit_limit: int = 10,
2519
2895
  ) -> dict[str, Any]:
@@ -2539,6 +2915,7 @@ def auto_knowledge(
2539
2915
  )
2540
2916
  verification = verify_knowledge(project)
2541
2917
  audit = audit_entries(project, limit=audit_limit)
2918
+ ingest_mode = sync.get("index", {}).get("ingestMode", {}) if isinstance(sync.get("index"), dict) else {}
2542
2919
  return {
2543
2920
  "project": str(project),
2544
2921
  "generatedAt": now_iso(),
@@ -2578,6 +2955,15 @@ def auto_knowledge(
2578
2955
  "conflictReview": len(audit["conflictReview"]),
2579
2956
  "activeReview": len(audit["activeReview"]),
2580
2957
  },
2958
+ "lifecycle": {
2959
+ "validatorsApplied": suggestions["applied"],
2960
+ "candidateAutoPromoted": ingest_mode.get(
2961
+ "candidateAutoPromoted", config_summary.get("candidateAutoPromoted") or 0
2962
+ ),
2963
+ "activeAutoDemoted": ingest_mode.get("activeAutoDemoted", 0),
2964
+ "validationAutoDemoted": ingest_mode.get("validationAutoDemoted", verification["autoDemoted"]),
2965
+ "pendingAgentJudge": len(audit["conflictReview"]) + len(audit["candidateReview"]),
2966
+ },
2581
2967
  }
2582
2968
 
2583
2969
 
@@ -2599,11 +2985,12 @@ def query_index(
2599
2985
  project = project.resolve()
2600
2986
  knowledge = project / ".harness" / "knowledge"
2601
2987
  sqlite_path = knowledge / "index.sqlite"
2602
- if not sqlite_path.exists():
2603
- build_index(project)
2604
- sync = sync_status(project)
2605
- if not sync["upToDate"]:
2606
- build_index(project)
2988
+ # API-009: one ensure-current. Build the shared snapshot once (inputs_hash
2989
+ # computed exactly once) and a single build_index call whose no-op fast path
2990
+ # keeps an up-to-date project a true no-op. Replaces the old sync_status +
2991
+ # build_index double orchestration, which computed inputs_hash twice.
2992
+ snapshot = build_snapshot(project)
2993
+ build_index(project, snapshot=snapshot)
2607
2994
  entries = search_entries(sqlite_path, query, limit, file_filters, statuses, types)
2608
2995
  context_path = write_context_pack(project, knowledge, query, entries)
2609
2996
  filters = {
@@ -2698,9 +3085,14 @@ def sync_status(project: Path, update: bool = False, incremental: bool = True) -
2698
3085
  if current_by_path[path].get("summarySha256") != indexed_by_path[path].get("summarySha256"):
2699
3086
  reasons.append("archive checksum changed: " + str(path))
2700
3087
 
2701
- current_head = git_head(project)
2702
- if current_head != index.get("headCommit"):
2703
- reasons.append("head commit changed since last ingest")
3088
+ # HEAD is intentionally not an invalidation key (cluster 6, UT-027): a
3089
+ # business-code commit that does not touch any archive must not force a
3090
+ # rebuild. The archive checksum checks above already cover archive
3091
+ # changes; compare the full input fingerprint to catch config/schema
3092
+ # drift that the per-archive checks would miss.
3093
+ current_inputs_hash = compute_inputs_hash(current_records, load_config(knowledge), knowledge)
3094
+ if current_inputs_hash != index.get("inputsHash") and not reasons:
3095
+ reasons.append("knowledge inputs changed (config or schema)")
2704
3096
 
2705
3097
  action = "none"
2706
3098
  refreshed: dict[str, Any] | None = None
@@ -3902,7 +4294,11 @@ def main(argv: list[str] | None = None) -> int:
3902
4294
  default=[],
3903
4295
  help="Only suggest validators for this lifecycle status",
3904
4296
  )
3905
- auto.add_argument("--apply-suggestions", action="store_true", help="Write validator suggestions into entry JSON files")
4297
+ auto.add_argument(
4298
+ "--no-apply-suggestions",
4299
+ action="store_true",
4300
+ help="Do not write validator suggestions into entry JSON files (default applies them)",
4301
+ )
3906
4302
  auto.add_argument("--no-incremental", action="store_true", help="Refresh without reusing the archive entry cache")
3907
4303
 
3908
4304
  audit = sub.add_parser("audit", help="Generate review lists for candidate, stale, and superseded entries")
@@ -3999,7 +4395,7 @@ def main(argv: list[str] | None = None) -> int:
3999
4395
  Path(args.project),
4000
4396
  limit=args.limit,
4001
4397
  suggest_statuses=args.suggest_statuses,
4002
- apply_suggestions=args.apply_suggestions,
4398
+ apply_suggestions=not args.no_apply_suggestions,
4003
4399
  incremental=not args.no_incremental,
4004
4400
  audit_limit=args.audit_limit,
4005
4401
  )