@hunter-harness/workflow-harness 0.2.42 → 0.2.43

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 (238) hide show
  1. package/harness/bundles/general/claude-code/.harness-build.json +1 -1
  2. package/harness/bundles/general/claude-code/contracts/build-profile-v3.schema.json +56 -1
  3. package/harness/bundles/general/claude-code/harness-archive/SKILL.md +1 -1
  4. package/harness/bundles/general/claude-code/harness-archive/reference.md +1 -0
  5. package/harness/bundles/general/claude-code/harness-archive/templates/render-summary.mjs +8 -0
  6. package/harness/bundles/general/claude-code/harness-archive/templates/summary-data-template.json +28 -0
  7. package/harness/bundles/general/claude-code/harness-codebase-map/SKILL.md +1 -1
  8. package/harness/bundles/general/claude-code/harness-knowledge-ingest/SKILL.md +1 -1
  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 +1 -1
  12. package/harness/bundles/general/claude-code/harness-run/SKILL.md +1 -1
  13. package/harness/bundles/general/claude-code/harness-run/reference.md +6 -1
  14. package/harness/bundles/general/claude-code/harness-submit/SKILL.md +1 -1
  15. package/harness/bundles/general/claude-code/harness-sync/SKILL.md +1 -1
  16. package/harness/bundles/general/claude-code/harness-test/SKILL.md +8 -2
  17. package/harness/bundles/general/claude-code/protocols/execution-session-protocol.md +52 -0
  18. package/harness/bundles/general/claude-code/protocols/state-layout-protocol.md +24 -1
  19. package/harness/bundles/general/claude-code/protocols/verification-graph-protocol.md +6 -0
  20. package/harness/bundles/general/claude-code/scripts/harness_archive.py +4 -0
  21. package/harness/bundles/general/claude-code/scripts/harness_efficiency.py +290 -0
  22. package/harness/bundles/general/claude-code/scripts/harness_environment.py +719 -35
  23. package/harness/bundles/general/claude-code/scripts/harness_fixback.py +742 -0
  24. package/harness/bundles/general/claude-code/scripts/harness_profile.py +34 -0
  25. package/harness/bundles/general/claude-code/scripts/harness_runtime.py +1165 -3
  26. package/harness/bundles/general/claude-code/scripts/harness_service.py +76 -0
  27. package/harness/bundles/general/claude-code/scripts/harness_test_runner.py +127 -39
  28. package/harness/bundles/general/claude-code/scripts/harness_verification.py +408 -8
  29. package/harness/bundles/general/codebuddy/.harness-build.json +1 -1
  30. package/harness/bundles/general/codebuddy/contracts/build-profile-v3.schema.json +56 -1
  31. package/harness/bundles/general/codebuddy/harness-archive/SKILL.md +1 -1
  32. package/harness/bundles/general/codebuddy/harness-archive/reference.md +1 -0
  33. package/harness/bundles/general/codebuddy/harness-archive/templates/render-summary.mjs +8 -0
  34. package/harness/bundles/general/codebuddy/harness-archive/templates/summary-data-template.json +28 -0
  35. package/harness/bundles/general/codebuddy/harness-codebase-map/SKILL.md +1 -1
  36. package/harness/bundles/general/codebuddy/harness-knowledge-ingest/SKILL.md +1 -1
  37. package/harness/bundles/general/codebuddy/harness-knowledge-query/SKILL.md +1 -1
  38. package/harness/bundles/general/codebuddy/harness-plan/SKILL.md +1 -1
  39. package/harness/bundles/general/codebuddy/harness-review/SKILL.md +1 -1
  40. package/harness/bundles/general/codebuddy/harness-run/SKILL.md +1 -1
  41. package/harness/bundles/general/codebuddy/harness-run/reference.md +6 -1
  42. package/harness/bundles/general/codebuddy/harness-submit/SKILL.md +1 -1
  43. package/harness/bundles/general/codebuddy/harness-sync/SKILL.md +1 -1
  44. package/harness/bundles/general/codebuddy/harness-test/SKILL.md +8 -2
  45. package/harness/bundles/general/codebuddy/protocols/execution-session-protocol.md +52 -0
  46. package/harness/bundles/general/codebuddy/protocols/state-layout-protocol.md +24 -1
  47. package/harness/bundles/general/codebuddy/protocols/verification-graph-protocol.md +6 -0
  48. package/harness/bundles/general/codebuddy/scripts/harness_archive.py +4 -0
  49. package/harness/bundles/general/codebuddy/scripts/harness_efficiency.py +290 -0
  50. package/harness/bundles/general/codebuddy/scripts/harness_environment.py +719 -35
  51. package/harness/bundles/general/codebuddy/scripts/harness_fixback.py +742 -0
  52. package/harness/bundles/general/codebuddy/scripts/harness_profile.py +34 -0
  53. package/harness/bundles/general/codebuddy/scripts/harness_runtime.py +1165 -3
  54. package/harness/bundles/general/codebuddy/scripts/harness_service.py +76 -0
  55. package/harness/bundles/general/codebuddy/scripts/harness_test_runner.py +127 -39
  56. package/harness/bundles/general/codebuddy/scripts/harness_verification.py +408 -8
  57. package/harness/bundles/general/codex/.harness-build.json +1 -1
  58. package/harness/bundles/general/codex/contracts/build-profile-v3.schema.json +56 -1
  59. package/harness/bundles/general/codex/harness-archive/SKILL.md +1 -1
  60. package/harness/bundles/general/codex/harness-archive/reference.md +1 -0
  61. package/harness/bundles/general/codex/harness-archive/templates/render-summary.mjs +8 -0
  62. package/harness/bundles/general/codex/harness-archive/templates/summary-data-template.json +28 -0
  63. package/harness/bundles/general/codex/harness-codebase-map/SKILL.md +1 -1
  64. package/harness/bundles/general/codex/harness-knowledge-ingest/SKILL.md +1 -1
  65. package/harness/bundles/general/codex/harness-knowledge-query/SKILL.md +1 -1
  66. package/harness/bundles/general/codex/harness-plan/SKILL.md +1 -1
  67. package/harness/bundles/general/codex/harness-review/SKILL.md +1 -1
  68. package/harness/bundles/general/codex/harness-run/SKILL.md +1 -1
  69. package/harness/bundles/general/codex/harness-run/reference.md +6 -1
  70. package/harness/bundles/general/codex/harness-submit/SKILL.md +1 -1
  71. package/harness/bundles/general/codex/harness-sync/SKILL.md +1 -1
  72. package/harness/bundles/general/codex/harness-test/SKILL.md +8 -2
  73. package/harness/bundles/general/codex/protocols/execution-session-protocol.md +52 -0
  74. package/harness/bundles/general/codex/protocols/state-layout-protocol.md +24 -1
  75. package/harness/bundles/general/codex/protocols/verification-graph-protocol.md +6 -0
  76. package/harness/bundles/general/codex/scripts/harness_archive.py +4 -0
  77. package/harness/bundles/general/codex/scripts/harness_efficiency.py +290 -0
  78. package/harness/bundles/general/codex/scripts/harness_environment.py +719 -35
  79. package/harness/bundles/general/codex/scripts/harness_fixback.py +742 -0
  80. package/harness/bundles/general/codex/scripts/harness_profile.py +34 -0
  81. package/harness/bundles/general/codex/scripts/harness_runtime.py +1165 -3
  82. package/harness/bundles/general/codex/scripts/harness_service.py +76 -0
  83. package/harness/bundles/general/codex/scripts/harness_test_runner.py +127 -39
  84. package/harness/bundles/general/codex/scripts/harness_verification.py +408 -8
  85. package/harness/bundles/general/cursor/.harness-build.json +1 -1
  86. package/harness/bundles/general/cursor/contracts/build-profile-v3.schema.json +56 -1
  87. package/harness/bundles/general/cursor/harness-archive/SKILL.md +1 -1
  88. package/harness/bundles/general/cursor/harness-archive/reference.md +1 -0
  89. package/harness/bundles/general/cursor/harness-archive/templates/render-summary.mjs +8 -0
  90. package/harness/bundles/general/cursor/harness-archive/templates/summary-data-template.json +28 -0
  91. package/harness/bundles/general/cursor/harness-codebase-map/SKILL.md +1 -1
  92. package/harness/bundles/general/cursor/harness-knowledge-ingest/SKILL.md +1 -1
  93. package/harness/bundles/general/cursor/harness-knowledge-query/SKILL.md +1 -1
  94. package/harness/bundles/general/cursor/harness-plan/SKILL.md +1 -1
  95. package/harness/bundles/general/cursor/harness-review/SKILL.md +1 -1
  96. package/harness/bundles/general/cursor/harness-run/SKILL.md +1 -1
  97. package/harness/bundles/general/cursor/harness-run/reference.md +6 -1
  98. package/harness/bundles/general/cursor/harness-submit/SKILL.md +1 -1
  99. package/harness/bundles/general/cursor/harness-sync/SKILL.md +1 -1
  100. package/harness/bundles/general/cursor/harness-test/SKILL.md +8 -2
  101. package/harness/bundles/general/cursor/protocols/execution-session-protocol.md +52 -0
  102. package/harness/bundles/general/cursor/protocols/state-layout-protocol.md +24 -1
  103. package/harness/bundles/general/cursor/protocols/verification-graph-protocol.md +6 -0
  104. package/harness/bundles/general/cursor/scripts/harness_archive.py +4 -0
  105. package/harness/bundles/general/cursor/scripts/harness_efficiency.py +290 -0
  106. package/harness/bundles/general/cursor/scripts/harness_environment.py +719 -35
  107. package/harness/bundles/general/cursor/scripts/harness_fixback.py +742 -0
  108. package/harness/bundles/general/cursor/scripts/harness_profile.py +34 -0
  109. package/harness/bundles/general/cursor/scripts/harness_runtime.py +1165 -3
  110. package/harness/bundles/general/cursor/scripts/harness_service.py +76 -0
  111. package/harness/bundles/general/cursor/scripts/harness_test_runner.py +127 -39
  112. package/harness/bundles/general/cursor/scripts/harness_verification.py +408 -8
  113. package/harness/bundles/java/claude-code/.harness-build.json +1 -1
  114. package/harness/bundles/java/claude-code/contracts/build-profile-v3.schema.json +56 -1
  115. package/harness/bundles/java/claude-code/harness-apidoc/SKILL.md +1 -1
  116. package/harness/bundles/java/claude-code/harness-archive/SKILL.md +1 -1
  117. package/harness/bundles/java/claude-code/harness-archive/reference.md +1 -0
  118. package/harness/bundles/java/claude-code/harness-archive/templates/render-summary.mjs +8 -0
  119. package/harness/bundles/java/claude-code/harness-archive/templates/summary-data-template.json +28 -0
  120. package/harness/bundles/java/claude-code/harness-codebase-map/SKILL.md +1 -1
  121. package/harness/bundles/java/claude-code/harness-knowledge-ingest/SKILL.md +1 -1
  122. package/harness/bundles/java/claude-code/harness-knowledge-query/SKILL.md +1 -1
  123. package/harness/bundles/java/claude-code/harness-package/SKILL.md +1 -1
  124. package/harness/bundles/java/claude-code/harness-plan/SKILL.md +1 -1
  125. package/harness/bundles/java/claude-code/harness-review/SKILL.md +1 -1
  126. package/harness/bundles/java/claude-code/harness-run/SKILL.md +1 -1
  127. package/harness/bundles/java/claude-code/harness-submit/SKILL.md +1 -1
  128. package/harness/bundles/java/claude-code/harness-sync/SKILL.md +1 -1
  129. package/harness/bundles/java/claude-code/harness-test/SKILL.md +8 -2
  130. package/harness/bundles/java/claude-code/protocols/execution-session-protocol.md +52 -0
  131. package/harness/bundles/java/claude-code/protocols/state-layout-protocol.md +24 -1
  132. package/harness/bundles/java/claude-code/protocols/verification-graph-protocol.md +6 -0
  133. package/harness/bundles/java/claude-code/scripts/harness_archive.py +4 -0
  134. package/harness/bundles/java/claude-code/scripts/harness_efficiency.py +290 -0
  135. package/harness/bundles/java/claude-code/scripts/harness_environment.py +719 -35
  136. package/harness/bundles/java/claude-code/scripts/harness_fixback.py +742 -0
  137. package/harness/bundles/java/claude-code/scripts/harness_profile.py +34 -0
  138. package/harness/bundles/java/claude-code/scripts/harness_runtime.py +1165 -3
  139. package/harness/bundles/java/claude-code/scripts/harness_service.py +76 -0
  140. package/harness/bundles/java/claude-code/scripts/harness_test_runner.py +127 -39
  141. package/harness/bundles/java/claude-code/scripts/harness_verification.py +408 -8
  142. package/harness/bundles/java/codebuddy/.harness-build.json +1 -1
  143. package/harness/bundles/java/codebuddy/contracts/build-profile-v3.schema.json +56 -1
  144. package/harness/bundles/java/codebuddy/harness-apidoc/SKILL.md +1 -1
  145. package/harness/bundles/java/codebuddy/harness-archive/SKILL.md +1 -1
  146. package/harness/bundles/java/codebuddy/harness-archive/reference.md +1 -0
  147. package/harness/bundles/java/codebuddy/harness-archive/templates/render-summary.mjs +8 -0
  148. package/harness/bundles/java/codebuddy/harness-archive/templates/summary-data-template.json +28 -0
  149. package/harness/bundles/java/codebuddy/harness-codebase-map/SKILL.md +1 -1
  150. package/harness/bundles/java/codebuddy/harness-knowledge-ingest/SKILL.md +1 -1
  151. package/harness/bundles/java/codebuddy/harness-knowledge-query/SKILL.md +1 -1
  152. package/harness/bundles/java/codebuddy/harness-package/SKILL.md +1 -1
  153. package/harness/bundles/java/codebuddy/harness-plan/SKILL.md +1 -1
  154. package/harness/bundles/java/codebuddy/harness-review/SKILL.md +1 -1
  155. package/harness/bundles/java/codebuddy/harness-run/SKILL.md +1 -1
  156. package/harness/bundles/java/codebuddy/harness-submit/SKILL.md +1 -1
  157. package/harness/bundles/java/codebuddy/harness-sync/SKILL.md +1 -1
  158. package/harness/bundles/java/codebuddy/harness-test/SKILL.md +8 -2
  159. package/harness/bundles/java/codebuddy/protocols/execution-session-protocol.md +52 -0
  160. package/harness/bundles/java/codebuddy/protocols/state-layout-protocol.md +24 -1
  161. package/harness/bundles/java/codebuddy/protocols/verification-graph-protocol.md +6 -0
  162. package/harness/bundles/java/codebuddy/scripts/harness_archive.py +4 -0
  163. package/harness/bundles/java/codebuddy/scripts/harness_efficiency.py +290 -0
  164. package/harness/bundles/java/codebuddy/scripts/harness_environment.py +719 -35
  165. package/harness/bundles/java/codebuddy/scripts/harness_fixback.py +742 -0
  166. package/harness/bundles/java/codebuddy/scripts/harness_profile.py +34 -0
  167. package/harness/bundles/java/codebuddy/scripts/harness_runtime.py +1165 -3
  168. package/harness/bundles/java/codebuddy/scripts/harness_service.py +76 -0
  169. package/harness/bundles/java/codebuddy/scripts/harness_test_runner.py +127 -39
  170. package/harness/bundles/java/codebuddy/scripts/harness_verification.py +408 -8
  171. package/harness/bundles/java/codex/.harness-build.json +1 -1
  172. package/harness/bundles/java/codex/contracts/build-profile-v3.schema.json +56 -1
  173. package/harness/bundles/java/codex/harness-apidoc/SKILL.md +1 -1
  174. package/harness/bundles/java/codex/harness-archive/SKILL.md +1 -1
  175. package/harness/bundles/java/codex/harness-archive/reference.md +1 -0
  176. package/harness/bundles/java/codex/harness-archive/templates/render-summary.mjs +8 -0
  177. package/harness/bundles/java/codex/harness-archive/templates/summary-data-template.json +28 -0
  178. package/harness/bundles/java/codex/harness-codebase-map/SKILL.md +1 -1
  179. package/harness/bundles/java/codex/harness-knowledge-ingest/SKILL.md +1 -1
  180. package/harness/bundles/java/codex/harness-knowledge-query/SKILL.md +1 -1
  181. package/harness/bundles/java/codex/harness-package/SKILL.md +1 -1
  182. package/harness/bundles/java/codex/harness-plan/SKILL.md +1 -1
  183. package/harness/bundles/java/codex/harness-review/SKILL.md +1 -1
  184. package/harness/bundles/java/codex/harness-run/SKILL.md +1 -1
  185. package/harness/bundles/java/codex/harness-submit/SKILL.md +1 -1
  186. package/harness/bundles/java/codex/harness-sync/SKILL.md +1 -1
  187. package/harness/bundles/java/codex/harness-test/SKILL.md +8 -2
  188. package/harness/bundles/java/codex/protocols/execution-session-protocol.md +52 -0
  189. package/harness/bundles/java/codex/protocols/state-layout-protocol.md +24 -1
  190. package/harness/bundles/java/codex/protocols/verification-graph-protocol.md +6 -0
  191. package/harness/bundles/java/codex/scripts/harness_archive.py +4 -0
  192. package/harness/bundles/java/codex/scripts/harness_efficiency.py +290 -0
  193. package/harness/bundles/java/codex/scripts/harness_environment.py +719 -35
  194. package/harness/bundles/java/codex/scripts/harness_fixback.py +742 -0
  195. package/harness/bundles/java/codex/scripts/harness_profile.py +34 -0
  196. package/harness/bundles/java/codex/scripts/harness_runtime.py +1165 -3
  197. package/harness/bundles/java/codex/scripts/harness_service.py +76 -0
  198. package/harness/bundles/java/codex/scripts/harness_test_runner.py +127 -39
  199. package/harness/bundles/java/codex/scripts/harness_verification.py +408 -8
  200. package/harness/bundles/java/cursor/.harness-build.json +1 -1
  201. package/harness/bundles/java/cursor/contracts/build-profile-v3.schema.json +56 -1
  202. package/harness/bundles/java/cursor/harness-apidoc/SKILL.md +1 -1
  203. package/harness/bundles/java/cursor/harness-archive/SKILL.md +1 -1
  204. package/harness/bundles/java/cursor/harness-archive/reference.md +1 -0
  205. package/harness/bundles/java/cursor/harness-archive/templates/render-summary.mjs +8 -0
  206. package/harness/bundles/java/cursor/harness-archive/templates/summary-data-template.json +28 -0
  207. package/harness/bundles/java/cursor/harness-codebase-map/SKILL.md +1 -1
  208. package/harness/bundles/java/cursor/harness-knowledge-ingest/SKILL.md +1 -1
  209. package/harness/bundles/java/cursor/harness-knowledge-query/SKILL.md +1 -1
  210. package/harness/bundles/java/cursor/harness-package/SKILL.md +1 -1
  211. package/harness/bundles/java/cursor/harness-plan/SKILL.md +1 -1
  212. package/harness/bundles/java/cursor/harness-review/SKILL.md +1 -1
  213. package/harness/bundles/java/cursor/harness-run/SKILL.md +1 -1
  214. package/harness/bundles/java/cursor/harness-submit/SKILL.md +1 -1
  215. package/harness/bundles/java/cursor/harness-sync/SKILL.md +1 -1
  216. package/harness/bundles/java/cursor/harness-test/SKILL.md +8 -2
  217. package/harness/bundles/java/cursor/protocols/execution-session-protocol.md +52 -0
  218. package/harness/bundles/java/cursor/protocols/state-layout-protocol.md +24 -1
  219. package/harness/bundles/java/cursor/protocols/verification-graph-protocol.md +6 -0
  220. package/harness/bundles/java/cursor/scripts/harness_archive.py +4 -0
  221. package/harness/bundles/java/cursor/scripts/harness_efficiency.py +290 -0
  222. package/harness/bundles/java/cursor/scripts/harness_environment.py +719 -35
  223. package/harness/bundles/java/cursor/scripts/harness_fixback.py +742 -0
  224. package/harness/bundles/java/cursor/scripts/harness_profile.py +34 -0
  225. package/harness/bundles/java/cursor/scripts/harness_runtime.py +1165 -3
  226. package/harness/bundles/java/cursor/scripts/harness_service.py +76 -0
  227. package/harness/bundles/java/cursor/scripts/harness_test_runner.py +127 -39
  228. package/harness/bundles/java/cursor/scripts/harness_verification.py +408 -8
  229. package/harness/manifests/general/claude-code.json +40 -27
  230. package/harness/manifests/general/codebuddy.json +40 -27
  231. package/harness/manifests/general/codex.json +40 -27
  232. package/harness/manifests/general/cursor.json +40 -27
  233. package/harness/manifests/java/claude-code.json +41 -28
  234. package/harness/manifests/java/codebuddy.json +41 -28
  235. package/harness/manifests/java/codex.json +41 -28
  236. package/harness/manifests/java/cursor.json +41 -28
  237. package/hunter-workflow-family.json +6 -4
  238. package/package.json +1 -1
@@ -1855,6 +1855,74 @@ def cmd_stop(args: argparse.Namespace) -> int:
1855
1855
  return 0 if result.get("ok") else 1
1856
1856
 
1857
1857
 
1858
+ def retire_stale_session(change_dir: Path) -> dict[str, Any]:
1859
+ """Retire only Harness state whose recorded process identity is gone.
1860
+
1861
+ This operation never terminates a process or releases a live listener. The
1862
+ original receipt is moved into an immutable evidence directory so a new
1863
+ superseding session may be created safely.
1864
+ """
1865
+ change_dir = resolve_path(change_dir)
1866
+ try:
1867
+ session = load_session(change_dir)
1868
+ except SessionCorrupt as exc:
1869
+ return {
1870
+ "ok": False,
1871
+ "code": "SERVICE_SESSION_CORRUPT",
1872
+ "action": "retire-refused",
1873
+ "error": str(exc),
1874
+ "unknownProcessUntouched": True,
1875
+ }
1876
+ if session is None:
1877
+ return {
1878
+ "ok": True,
1879
+ "code": "SERVICE_SESSION_ABSENT",
1880
+ "action": "already-retired",
1881
+ "unknownProcessUntouched": True,
1882
+ }
1883
+ identity = verify_process_identity(session)
1884
+ if identity is True:
1885
+ return {
1886
+ "ok": False,
1887
+ "code": "SERVICE_PROCESS_STILL_OWNED",
1888
+ "action": "retire-refused",
1889
+ "unknownProcessUntouched": True,
1890
+ }
1891
+ if identity is None:
1892
+ return {
1893
+ "ok": False,
1894
+ "code": "SERVICE_PROCESS_IDENTITY_UNVERIFIED",
1895
+ "action": "retire-refused",
1896
+ "unknownProcessUntouched": True,
1897
+ }
1898
+ source = session_path(change_dir)
1899
+ raw_id = str(session.get("sessionId") or session.get("session_id") or "")
1900
+ safe_id = re.sub(r"[^A-Za-z0-9._-]+", "-", raw_id).strip("-")
1901
+ if not safe_id:
1902
+ safe_id = sha256_text(json.dumps(session, sort_keys=True))[7:23]
1903
+ retired_root = change_dir / "runtime" / "retired-service-sessions"
1904
+ retired_root.mkdir(parents=True, exist_ok=True)
1905
+ destination = retired_root / f"{safe_id}-{int(time.time() * 1000)}.json"
1906
+ os.replace(source, destination)
1907
+ receipt = {
1908
+ "schemaVersion": 1,
1909
+ "action": "retired-stale",
1910
+ "code": "STALE_IDENTITY_RETIRED",
1911
+ "retiredAt": now_iso(),
1912
+ "retiredEvidence": str(destination),
1913
+ "unknownProcessUntouched": True,
1914
+ "sessionId": raw_id or None,
1915
+ }
1916
+ write_json(retired_root / f"{destination.stem}.receipt.json", receipt)
1917
+ return {"ok": True, **receipt}
1918
+
1919
+
1920
+ def cmd_retire_stale(args: argparse.Namespace) -> int:
1921
+ result = retire_stale_session(resolve_path(args.change_dir))
1922
+ emit_json(result, as_json=bool(args.json))
1923
+ return 0 if result.get("ok") else 1
1924
+
1925
+
1858
1926
  def build_parser() -> argparse.ArgumentParser:
1859
1927
  parser = argparse.ArgumentParser(
1860
1928
  prog="harness_service.py",
@@ -1934,6 +2002,14 @@ def build_parser() -> argparse.ArgumentParser:
1934
2002
  p_stop.add_argument("--json", action="store_true")
1935
2003
  p_stop.set_defaults(func=cmd_stop)
1936
2004
 
2005
+ p_retire = sub.add_parser(
2006
+ "retire-stale",
2007
+ help="retire stale Harness state without terminating unknown processes",
2008
+ )
2009
+ p_retire.add_argument("--change-dir", required=True)
2010
+ p_retire.add_argument("--json", action="store_true")
2011
+ p_retire.set_defaults(func=cmd_retire_stale)
2012
+
1937
2013
  return parser
1938
2014
 
1939
2015
 
@@ -37,7 +37,10 @@ DEFAULT_RESOURCE_MODULES = (
37
37
  "test_harness_integration.py",
38
38
  "test_harness_service.py",
39
39
  )
40
- DEFAULT_DETACHED_SERVICE_MODULES = ("test_harness_service.py",)
40
+ DEFAULT_DETACHED_SERVICE_MODULES = (
41
+ "test_harness_service.py",
42
+ "test_harness_runtime.py",
43
+ )
41
44
  TRUTHY_VALUES = frozenset({"1", "true", "yes", "on"})
42
45
 
43
46
 
@@ -189,16 +192,17 @@ def classify_test_lock(path: Path) -> dict[str, object]:
189
192
  "path": str(path),
190
193
  "token": payload.get("token"),
191
194
  }
192
- if time.time() < float(expires):
193
- return {
194
- "classification": "ACTIVE",
195
- "path": str(path),
196
- "token": payload.get("token"),
197
- "owner": owner,
198
- }
195
+ unexpired = time.time() < float(expires)
199
196
  if not isinstance(owner, dict) or any(
200
197
  not owner.get(field) for field in ("pid", "executable", "startedAt")
201
198
  ):
199
+ if unexpired:
200
+ return {
201
+ "classification": "ACTIVE",
202
+ "path": str(path),
203
+ "token": payload.get("token"),
204
+ "owner": owner,
205
+ }
202
206
  return {
203
207
  "classification": "OWNER_IDENTITY_INCOMPLETE",
204
208
  "path": str(path),
@@ -207,6 +211,13 @@ def classify_test_lock(path: Path) -> dict[str, object]:
207
211
  try:
208
212
  owner_pid = int(owner["pid"])
209
213
  except (TypeError, ValueError):
214
+ if unexpired:
215
+ return {
216
+ "classification": "ACTIVE",
217
+ "path": str(path),
218
+ "token": payload.get("token"),
219
+ "owner": owner,
220
+ }
210
221
  return {
211
222
  "classification": "OWNER_IDENTITY_INCOMPLETE",
212
223
  "path": str(path),
@@ -219,6 +230,13 @@ def classify_test_lock(path: Path) -> dict[str, object]:
219
230
  "token": payload.get("token"),
220
231
  "owner": owner,
221
232
  }
233
+ if unexpired:
234
+ return {
235
+ "classification": "ACTIVE",
236
+ "path": str(path),
237
+ "token": payload.get("token"),
238
+ "owner": owner,
239
+ }
222
240
  verified = henv.hservice.verify_process_identity(
223
241
  {
224
242
  "pid": owner_pid,
@@ -244,7 +262,7 @@ def classify_test_lock(path: Path) -> dict[str, object]:
244
262
 
245
263
 
246
264
  def reap_test_lock(path: Path) -> dict[str, object]:
247
- """Reap only an expired lock whose exact recorded owner is absent."""
265
+ """Reap only a lock whose exact recorded owner is definitely absent."""
248
266
  classification = classify_test_lock(path)
249
267
  reaped = classification.get("classification") == "RECLAIMABLE"
250
268
  if reaped:
@@ -484,6 +502,20 @@ class _WindowsBasicAccountingInformation(ctypes.Structure):
484
502
  ]
485
503
 
486
504
 
505
+ _WINDOWS_JOB_PROCESS_ID_CAPACITY = 4096
506
+
507
+
508
+ class _WindowsJobProcessIdList(ctypes.Structure):
509
+ _fields_ = [
510
+ ("NumberOfAssignedProcesses", ctypes.c_uint32),
511
+ ("NumberOfProcessIdsInList", ctypes.c_uint32),
512
+ (
513
+ "ProcessIdList",
514
+ ctypes.c_size_t * _WINDOWS_JOB_PROCESS_ID_CAPACITY,
515
+ ),
516
+ ]
517
+
518
+
487
519
  class _WindowsKillOnCloseJob:
488
520
  """Best-effort Windows Job Object that kills descendants when closed."""
489
521
 
@@ -491,9 +523,11 @@ class _WindowsKillOnCloseJob:
491
523
  _JOB_OBJECT_LIMIT_BREAKAWAY_OK = 0x00000800
492
524
  _JOB_OBJECT_EXTENDED_LIMIT_INFORMATION_CLASS = 9
493
525
  _JOB_OBJECT_BASIC_ACCOUNTING_INFORMATION_CLASS = 1
526
+ _JOB_OBJECT_BASIC_PROCESS_ID_LIST_CLASS = 3
494
527
 
495
- def __init__(self) -> None:
528
+ def __init__(self, *, allow_breakaway: bool = False) -> None:
496
529
  self.handle: int | None = None
530
+ self.allow_breakaway = allow_breakaway
497
531
 
498
532
  def assign(self, process: subprocess.Popen[object]) -> bool:
499
533
  if os.name != "nt":
@@ -506,7 +540,11 @@ class _WindowsKillOnCloseJob:
506
540
  info = _WindowsExtendedLimitInformation()
507
541
  info.BasicLimitInformation.LimitFlags = (
508
542
  self._JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE
509
- | self._JOB_OBJECT_LIMIT_BREAKAWAY_OK
543
+ | (
544
+ self._JOB_OBJECT_LIMIT_BREAKAWAY_OK
545
+ if self.allow_breakaway
546
+ else 0
547
+ )
510
548
  )
511
549
  configured = kernel32.SetInformationJobObject(
512
550
  ctypes.c_void_p(handle),
@@ -535,26 +573,73 @@ class _WindowsKillOnCloseJob:
535
573
  ctypes.windll.kernel32.CloseHandle(ctypes.c_void_p(self.handle))
536
574
  self.handle = None
537
575
 
538
- def terminate_and_wait(self, timeout_seconds: float = 5.0) -> None:
576
+ def terminate_and_wait(self, timeout_seconds: float = 5.0) -> bool:
539
577
  if self.handle is None:
540
- return
578
+ return True
541
579
  kernel32 = ctypes.windll.kernel32
542
580
  handle = ctypes.c_void_p(self.handle)
543
- kernel32.TerminateJobObject(handle, 1)
544
581
  deadline = time.monotonic() + max(0.0, timeout_seconds)
545
- while time.monotonic() < deadline:
546
- accounting = _WindowsBasicAccountingInformation()
547
- queried = kernel32.QueryInformationJobObject(
548
- handle,
549
- self._JOB_OBJECT_BASIC_ACCOUNTING_INFORMATION_CLASS,
550
- ctypes.byref(accounting),
551
- ctypes.sizeof(accounting),
552
- None,
553
- )
554
- if not queried or accounting.ActiveProcesses == 0:
555
- break
556
- time.sleep(0.02)
557
- self.close()
582
+ process_ids = _WindowsJobProcessIdList()
583
+ process_list_queried = kernel32.QueryInformationJobObject(
584
+ handle,
585
+ self._JOB_OBJECT_BASIC_PROCESS_ID_LIST_CLASS,
586
+ ctypes.byref(process_ids),
587
+ ctypes.sizeof(process_ids),
588
+ None,
589
+ )
590
+ process_list_complete = bool(
591
+ process_list_queried
592
+ and process_ids.NumberOfAssignedProcesses
593
+ == process_ids.NumberOfProcessIdsInList
594
+ )
595
+ kernel32.OpenProcess.restype = ctypes.c_void_p
596
+ process_handles: list[int] = []
597
+ if process_list_complete:
598
+ for index in range(int(process_ids.NumberOfProcessIdsInList)):
599
+ process_handle = kernel32.OpenProcess(
600
+ 0x00100000,
601
+ False,
602
+ int(process_ids.ProcessIdList[index]),
603
+ )
604
+ if process_handle:
605
+ process_handles.append(int(process_handle))
606
+ else:
607
+ process_list_complete = False
608
+ terminated = bool(kernel32.TerminateJobObject(handle, 1))
609
+ handles_signaled = process_list_complete
610
+ try:
611
+ for process_handle in process_handles:
612
+ remaining_ms = max(
613
+ 0,
614
+ int((deadline - time.monotonic()) * 1000),
615
+ )
616
+ if (
617
+ kernel32.WaitForSingleObject(
618
+ ctypes.c_void_p(process_handle),
619
+ remaining_ms,
620
+ )
621
+ != 0
622
+ ):
623
+ handles_signaled = False
624
+ accounting_drained = False
625
+ while time.monotonic() < deadline:
626
+ accounting = _WindowsBasicAccountingInformation()
627
+ queried = kernel32.QueryInformationJobObject(
628
+ handle,
629
+ self._JOB_OBJECT_BASIC_ACCOUNTING_INFORMATION_CLASS,
630
+ ctypes.byref(accounting),
631
+ ctypes.sizeof(accounting),
632
+ None,
633
+ )
634
+ if queried and accounting.ActiveProcesses == 0:
635
+ accounting_drained = True
636
+ break
637
+ time.sleep(0.02)
638
+ finally:
639
+ for process_handle in process_handles:
640
+ kernel32.CloseHandle(ctypes.c_void_p(process_handle))
641
+ self.close()
642
+ return terminated and handles_signaled and accounting_drained
558
643
 
559
644
 
560
645
  class _WindowsProcessEntry(ctypes.Structure):
@@ -744,15 +829,15 @@ def _cleanup_descendants_after_success(
744
829
  process: subprocess.Popen[object],
745
830
  *,
746
831
  windows_job: _WindowsKillOnCloseJob | None,
747
- ) -> None:
832
+ ) -> bool:
748
833
  if windows_job is not None and windows_job.handle is not None:
749
- windows_job.terminate_and_wait()
750
- return
834
+ return windows_job.terminate_and_wait()
751
835
  if os.name != "nt":
752
836
  try:
753
837
  os.killpg(process.pid, signal.SIGTERM)
754
838
  except ProcessLookupError:
755
839
  pass
840
+ return os.name != "nt"
756
841
 
757
842
 
758
843
  def run_managed_command(
@@ -796,8 +881,8 @@ def run_managed_command(
796
881
  try:
797
882
  process = subprocess.Popen(list(argv), **popen_kwargs)
798
883
  windows_job = (
799
- _WindowsKillOnCloseJob()
800
- if os.name == "nt" and not allow_detached_processes
884
+ _WindowsKillOnCloseJob(allow_breakaway=allow_detached_processes)
885
+ if os.name == "nt"
801
886
  else None
802
887
  )
803
888
  isolated = bool(windows_job and windows_job.assign(process))
@@ -832,16 +917,16 @@ def run_managed_command(
832
917
  if os.name == "nt":
833
918
  _terminate_tracked_windows_descendants(known_windows_descendants)
834
919
  raise
920
+ descendants_drained = True
835
921
  if not timed_out:
836
- _cleanup_descendants_after_success(process, windows_job=windows_job)
922
+ descendants_drained = _cleanup_descendants_after_success(
923
+ process,
924
+ windows_job=windows_job,
925
+ )
837
926
  if os.name == "nt":
838
927
  _update_windows_descendants(process.pid, known_windows_descendants)
839
928
  _terminate_tracked_windows_descendants(known_windows_descendants)
840
- isolated = (
841
- windows_snapshot_ok
842
- if allow_detached_processes
843
- else isolated and windows_snapshot_ok
844
- )
929
+ isolated = isolated and windows_snapshot_ok and descendants_drained
845
930
  duration = time.monotonic() - started
846
931
  output_tail = ""
847
932
  if output_stream is not None:
@@ -1320,7 +1405,10 @@ def build_parser() -> argparse.ArgumentParser:
1320
1405
  exec_parser.add_argument(
1321
1406
  "--allow-detached-processes",
1322
1407
  action="store_true",
1323
- help="Use exact Windows PID-lineage tracking instead of a Job Object.",
1408
+ help=(
1409
+ "Permit explicit Windows Job breakaway while retaining ordinary "
1410
+ "descendants and tracking escaped process lineages."
1411
+ ),
1324
1412
  )
1325
1413
  exec_parser.set_defaults(handler=_run_exec)
1326
1414