@hunter-harness/workflow-harness 0.2.43 → 0.2.45

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 (274) 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 +100 -0
  3. package/harness/bundles/general/claude-code/contracts/fixtures/managed-execution.json +341 -0
  4. package/harness/bundles/general/claude-code/contracts/process-identity.schema.json +132 -0
  5. package/harness/bundles/general/claude-code/contracts/run-session.schema.json +142 -0
  6. package/harness/bundles/general/claude-code/contracts/service-retirement-receipt.schema.json +85 -0
  7. package/harness/bundles/general/claude-code/contracts/service-session.schema.json +147 -0
  8. package/harness/bundles/general/claude-code/harness-archive/SKILL.md +1 -1
  9. package/harness/bundles/general/claude-code/harness-codebase-map/SKILL.md +1 -1
  10. package/harness/bundles/general/claude-code/harness-knowledge-ingest/SKILL.md +1 -1
  11. package/harness/bundles/general/claude-code/harness-knowledge-query/SKILL.md +1 -1
  12. package/harness/bundles/general/claude-code/harness-plan/SKILL.md +1 -1
  13. package/harness/bundles/general/claude-code/harness-review/SKILL.md +1 -1
  14. package/harness/bundles/general/claude-code/harness-run/SKILL.md +1 -1
  15. package/harness/bundles/general/claude-code/harness-submit/SKILL.md +1 -1
  16. package/harness/bundles/general/claude-code/harness-submit/checklist.md +12 -0
  17. package/harness/bundles/general/claude-code/harness-sync/SKILL.md +1 -1
  18. package/harness/bundles/general/claude-code/harness-test/SKILL.md +1 -1
  19. package/harness/bundles/general/claude-code/scripts/harness_archive.py +324 -28
  20. package/harness/bundles/general/claude-code/scripts/harness_change.py +137 -2
  21. package/harness/bundles/general/claude-code/scripts/harness_efficiency.py +153 -0
  22. package/harness/bundles/general/claude-code/scripts/harness_environment.py +55 -25
  23. package/harness/bundles/general/claude-code/scripts/harness_execution_contracts.py +596 -0
  24. package/harness/bundles/general/claude-code/scripts/harness_integration.py +289 -52
  25. package/harness/bundles/general/claude-code/scripts/harness_ledger.py +5 -0
  26. package/harness/bundles/general/claude-code/scripts/harness_orchestration.py +344 -0
  27. package/harness/bundles/general/claude-code/scripts/harness_process.py +1065 -0
  28. package/harness/bundles/general/claude-code/scripts/harness_profile.py +244 -4
  29. package/harness/bundles/general/claude-code/scripts/harness_runtime.py +646 -132
  30. package/harness/bundles/general/claude-code/scripts/harness_service.py +1469 -194
  31. package/harness/bundles/general/claude-code/scripts/harness_test_runner.py +107 -117
  32. package/harness/bundles/general/claude-code/scripts/harness_verification.py +79 -0
  33. package/harness/bundles/general/codebuddy/.harness-build.json +1 -1
  34. package/harness/bundles/general/codebuddy/contracts/build-profile-v3.schema.json +100 -0
  35. package/harness/bundles/general/codebuddy/contracts/fixtures/managed-execution.json +341 -0
  36. package/harness/bundles/general/codebuddy/contracts/process-identity.schema.json +132 -0
  37. package/harness/bundles/general/codebuddy/contracts/run-session.schema.json +142 -0
  38. package/harness/bundles/general/codebuddy/contracts/service-retirement-receipt.schema.json +85 -0
  39. package/harness/bundles/general/codebuddy/contracts/service-session.schema.json +147 -0
  40. package/harness/bundles/general/codebuddy/harness-archive/SKILL.md +1 -1
  41. package/harness/bundles/general/codebuddy/harness-codebase-map/SKILL.md +1 -1
  42. package/harness/bundles/general/codebuddy/harness-knowledge-ingest/SKILL.md +1 -1
  43. package/harness/bundles/general/codebuddy/harness-knowledge-query/SKILL.md +1 -1
  44. package/harness/bundles/general/codebuddy/harness-plan/SKILL.md +1 -1
  45. package/harness/bundles/general/codebuddy/harness-review/SKILL.md +1 -1
  46. package/harness/bundles/general/codebuddy/harness-run/SKILL.md +1 -1
  47. package/harness/bundles/general/codebuddy/harness-submit/SKILL.md +1 -1
  48. package/harness/bundles/general/codebuddy/harness-submit/checklist.md +12 -0
  49. package/harness/bundles/general/codebuddy/harness-sync/SKILL.md +1 -1
  50. package/harness/bundles/general/codebuddy/harness-test/SKILL.md +1 -1
  51. package/harness/bundles/general/codebuddy/scripts/harness_archive.py +324 -28
  52. package/harness/bundles/general/codebuddy/scripts/harness_change.py +137 -2
  53. package/harness/bundles/general/codebuddy/scripts/harness_efficiency.py +153 -0
  54. package/harness/bundles/general/codebuddy/scripts/harness_environment.py +55 -25
  55. package/harness/bundles/general/codebuddy/scripts/harness_execution_contracts.py +596 -0
  56. package/harness/bundles/general/codebuddy/scripts/harness_integration.py +289 -52
  57. package/harness/bundles/general/codebuddy/scripts/harness_ledger.py +5 -0
  58. package/harness/bundles/general/codebuddy/scripts/harness_orchestration.py +344 -0
  59. package/harness/bundles/general/codebuddy/scripts/harness_process.py +1065 -0
  60. package/harness/bundles/general/codebuddy/scripts/harness_profile.py +244 -4
  61. package/harness/bundles/general/codebuddy/scripts/harness_runtime.py +646 -132
  62. package/harness/bundles/general/codebuddy/scripts/harness_service.py +1469 -194
  63. package/harness/bundles/general/codebuddy/scripts/harness_test_runner.py +107 -117
  64. package/harness/bundles/general/codebuddy/scripts/harness_verification.py +79 -0
  65. package/harness/bundles/general/codex/.harness-build.json +1 -1
  66. package/harness/bundles/general/codex/contracts/build-profile-v3.schema.json +100 -0
  67. package/harness/bundles/general/codex/contracts/fixtures/managed-execution.json +341 -0
  68. package/harness/bundles/general/codex/contracts/process-identity.schema.json +132 -0
  69. package/harness/bundles/general/codex/contracts/run-session.schema.json +142 -0
  70. package/harness/bundles/general/codex/contracts/service-retirement-receipt.schema.json +85 -0
  71. package/harness/bundles/general/codex/contracts/service-session.schema.json +147 -0
  72. package/harness/bundles/general/codex/harness-archive/SKILL.md +1 -1
  73. package/harness/bundles/general/codex/harness-codebase-map/SKILL.md +1 -1
  74. package/harness/bundles/general/codex/harness-knowledge-ingest/SKILL.md +1 -1
  75. package/harness/bundles/general/codex/harness-knowledge-query/SKILL.md +1 -1
  76. package/harness/bundles/general/codex/harness-plan/SKILL.md +1 -1
  77. package/harness/bundles/general/codex/harness-review/SKILL.md +1 -1
  78. package/harness/bundles/general/codex/harness-run/SKILL.md +1 -1
  79. package/harness/bundles/general/codex/harness-submit/SKILL.md +1 -1
  80. package/harness/bundles/general/codex/harness-submit/checklist.md +12 -0
  81. package/harness/bundles/general/codex/harness-sync/SKILL.md +1 -1
  82. package/harness/bundles/general/codex/harness-test/SKILL.md +1 -1
  83. package/harness/bundles/general/codex/scripts/harness_archive.py +324 -28
  84. package/harness/bundles/general/codex/scripts/harness_change.py +137 -2
  85. package/harness/bundles/general/codex/scripts/harness_efficiency.py +153 -0
  86. package/harness/bundles/general/codex/scripts/harness_environment.py +55 -25
  87. package/harness/bundles/general/codex/scripts/harness_execution_contracts.py +596 -0
  88. package/harness/bundles/general/codex/scripts/harness_integration.py +289 -52
  89. package/harness/bundles/general/codex/scripts/harness_ledger.py +5 -0
  90. package/harness/bundles/general/codex/scripts/harness_orchestration.py +344 -0
  91. package/harness/bundles/general/codex/scripts/harness_process.py +1065 -0
  92. package/harness/bundles/general/codex/scripts/harness_profile.py +244 -4
  93. package/harness/bundles/general/codex/scripts/harness_runtime.py +646 -132
  94. package/harness/bundles/general/codex/scripts/harness_service.py +1469 -194
  95. package/harness/bundles/general/codex/scripts/harness_test_runner.py +107 -117
  96. package/harness/bundles/general/codex/scripts/harness_verification.py +79 -0
  97. package/harness/bundles/general/cursor/.harness-build.json +1 -1
  98. package/harness/bundles/general/cursor/contracts/build-profile-v3.schema.json +100 -0
  99. package/harness/bundles/general/cursor/contracts/fixtures/managed-execution.json +341 -0
  100. package/harness/bundles/general/cursor/contracts/process-identity.schema.json +132 -0
  101. package/harness/bundles/general/cursor/contracts/run-session.schema.json +142 -0
  102. package/harness/bundles/general/cursor/contracts/service-retirement-receipt.schema.json +85 -0
  103. package/harness/bundles/general/cursor/contracts/service-session.schema.json +147 -0
  104. package/harness/bundles/general/cursor/harness-archive/SKILL.md +1 -1
  105. package/harness/bundles/general/cursor/harness-codebase-map/SKILL.md +1 -1
  106. package/harness/bundles/general/cursor/harness-knowledge-ingest/SKILL.md +1 -1
  107. package/harness/bundles/general/cursor/harness-knowledge-query/SKILL.md +1 -1
  108. package/harness/bundles/general/cursor/harness-plan/SKILL.md +1 -1
  109. package/harness/bundles/general/cursor/harness-review/SKILL.md +1 -1
  110. package/harness/bundles/general/cursor/harness-run/SKILL.md +1 -1
  111. package/harness/bundles/general/cursor/harness-submit/SKILL.md +1 -1
  112. package/harness/bundles/general/cursor/harness-submit/checklist.md +12 -0
  113. package/harness/bundles/general/cursor/harness-sync/SKILL.md +1 -1
  114. package/harness/bundles/general/cursor/harness-test/SKILL.md +1 -1
  115. package/harness/bundles/general/cursor/scripts/harness_archive.py +324 -28
  116. package/harness/bundles/general/cursor/scripts/harness_change.py +137 -2
  117. package/harness/bundles/general/cursor/scripts/harness_efficiency.py +153 -0
  118. package/harness/bundles/general/cursor/scripts/harness_environment.py +55 -25
  119. package/harness/bundles/general/cursor/scripts/harness_execution_contracts.py +596 -0
  120. package/harness/bundles/general/cursor/scripts/harness_integration.py +289 -52
  121. package/harness/bundles/general/cursor/scripts/harness_ledger.py +5 -0
  122. package/harness/bundles/general/cursor/scripts/harness_orchestration.py +344 -0
  123. package/harness/bundles/general/cursor/scripts/harness_process.py +1065 -0
  124. package/harness/bundles/general/cursor/scripts/harness_profile.py +244 -4
  125. package/harness/bundles/general/cursor/scripts/harness_runtime.py +646 -132
  126. package/harness/bundles/general/cursor/scripts/harness_service.py +1469 -194
  127. package/harness/bundles/general/cursor/scripts/harness_test_runner.py +107 -117
  128. package/harness/bundles/general/cursor/scripts/harness_verification.py +79 -0
  129. package/harness/bundles/java/claude-code/.harness-build.json +1 -1
  130. package/harness/bundles/java/claude-code/contracts/build-profile-v3.schema.json +100 -0
  131. package/harness/bundles/java/claude-code/contracts/fixtures/managed-execution.json +341 -0
  132. package/harness/bundles/java/claude-code/contracts/process-identity.schema.json +132 -0
  133. package/harness/bundles/java/claude-code/contracts/run-session.schema.json +142 -0
  134. package/harness/bundles/java/claude-code/contracts/service-retirement-receipt.schema.json +85 -0
  135. package/harness/bundles/java/claude-code/contracts/service-session.schema.json +147 -0
  136. package/harness/bundles/java/claude-code/harness-apidoc/SKILL.md +1 -1
  137. package/harness/bundles/java/claude-code/harness-archive/SKILL.md +1 -1
  138. package/harness/bundles/java/claude-code/harness-codebase-map/SKILL.md +1 -1
  139. package/harness/bundles/java/claude-code/harness-knowledge-ingest/SKILL.md +1 -1
  140. package/harness/bundles/java/claude-code/harness-knowledge-query/SKILL.md +1 -1
  141. package/harness/bundles/java/claude-code/harness-package/SKILL.md +1 -1
  142. package/harness/bundles/java/claude-code/harness-plan/SKILL.md +1 -1
  143. package/harness/bundles/java/claude-code/harness-review/SKILL.md +1 -1
  144. package/harness/bundles/java/claude-code/harness-run/SKILL.md +1 -1
  145. package/harness/bundles/java/claude-code/harness-submit/SKILL.md +1 -1
  146. package/harness/bundles/java/claude-code/harness-submit/checklist.md +12 -0
  147. package/harness/bundles/java/claude-code/harness-sync/SKILL.md +1 -1
  148. package/harness/bundles/java/claude-code/harness-test/SKILL.md +1 -1
  149. package/harness/bundles/java/claude-code/scripts/harness_archive.py +324 -28
  150. package/harness/bundles/java/claude-code/scripts/harness_change.py +137 -2
  151. package/harness/bundles/java/claude-code/scripts/harness_efficiency.py +153 -0
  152. package/harness/bundles/java/claude-code/scripts/harness_environment.py +55 -25
  153. package/harness/bundles/java/claude-code/scripts/harness_execution_contracts.py +596 -0
  154. package/harness/bundles/java/claude-code/scripts/harness_integration.py +289 -52
  155. package/harness/bundles/java/claude-code/scripts/harness_ledger.py +5 -0
  156. package/harness/bundles/java/claude-code/scripts/harness_orchestration.py +344 -0
  157. package/harness/bundles/java/claude-code/scripts/harness_process.py +1065 -0
  158. package/harness/bundles/java/claude-code/scripts/harness_profile.py +244 -4
  159. package/harness/bundles/java/claude-code/scripts/harness_runtime.py +646 -132
  160. package/harness/bundles/java/claude-code/scripts/harness_service.py +1469 -194
  161. package/harness/bundles/java/claude-code/scripts/harness_test_runner.py +107 -117
  162. package/harness/bundles/java/claude-code/scripts/harness_verification.py +79 -0
  163. package/harness/bundles/java/codebuddy/.harness-build.json +1 -1
  164. package/harness/bundles/java/codebuddy/contracts/build-profile-v3.schema.json +100 -0
  165. package/harness/bundles/java/codebuddy/contracts/fixtures/managed-execution.json +341 -0
  166. package/harness/bundles/java/codebuddy/contracts/process-identity.schema.json +132 -0
  167. package/harness/bundles/java/codebuddy/contracts/run-session.schema.json +142 -0
  168. package/harness/bundles/java/codebuddy/contracts/service-retirement-receipt.schema.json +85 -0
  169. package/harness/bundles/java/codebuddy/contracts/service-session.schema.json +147 -0
  170. package/harness/bundles/java/codebuddy/harness-apidoc/SKILL.md +1 -1
  171. package/harness/bundles/java/codebuddy/harness-archive/SKILL.md +1 -1
  172. package/harness/bundles/java/codebuddy/harness-codebase-map/SKILL.md +1 -1
  173. package/harness/bundles/java/codebuddy/harness-knowledge-ingest/SKILL.md +1 -1
  174. package/harness/bundles/java/codebuddy/harness-knowledge-query/SKILL.md +1 -1
  175. package/harness/bundles/java/codebuddy/harness-package/SKILL.md +1 -1
  176. package/harness/bundles/java/codebuddy/harness-plan/SKILL.md +1 -1
  177. package/harness/bundles/java/codebuddy/harness-review/SKILL.md +1 -1
  178. package/harness/bundles/java/codebuddy/harness-run/SKILL.md +1 -1
  179. package/harness/bundles/java/codebuddy/harness-submit/SKILL.md +1 -1
  180. package/harness/bundles/java/codebuddy/harness-submit/checklist.md +12 -0
  181. package/harness/bundles/java/codebuddy/harness-sync/SKILL.md +1 -1
  182. package/harness/bundles/java/codebuddy/harness-test/SKILL.md +1 -1
  183. package/harness/bundles/java/codebuddy/scripts/harness_archive.py +324 -28
  184. package/harness/bundles/java/codebuddy/scripts/harness_change.py +137 -2
  185. package/harness/bundles/java/codebuddy/scripts/harness_efficiency.py +153 -0
  186. package/harness/bundles/java/codebuddy/scripts/harness_environment.py +55 -25
  187. package/harness/bundles/java/codebuddy/scripts/harness_execution_contracts.py +596 -0
  188. package/harness/bundles/java/codebuddy/scripts/harness_integration.py +289 -52
  189. package/harness/bundles/java/codebuddy/scripts/harness_ledger.py +5 -0
  190. package/harness/bundles/java/codebuddy/scripts/harness_orchestration.py +344 -0
  191. package/harness/bundles/java/codebuddy/scripts/harness_process.py +1065 -0
  192. package/harness/bundles/java/codebuddy/scripts/harness_profile.py +244 -4
  193. package/harness/bundles/java/codebuddy/scripts/harness_runtime.py +646 -132
  194. package/harness/bundles/java/codebuddy/scripts/harness_service.py +1469 -194
  195. package/harness/bundles/java/codebuddy/scripts/harness_test_runner.py +107 -117
  196. package/harness/bundles/java/codebuddy/scripts/harness_verification.py +79 -0
  197. package/harness/bundles/java/codex/.harness-build.json +1 -1
  198. package/harness/bundles/java/codex/contracts/build-profile-v3.schema.json +100 -0
  199. package/harness/bundles/java/codex/contracts/fixtures/managed-execution.json +341 -0
  200. package/harness/bundles/java/codex/contracts/process-identity.schema.json +132 -0
  201. package/harness/bundles/java/codex/contracts/run-session.schema.json +142 -0
  202. package/harness/bundles/java/codex/contracts/service-retirement-receipt.schema.json +85 -0
  203. package/harness/bundles/java/codex/contracts/service-session.schema.json +147 -0
  204. package/harness/bundles/java/codex/harness-apidoc/SKILL.md +1 -1
  205. package/harness/bundles/java/codex/harness-archive/SKILL.md +1 -1
  206. package/harness/bundles/java/codex/harness-codebase-map/SKILL.md +1 -1
  207. package/harness/bundles/java/codex/harness-knowledge-ingest/SKILL.md +1 -1
  208. package/harness/bundles/java/codex/harness-knowledge-query/SKILL.md +1 -1
  209. package/harness/bundles/java/codex/harness-package/SKILL.md +1 -1
  210. package/harness/bundles/java/codex/harness-plan/SKILL.md +1 -1
  211. package/harness/bundles/java/codex/harness-review/SKILL.md +1 -1
  212. package/harness/bundles/java/codex/harness-run/SKILL.md +1 -1
  213. package/harness/bundles/java/codex/harness-submit/SKILL.md +1 -1
  214. package/harness/bundles/java/codex/harness-submit/checklist.md +12 -0
  215. package/harness/bundles/java/codex/harness-sync/SKILL.md +1 -1
  216. package/harness/bundles/java/codex/harness-test/SKILL.md +1 -1
  217. package/harness/bundles/java/codex/scripts/harness_archive.py +324 -28
  218. package/harness/bundles/java/codex/scripts/harness_change.py +137 -2
  219. package/harness/bundles/java/codex/scripts/harness_efficiency.py +153 -0
  220. package/harness/bundles/java/codex/scripts/harness_environment.py +55 -25
  221. package/harness/bundles/java/codex/scripts/harness_execution_contracts.py +596 -0
  222. package/harness/bundles/java/codex/scripts/harness_integration.py +289 -52
  223. package/harness/bundles/java/codex/scripts/harness_ledger.py +5 -0
  224. package/harness/bundles/java/codex/scripts/harness_orchestration.py +344 -0
  225. package/harness/bundles/java/codex/scripts/harness_process.py +1065 -0
  226. package/harness/bundles/java/codex/scripts/harness_profile.py +244 -4
  227. package/harness/bundles/java/codex/scripts/harness_runtime.py +646 -132
  228. package/harness/bundles/java/codex/scripts/harness_service.py +1469 -194
  229. package/harness/bundles/java/codex/scripts/harness_test_runner.py +107 -117
  230. package/harness/bundles/java/codex/scripts/harness_verification.py +79 -0
  231. package/harness/bundles/java/cursor/.harness-build.json +1 -1
  232. package/harness/bundles/java/cursor/contracts/build-profile-v3.schema.json +100 -0
  233. package/harness/bundles/java/cursor/contracts/fixtures/managed-execution.json +341 -0
  234. package/harness/bundles/java/cursor/contracts/process-identity.schema.json +132 -0
  235. package/harness/bundles/java/cursor/contracts/run-session.schema.json +142 -0
  236. package/harness/bundles/java/cursor/contracts/service-retirement-receipt.schema.json +85 -0
  237. package/harness/bundles/java/cursor/contracts/service-session.schema.json +147 -0
  238. package/harness/bundles/java/cursor/harness-apidoc/SKILL.md +1 -1
  239. package/harness/bundles/java/cursor/harness-archive/SKILL.md +1 -1
  240. package/harness/bundles/java/cursor/harness-codebase-map/SKILL.md +1 -1
  241. package/harness/bundles/java/cursor/harness-knowledge-ingest/SKILL.md +1 -1
  242. package/harness/bundles/java/cursor/harness-knowledge-query/SKILL.md +1 -1
  243. package/harness/bundles/java/cursor/harness-package/SKILL.md +1 -1
  244. package/harness/bundles/java/cursor/harness-plan/SKILL.md +1 -1
  245. package/harness/bundles/java/cursor/harness-review/SKILL.md +1 -1
  246. package/harness/bundles/java/cursor/harness-run/SKILL.md +1 -1
  247. package/harness/bundles/java/cursor/harness-submit/SKILL.md +1 -1
  248. package/harness/bundles/java/cursor/harness-submit/checklist.md +12 -0
  249. package/harness/bundles/java/cursor/harness-sync/SKILL.md +1 -1
  250. package/harness/bundles/java/cursor/harness-test/SKILL.md +1 -1
  251. package/harness/bundles/java/cursor/scripts/harness_archive.py +324 -28
  252. package/harness/bundles/java/cursor/scripts/harness_change.py +137 -2
  253. package/harness/bundles/java/cursor/scripts/harness_efficiency.py +153 -0
  254. package/harness/bundles/java/cursor/scripts/harness_environment.py +55 -25
  255. package/harness/bundles/java/cursor/scripts/harness_execution_contracts.py +596 -0
  256. package/harness/bundles/java/cursor/scripts/harness_integration.py +289 -52
  257. package/harness/bundles/java/cursor/scripts/harness_ledger.py +5 -0
  258. package/harness/bundles/java/cursor/scripts/harness_orchestration.py +344 -0
  259. package/harness/bundles/java/cursor/scripts/harness_process.py +1065 -0
  260. package/harness/bundles/java/cursor/scripts/harness_profile.py +244 -4
  261. package/harness/bundles/java/cursor/scripts/harness_runtime.py +646 -132
  262. package/harness/bundles/java/cursor/scripts/harness_service.py +1469 -194
  263. package/harness/bundles/java/cursor/scripts/harness_test_runner.py +107 -117
  264. package/harness/bundles/java/cursor/scripts/harness_verification.py +79 -0
  265. package/harness/manifests/general/claude-code.json +58 -26
  266. package/harness/manifests/general/codebuddy.json +58 -26
  267. package/harness/manifests/general/codex.json +58 -26
  268. package/harness/manifests/general/cursor.json +58 -26
  269. package/harness/manifests/java/claude-code.json +60 -28
  270. package/harness/manifests/java/codebuddy.json +60 -28
  271. package/harness/manifests/java/codex.json +60 -28
  272. package/harness/manifests/java/cursor.json +60 -28
  273. package/hunter-workflow-family.json +4 -4
  274. package/package.json +1 -1
@@ -27,7 +27,7 @@ import sys
27
27
  import time
28
28
  import uuid
29
29
  from pathlib import Path
30
- from typing import Any
30
+ from typing import Any, Callable, Mapping
31
31
 
32
32
  SCRIPTS_DIR = Path(__file__).resolve().parent
33
33
  if str(SCRIPTS_DIR) not in sys.path:
@@ -586,12 +586,14 @@ def _claim_lease_locked(
586
586
  run_id: str,
587
587
  ttl_seconds: int,
588
588
  steal: bool = False,
589
+ expected_generation: int | None = None,
589
590
  ) -> dict[str, Any]:
590
591
  path = _lease_path(project_root, change_id)
591
592
  path.parent.mkdir(parents=True, exist_ok=True)
592
593
  now = dt.datetime.now().astimezone()
593
594
  expires = now + dt.timedelta(seconds=max(1, ttl_seconds))
594
595
  new_lease = {
596
+ "leaseId": str(uuid.uuid4()),
595
597
  "changeId": change_id,
596
598
  "phase": phase,
597
599
  "runId": run_id,
@@ -599,6 +601,7 @@ def _claim_lease_locked(
599
601
  "acquiredAt": now.isoformat(timespec="milliseconds"),
600
602
  "expiresAt": expires.isoformat(timespec="milliseconds"),
601
603
  "ttlSeconds": ttl_seconds,
604
+ "generation": 1,
602
605
  }
603
606
  if path.is_file():
604
607
  try:
@@ -606,6 +609,12 @@ def _claim_lease_locked(
606
609
  except (OSError, json.JSONDecodeError):
607
610
  existing = None
608
611
  if isinstance(existing, dict) and not _lease_expired(existing):
612
+ if expected_generation is not None and existing.get("generation") != expected_generation:
613
+ return {
614
+ "ok": False,
615
+ "code": "LEASE_GENERATION_CONFLICT",
616
+ "holder": existing,
617
+ }
609
618
  same_owner = str(existing.get("runId")) == run_id
610
619
  if same_owner:
611
620
  existing.update(
@@ -614,6 +623,7 @@ def _claim_lease_locked(
614
623
  "pid": os.getpid(),
615
624
  "expiresAt": expires.isoformat(timespec="milliseconds"),
616
625
  "refreshedAt": now.isoformat(timespec="milliseconds"),
626
+ "generation": int(existing.get("generation") or 1) + 1,
617
627
  }
618
628
  )
619
629
  _write_json(path, existing)
@@ -637,6 +647,7 @@ def claim_lease(
637
647
  run_id: str,
638
648
  ttl_seconds: int,
639
649
  steal: bool = False,
650
+ expected_generation: int | None = None,
640
651
  ) -> dict[str, Any]:
641
652
  path = _lease_path(project_root, change_id)
642
653
  with _exclusive_file_lock(path.with_suffix(".lock")):
@@ -647,6 +658,7 @@ def claim_lease(
647
658
  run_id=run_id,
648
659
  ttl_seconds=ttl_seconds,
649
660
  steal=steal,
661
+ expected_generation=expected_generation,
650
662
  )
651
663
 
652
664
 
@@ -656,6 +668,8 @@ def _release_lease_locked(
656
668
  change_id: str,
657
669
  phase: str,
658
670
  run_id: str,
671
+ lease_id: str | None = None,
672
+ generation: int | None = None,
659
673
  ) -> dict[str, Any]:
660
674
  path = _lease_path(project_root, change_id)
661
675
  if not path.is_file():
@@ -688,6 +702,18 @@ def _release_lease_locked(
688
702
  "message": f"lease phase is {existing.get('phase')}, not {phase}",
689
703
  "holder": existing,
690
704
  }
705
+ if lease_id is not None and str(existing.get("leaseId")) != lease_id:
706
+ return {
707
+ "ok": False,
708
+ "code": "LEASE_ID_MISMATCH",
709
+ "holder": existing,
710
+ }
711
+ if generation is not None and existing.get("generation") != generation:
712
+ return {
713
+ "ok": False,
714
+ "code": "LEASE_GENERATION_CONFLICT",
715
+ "holder": existing,
716
+ }
691
717
  path.unlink(missing_ok=True)
692
718
  return {"ok": True, "code": "LEASE_RELEASED", "changeId": change_id, "phase": phase}
693
719
 
@@ -698,11 +724,18 @@ def release_lease(
698
724
  change_id: str,
699
725
  phase: str,
700
726
  run_id: str,
727
+ lease_id: str | None = None,
728
+ generation: int | None = None,
701
729
  ) -> dict[str, Any]:
702
730
  path = _lease_path(project_root, change_id)
703
731
  with _exclusive_file_lock(path.with_suffix(".lock")):
704
732
  return _release_lease_locked(
705
- project_root, change_id=change_id, phase=phase, run_id=run_id
733
+ project_root,
734
+ change_id=change_id,
735
+ phase=phase,
736
+ run_id=run_id,
737
+ lease_id=lease_id,
738
+ generation=generation,
706
739
  )
707
740
 
708
741
 
@@ -712,6 +745,8 @@ def lease_port(
712
745
  change_id: str,
713
746
  run_id: str,
714
747
  port_range: tuple[int, int],
748
+ generation: int = 1,
749
+ listener_identity: Mapping[str, Any] | None = None,
715
750
  ) -> dict[str, Any]:
716
751
  start, end = port_range
717
752
  if start > end:
@@ -749,6 +784,7 @@ def lease_port(
749
784
  "leaseId": lease_id,
750
785
  "changeId": change_id,
751
786
  "runId": run_id,
787
+ "generation": generation,
752
788
  "pid": os.getpid(),
753
789
  "port": port,
754
790
  "acquiredAt": now_iso(),
@@ -756,6 +792,9 @@ def lease_port(
756
792
  dt.datetime.now().astimezone() + dt.timedelta(hours=4)
757
793
  ).isoformat(timespec="milliseconds"),
758
794
  }
795
+ if isinstance(listener_identity, Mapping):
796
+ entry["listenerIdentity"] = dict(listener_identity)
797
+ entry["listenerProofRequired"] = True
759
798
  leases.append(entry)
760
799
  registry["leases"] = leases
761
800
  _write_json(registry_path, registry)
@@ -774,6 +813,9 @@ def release_port(
774
813
  run_id: str,
775
814
  port: int | None = None,
776
815
  lease_id: str | None = None,
816
+ generation: int | None = None,
817
+ listener_identity: Mapping[str, Any] | None = None,
818
+ listener_observer: Callable[[int], Mapping[str, Any] | None] | None = None,
777
819
  ) -> dict[str, Any]:
778
820
  """Release port leases by subset (retro §5.16).
779
821
 
@@ -802,6 +844,12 @@ def release_port(
802
844
  ]
803
845
  # Validate that the lease belongs to this change
804
846
  for item in to_release:
847
+ if generation is not None and item.get("generation") != generation:
848
+ return {
849
+ "ok": False,
850
+ "code": "PORT_LEASE_GENERATION_CONFLICT",
851
+ "holder": item,
852
+ }
805
853
  if str(item.get("changeId")) != change_id:
806
854
  return {
807
855
  "ok": False,
@@ -818,6 +866,12 @@ def release_port(
818
866
  ]
819
867
  # Validate runId matches
820
868
  for item in to_release:
869
+ if generation is not None and item.get("generation") != generation:
870
+ return {
871
+ "ok": False,
872
+ "code": "PORT_LEASE_GENERATION_CONFLICT",
873
+ "holder": item,
874
+ }
821
875
  if str(item.get("runId")) != run_id:
822
876
  return {
823
877
  "ok": False,
@@ -840,6 +894,47 @@ def release_port(
840
894
  and str(item.get("changeId")) == change_id
841
895
  and str(item.get("runId")) == run_id
842
896
  ]
897
+ if generation is not None:
898
+ mismatched = [
899
+ item for item in to_release if item.get("generation") != generation
900
+ ]
901
+ if mismatched:
902
+ return {
903
+ "ok": False,
904
+ "code": "PORT_LEASE_GENERATION_CONFLICT",
905
+ "holder": mismatched[0],
906
+ }
907
+
908
+ for item in to_release:
909
+ expected_listener = item.get("listenerIdentity")
910
+ if not isinstance(expected_listener, Mapping):
911
+ continue
912
+ observed_listener = (
913
+ listener_observer(int(item["port"]))
914
+ if listener_observer is not None and isinstance(item.get("port"), int)
915
+ else listener_identity
916
+ )
917
+ if not isinstance(observed_listener, Mapping):
918
+ return {
919
+ "ok": False,
920
+ "code": "LISTENER_IDENTITY_UNVERIFIABLE",
921
+ "message": "listener identity could not be independently observed",
922
+ "holder": item,
923
+ }
924
+ try:
925
+ from harness_process import verify_process_identity
926
+
927
+ decision = verify_process_identity(expected_listener, observed_listener)
928
+ except (ImportError, TypeError, ValueError):
929
+ decision = {"ok": False, "reasonCode": "IDENTITY_UNVERIFIABLE"}
930
+ if decision.get("ok") is not True:
931
+ return {
932
+ "ok": False,
933
+ "code": "LISTENER_IDENTITY_UNVERIFIABLE",
934
+ "message": "listener identity did not match the leased service",
935
+ "details": decision,
936
+ "holder": item,
937
+ }
843
938
 
844
939
  if not to_release:
845
940
  # Check if there are other leases under this changeId (different runId)
@@ -1011,6 +1106,7 @@ def cmd_claim(args: argparse.Namespace) -> int:
1011
1106
  run_id=args.run_id,
1012
1107
  ttl_seconds=int(args.ttl_seconds),
1013
1108
  steal=bool(args.steal),
1109
+ expected_generation=getattr(args, "expected_generation", None),
1014
1110
  )
1015
1111
  if payload.get("ok"):
1016
1112
  emit(payload, as_json=bool(args.json))
@@ -1030,6 +1126,8 @@ def cmd_release(args: argparse.Namespace) -> int:
1030
1126
  change_id=args.change,
1031
1127
  phase=args.phase,
1032
1128
  run_id=args.run_id,
1129
+ lease_id=getattr(args, "lease_id", None),
1130
+ generation=getattr(args, "generation", None),
1033
1131
  )
1034
1132
  if payload.get("ok"):
1035
1133
  emit(payload, as_json=bool(args.json))
@@ -1051,11 +1149,26 @@ def cmd_lease_port(args: argparse.Namespace) -> int:
1051
1149
  f"expected --range <start-end>, got {args.range!r}",
1052
1150
  as_json=bool(args.json),
1053
1151
  )
1152
+ listener_identity = None
1153
+ if getattr(args, "listener_identity_json", None):
1154
+ try:
1155
+ parsed_listener = json.loads(args.listener_identity_json)
1156
+ if not isinstance(parsed_listener, dict):
1157
+ raise ValueError("listener identity must be an object")
1158
+ listener_identity = parsed_listener
1159
+ except (TypeError, ValueError, json.JSONDecodeError) as exc:
1160
+ return emit_error(
1161
+ "LISTENER_IDENTITY_UNVERIFIABLE",
1162
+ str(exc),
1163
+ as_json=bool(args.json),
1164
+ )
1054
1165
  payload = lease_port(
1055
1166
  project,
1056
1167
  change_id=args.change,
1057
1168
  run_id=args.run_id,
1058
1169
  port_range=parsed,
1170
+ generation=int(getattr(args, "generation", 1)),
1171
+ listener_identity=listener_identity,
1059
1172
  )
1060
1173
  if payload.get("ok"):
1061
1174
  emit(payload, as_json=bool(args.json))
@@ -1070,12 +1183,27 @@ def cmd_lease_port(args: argparse.Namespace) -> int:
1070
1183
 
1071
1184
  def cmd_release_port(args: argparse.Namespace) -> int:
1072
1185
  project = resolve_main_project_root()
1186
+ listener_identity = None
1187
+ if getattr(args, "listener_identity_json", None):
1188
+ try:
1189
+ parsed_listener = json.loads(args.listener_identity_json)
1190
+ if not isinstance(parsed_listener, dict):
1191
+ raise ValueError("listener identity must be an object")
1192
+ listener_identity = parsed_listener
1193
+ except (TypeError, ValueError, json.JSONDecodeError) as exc:
1194
+ return emit_error(
1195
+ "LISTENER_IDENTITY_UNVERIFIABLE",
1196
+ str(exc),
1197
+ as_json=bool(args.json),
1198
+ )
1073
1199
  payload = release_port(
1074
1200
  project,
1075
1201
  change_id=args.change,
1076
1202
  run_id=args.run_id,
1077
1203
  port=getattr(args, "port", None),
1078
1204
  lease_id=getattr(args, "lease_id", None),
1205
+ generation=getattr(args, "generation", None),
1206
+ listener_identity=listener_identity,
1079
1207
  )
1080
1208
  if payload.get("ok"):
1081
1209
  emit(payload, as_json=bool(args.json))
@@ -1329,18 +1457,23 @@ def build_parser() -> argparse.ArgumentParser:
1329
1457
  p_claim.add_argument("--run-id", required=True)
1330
1458
  p_claim.add_argument("--ttl-seconds", type=int, default=3600)
1331
1459
  p_claim.add_argument("--steal", action="store_true")
1460
+ p_claim.add_argument("--expected-generation", type=int, default=None)
1332
1461
  p_claim.set_defaults(func=cmd_claim)
1333
1462
 
1334
1463
  p_release = sub.add_parser("release", parents=[shared])
1335
1464
  p_release.add_argument("--change", required=True)
1336
1465
  p_release.add_argument("--phase", required=True)
1337
1466
  p_release.add_argument("--run-id", required=True)
1467
+ p_release.add_argument("--lease-id", default=None)
1468
+ p_release.add_argument("--generation", type=int, default=None)
1338
1469
  p_release.set_defaults(func=cmd_release)
1339
1470
 
1340
1471
  p_port = sub.add_parser("lease-port", parents=[shared])
1341
1472
  p_port.add_argument("--change", required=True)
1342
1473
  p_port.add_argument("--run-id", required=True)
1343
1474
  p_port.add_argument("--range", required=True)
1475
+ p_port.add_argument("--generation", type=int, default=1)
1476
+ p_port.add_argument("--listener-identity-json", default=None)
1344
1477
  p_port.set_defaults(func=cmd_lease_port)
1345
1478
 
1346
1479
  p_port_release = sub.add_parser("release-port", parents=[shared])
@@ -1348,6 +1481,8 @@ def build_parser() -> argparse.ArgumentParser:
1348
1481
  p_port_release.add_argument("--run-id", required=True)
1349
1482
  p_port_release.add_argument("--port", type=int, default=None)
1350
1483
  p_port_release.add_argument("--lease-id", default=None)
1484
+ p_port_release.add_argument("--generation", type=int, default=None)
1485
+ p_port_release.add_argument("--listener-identity-json", default=None)
1351
1486
  p_port_release.set_defaults(func=cmd_release_port)
1352
1487
 
1353
1488
  p_lock = sub.add_parser("integration-lock", parents=[shared])
@@ -30,6 +30,131 @@ def _integer(value: Any) -> int:
30
30
  return 0
31
31
 
32
32
 
33
+ def _timestamp(value: Any) -> dt.datetime | None:
34
+ if isinstance(value, dt.datetime):
35
+ parsed = value
36
+ elif value:
37
+ try:
38
+ parsed = dt.datetime.fromisoformat(str(value).replace("Z", "+00:00"))
39
+ except (TypeError, ValueError):
40
+ return None
41
+ else:
42
+ return None
43
+ if parsed.tzinfo is None:
44
+ parsed = parsed.replace(tzinfo=dt.timezone.utc)
45
+ return parsed.astimezone(dt.timezone.utc)
46
+
47
+
48
+ def _now_timestamp(value: Any = None) -> dt.datetime:
49
+ return _timestamp(value) or dt.datetime.now(dt.timezone.utc)
50
+
51
+
52
+ def _seconds(value: Any) -> float | None:
53
+ try:
54
+ parsed = float(value)
55
+ except (TypeError, ValueError):
56
+ return None
57
+ return parsed if parsed > 0 else None
58
+
59
+
60
+ def classify_progress(
61
+ session: dict[str, Any],
62
+ *,
63
+ now: Any = None,
64
+ historical_durations: list[int | float] | None = None,
65
+ ) -> dict[str, Any]:
66
+ """Classify observable run state without changing execution policy.
67
+
68
+ The classifier is intentionally conservative: incomplete clock/heartbeat
69
+ data produces explicit ``INCOMPLETE``/``NO_BUDGET`` facts instead of an
70
+ invented ETA or a guessed failure cause.
71
+ """
72
+ status = str(session.get("status") or "RUNNING").upper()
73
+ terminal = {
74
+ "OK": "COMPLETED",
75
+ "PASS": "COMPLETED",
76
+ "COMPLETED": "COMPLETED",
77
+ "FAIL": "FAILED",
78
+ "FAILED": "FAILED",
79
+ "CANCELLED": "CANCELLED",
80
+ "CANCELED": "CANCELLED",
81
+ "INCOMPLETE": "INCOMPLETE",
82
+ }
83
+ started = _timestamp(session.get("startedAt") or session.get("createdAt"))
84
+ current = _now_timestamp(now)
85
+ elapsed_seconds: float | None = None
86
+ if started is not None:
87
+ elapsed_seconds = max(0.0, (current - started).total_seconds())
88
+
89
+ timeout_seconds = _seconds(session.get("timeoutSeconds"))
90
+ expected_seconds = _seconds(
91
+ session.get("expectedDurationSeconds")
92
+ or session.get("estimatedDurationSeconds")
93
+ )
94
+ budget_seconds = _seconds(
95
+ session.get("budgetSeconds")
96
+ or session.get("maxDurationSeconds")
97
+ or expected_seconds
98
+ or timeout_seconds
99
+ )
100
+ if elapsed_seconds is None or budget_seconds is None:
101
+ budget_state = "NO_BUDGET"
102
+ elif elapsed_seconds >= budget_seconds:
103
+ budget_state = "OVER_BUDGET"
104
+ else:
105
+ budget_state = "WITHIN_BUDGET"
106
+
107
+ progress_state = terminal.get(status)
108
+ diagnostic_required = False
109
+ heartbeat_grace = _seconds(
110
+ session.get("heartbeatGraceSeconds")
111
+ or session.get("heartbeatTimeoutSeconds")
112
+ or 60
113
+ ) or 60
114
+ heartbeat = _timestamp(session.get("lastHeartbeatAt"))
115
+ output = _timestamp(
116
+ session.get("lastOutputAt") or session.get("lastLogAt")
117
+ )
118
+ heartbeat_gap = (
119
+ max(0.0, (current - heartbeat).total_seconds())
120
+ if heartbeat is not None
121
+ else None
122
+ )
123
+ output_gap = (
124
+ max(0.0, (current - output).total_seconds()) if output is not None else None
125
+ )
126
+
127
+ if progress_state is None:
128
+ progress_state = "RUNNING"
129
+ if timeout_seconds is not None and elapsed_seconds is not None and elapsed_seconds >= timeout_seconds:
130
+ progress_state = "TIMEOUT"
131
+ elif output_gap is not None and output_gap > heartbeat_grace:
132
+ progress_state = "NO_OUTPUT_PROCESS_ACTIVE"
133
+ elif heartbeat_gap is None or heartbeat_gap > heartbeat_grace:
134
+ progress_state = "HEARTBEAT_LOST"
135
+ elif _integer(session.get("resourceWaitMs")) > 0 or session.get("resourceWaitActive") is True:
136
+ progress_state = "RESOURCE_WAIT"
137
+ elif expected_seconds is not None and elapsed_seconds is not None and elapsed_seconds > expected_seconds:
138
+ progress_state = "SLOW_PROGRESSING"
139
+ diagnostic_required = progress_state in {
140
+ "TIMEOUT",
141
+ "HEARTBEAT_LOST",
142
+ "NO_OUTPUT_PROCESS_ACTIVE",
143
+ } or budget_state == "OVER_BUDGET"
144
+ elif progress_state not in {"COMPLETED", "FAILED", "CANCELLED"}:
145
+ diagnostic_required = True
146
+
147
+ return {
148
+ "progressState": progress_state,
149
+ "budgetState": budget_state,
150
+ "diagnosticRequired": diagnostic_required,
151
+ "elapsedSeconds": int(elapsed_seconds) if elapsed_seconds is not None else None,
152
+ "budgetSeconds": budget_seconds,
153
+ "heartbeatGapSeconds": int(heartbeat_gap) if heartbeat_gap is not None else None,
154
+ "outputGapSeconds": int(output_gap) if output_gap is not None else None,
155
+ }
156
+
157
+
33
158
  def _failure_class(session: dict[str, Any]) -> str | None:
34
159
  status = str(session.get("status") or "").upper()
35
160
  reason = str(session.get("reasonCode") or "").upper()
@@ -128,10 +253,27 @@ def build_efficiency_summary(
128
253
  invalidations: list[dict[str, Any]],
129
254
  ) -> dict[str, Any]:
130
255
  failure_counts = {key: 0 for key in FAILURE_CLASSES}
256
+ progress_states: Counter[str] = Counter()
257
+ budget_states: Counter[str] = Counter()
258
+ slow_stages: Counter[str] = Counter()
131
259
  for session in run_sessions:
132
260
  classification = _failure_class(session)
133
261
  if classification is not None:
134
262
  failure_counts[classification] += 1
263
+ progress = str(
264
+ session.get("progressState")
265
+ or classify_progress(session).get("progressState")
266
+ or "INCOMPLETE"
267
+ ).upper()
268
+ budget = str(
269
+ session.get("budgetState")
270
+ or classify_progress(session).get("budgetState")
271
+ or "NO_BUDGET"
272
+ ).upper()
273
+ progress_states[progress] += 1
274
+ budget_states[budget] += 1
275
+ if progress == "SLOW_PROGRESSING":
276
+ slow_stages[str(session.get("stage") or session.get("verification") or "unknown")] += 1
135
277
  environment_actions: Counter[str] = Counter()
136
278
  for item in environment_receipts:
137
279
  action = str(item.get("action") or "").lower()
@@ -183,6 +325,9 @@ def build_efficiency_summary(
183
325
  for item in run_sessions
184
326
  ),
185
327
  "failureClasses": failure_counts,
328
+ "progressStates": dict(sorted(progress_states.items())),
329
+ "budgetStates": dict(sorted(budget_states.items())),
330
+ "slowStages": dict(sorted(slow_stages.items())),
186
331
  "invalidationReasons": _counts(
187
332
  str(item.get("reasonCode") or "UNKNOWN")
188
333
  for item in invalidations
@@ -226,6 +371,10 @@ def compact_progress_view(
226
371
  "typicalSeconds": int(median(durations)),
227
372
  "highSeconds": durations[-1],
228
373
  }
374
+ classification = classify_progress(
375
+ session,
376
+ historical_durations=historical_durations,
377
+ )
229
378
  return {
230
379
  "verification": session.get("verification"),
231
380
  "stage": session.get("stage"),
@@ -235,6 +384,10 @@ def compact_progress_view(
235
384
  "eta": eta,
236
385
  "resourceWait": list(session.get("resourceLocks") or []),
237
386
  "status": session.get("status", "RUNNING"),
387
+ "progressState": classification["progressState"],
388
+ "budgetState": classification["budgetState"],
389
+ "diagnosticRequired": classification["diagnosticRequired"],
390
+ "elapsedSeconds": classification["elapsedSeconds"],
238
391
  }
239
392
 
240
393
 
@@ -23,10 +23,31 @@ from contextlib import contextmanager
23
23
  from pathlib import Path
24
24
  from typing import Any, Iterator
25
25
 
26
- import harness_service as hservice
26
+ import harness_process as hprocess
27
27
  from harness_paths import resolve_state_dir_for_contract
28
28
 
29
29
 
30
+ def _verify_recorded_owner(owner: dict[str, Any]) -> bool | None:
31
+ try:
32
+ pid = int(owner.get("pid") or 0)
33
+ except (TypeError, ValueError):
34
+ return None
35
+ observed = hprocess.observe_process_identity(pid)
36
+ decision = hprocess.verify_process_identity(
37
+ {
38
+ "pid": pid,
39
+ "createdAt": owner.get("startedAt"),
40
+ "executable": owner.get("executable"),
41
+ },
42
+ observed,
43
+ )
44
+ if decision.get("ok") is True:
45
+ return True
46
+ if decision.get("reasonCode") == "IDENTITY_UNVERIFIABLE":
47
+ return None
48
+ return False
49
+
50
+
30
51
  if hasattr(sys.stdout, "reconfigure"):
31
52
  sys.stdout.reconfigure(encoding="utf-8")
32
53
  if hasattr(sys.stderr, "reconfigure"):
@@ -358,7 +379,7 @@ def _lease_registry_guard(lease_root: Path) -> Iterator[None]:
358
379
  except (OSError, json.JSONDecodeError):
359
380
  holder = {}
360
381
  pid = holder.get("pid") if isinstance(holder, dict) else None
361
- if isinstance(pid, int) and pid > 0 and not hservice.is_pid_alive(pid):
382
+ if isinstance(pid, int) and pid > 0 and not hprocess.is_pid_alive(pid):
362
383
  try:
363
384
  lock_path.unlink()
364
385
  continue
@@ -444,22 +465,40 @@ def execute_provider_operation(
444
465
  raise ValueError("ENVIRONMENT_PROVIDER_LEASE_INVALID")
445
466
  executable = shutil.which(argv[0]) or str(Path(argv[0]).expanduser().resolve())
446
467
  started_at = now_iso()
447
- process = subprocess.Popen(
468
+ spawned = hprocess.spawn_structured_argv(
448
469
  argv,
449
- cwd=str(project.resolve()),
450
- stdin=subprocess.DEVNULL,
470
+ cwd=project.resolve(),
471
+ environment={},
472
+ owner_token=f"environment:{change_id}:{stack_id}:{uuid.uuid4().hex}",
451
473
  stdout=subprocess.PIPE,
452
474
  stderr=subprocess.PIPE,
453
- shell=False,
454
475
  )
455
- created = hservice.get_process_create_time(process.pid)
476
+ process = spawned.process
477
+ created = hprocess.get_process_create_time(process.pid)
478
+ cleanup_complete = False
456
479
  try:
457
480
  stdout, stderr = process.communicate(timeout=timeout_seconds)
458
481
  timed_out = False
459
482
  except subprocess.TimeoutExpired:
460
483
  timed_out = True
461
- hservice.terminate_process_tree(process.pid)
484
+ hprocess.capture_owned_members(spawned)
485
+ cleanup = hprocess.terminate_owned_tree(
486
+ spawned.attestation,
487
+ spawned.ownershipProof,
488
+ {"graceSeconds": 3.0},
489
+ )
490
+ cleanup_complete = bool(cleanup.get("cleanupComplete"))
462
491
  stdout, stderr = process.communicate()
492
+ finally:
493
+ if process.poll() is None:
494
+ hprocess.capture_owned_members(spawned)
495
+ cleanup = hprocess.terminate_owned_tree(
496
+ spawned.attestation,
497
+ spawned.ownershipProof,
498
+ {"graceSeconds": 3.0},
499
+ )
500
+ cleanup_complete = bool(cleanup.get("cleanupComplete"))
501
+ spawned.close()
463
502
  operation_id = uuid.uuid4().hex
464
503
  receipt_path = _provider_operation_root(root) / f"{operation}-{operation_id}.json"
465
504
  receipt: dict[str, Any] = {
@@ -490,6 +529,9 @@ def execute_provider_operation(
490
529
  "utf-8"
491
530
  )
492
531
  ),
532
+ "processAttestation": spawned.attestation,
533
+ "ownershipProof": spawned.ownershipProof,
534
+ "cleanupComplete": cleanup_complete or process.poll() is not None,
493
535
  },
494
536
  "exitCode": process.returncode,
495
537
  "timedOut": timed_out,
@@ -716,7 +758,7 @@ def classify_leases(lease_root: Path) -> list[dict[str, Any]]:
716
758
  item["classification"] = "OWNER_IDENTITY_INCOMPLETE"
717
759
  results.append(item)
718
760
  continue
719
- if not hservice.is_pid_alive(owner_pid):
761
+ if not hprocess.is_pid_alive(owner_pid):
720
762
  item["classification"] = "RECLAIMABLE"
721
763
  results.append(item)
722
764
  continue
@@ -728,13 +770,7 @@ def classify_leases(lease_root: Path) -> list[dict[str, Any]]:
728
770
  )
729
771
  results.append(item)
730
772
  continue
731
- identity = hservice.verify_process_identity(
732
- {
733
- "pid": owner_pid,
734
- "startedAt": owner["startedAt"],
735
- "processIdentity": {"executable": owner["executable"]},
736
- }
737
- )
773
+ identity = _verify_recorded_owner(owner)
738
774
  item["classification"] = (
739
775
  "OWNER_ACTIVE"
740
776
  if identity is True
@@ -1021,13 +1057,7 @@ def _acquire_lease_locked(
1021
1057
  owner_pid = int(owner.get("pid"))
1022
1058
  except (TypeError, ValueError):
1023
1059
  owner_pid = 0
1024
- owner_verified = hservice.verify_process_identity(
1025
- {
1026
- "pid": owner_pid,
1027
- "startedAt": owner.get("startedAt"),
1028
- "processIdentity": {"executable": owner.get("executable")},
1029
- }
1030
- ) is True
1060
+ owner_verified = _verify_recorded_owner(owner) is True
1031
1061
  expires_at = _parse_expiry(current.get("expiresAt"))
1032
1062
  unexpired = (
1033
1063
  expires_at is not None
@@ -1106,10 +1136,10 @@ def _acquire_lease_locked(
1106
1136
  "resetCount": reset_count + (1 if is_reset else 0),
1107
1137
  "owner": {
1108
1138
  "pid": os.getpid(),
1109
- "executable": hservice.get_process_executable(os.getpid())
1139
+ "executable": hprocess.get_process_executable(os.getpid())
1110
1140
  or str(Path(sys.executable).resolve()),
1111
1141
  "startedAt": (
1112
- hservice.get_process_create_time(os.getpid())
1142
+ hprocess.get_process_create_time(os.getpid())
1113
1143
  or started
1114
1144
  ).isoformat(timespec="seconds"),
1115
1145
  },