harnery 0.36.0 → 0.38.0

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 (432) hide show
  1. package/dist/commander.d.ts +9 -0
  2. package/dist/commander.d.ts.map +1 -1
  3. package/dist/commander.js +10 -1
  4. package/dist/commands/admission.d.ts +21 -0
  5. package/dist/commands/admission.d.ts.map +1 -0
  6. package/dist/commands/admission.js +565 -0
  7. package/dist/commands/agents.d.ts +7 -0
  8. package/dist/commands/agents.d.ts.map +1 -1
  9. package/dist/commands/agents.js +148 -47
  10. package/dist/commands/artifacts.d.ts.map +1 -1
  11. package/dist/commands/artifacts.js +73 -2
  12. package/dist/commands/browse-ai.d.ts +2 -2
  13. package/dist/commands/browse-ai.d.ts.map +1 -1
  14. package/dist/commands/browse-ai.js +6 -4
  15. package/dist/commands/browse.d.ts.map +1 -1
  16. package/dist/commands/browse.js +360 -21
  17. package/dist/commands/diagnostics.d.ts +4 -0
  18. package/dist/commands/diagnostics.d.ts.map +1 -0
  19. package/dist/commands/diagnostics.js +229 -0
  20. package/dist/commands/fetch.js +1 -0
  21. package/dist/commands/files.d.ts +12 -0
  22. package/dist/commands/files.d.ts.map +1 -1
  23. package/dist/commands/files.js +54 -1
  24. package/dist/commands/governor.d.ts.map +1 -1
  25. package/dist/commands/governor.js +4 -0
  26. package/dist/commands/ledger-v3.d.ts.map +1 -1
  27. package/dist/commands/ledger-v3.js +71 -2
  28. package/dist/commands/qa-record.d.ts +144 -0
  29. package/dist/commands/qa-record.d.ts.map +1 -0
  30. package/dist/commands/qa-record.js +0 -0
  31. package/dist/commands/qa-run.d.ts +8 -0
  32. package/dist/commands/qa-run.d.ts.map +1 -0
  33. package/dist/commands/qa-run.js +396 -0
  34. package/dist/commands/qa-status.d.ts +71 -0
  35. package/dist/commands/qa-status.d.ts.map +1 -0
  36. package/dist/commands/qa-status.js +490 -0
  37. package/dist/commands/qa-verify.d.ts +40 -0
  38. package/dist/commands/qa-verify.d.ts.map +1 -0
  39. package/dist/commands/qa-verify.js +180 -0
  40. package/dist/commands/resources.d.ts +4 -0
  41. package/dist/commands/resources.d.ts.map +1 -0
  42. package/dist/commands/resources.js +29 -0
  43. package/dist/commands/review-pack.d.ts +4 -0
  44. package/dist/commands/review-pack.d.ts.map +1 -0
  45. package/dist/commands/review-pack.js +1001 -0
  46. package/dist/commands/supervisor.d.ts +4 -0
  47. package/dist/commands/supervisor.d.ts.map +1 -0
  48. package/dist/commands/supervisor.js +101 -0
  49. package/dist/commands/web.d.ts.map +1 -1
  50. package/dist/commands/web.js +17 -0
  51. package/dist/commands/workflow.d.ts.map +1 -1
  52. package/dist/commands/workflow.js +4 -0
  53. package/dist/core/agents/cli.js +7 -3
  54. package/dist/core/agents/health.d.ts +17 -0
  55. package/dist/core/agents/health.d.ts.map +1 -0
  56. package/dist/core/agents/health.js +109 -0
  57. package/dist/core/agents/mailbox.d.ts +92 -0
  58. package/dist/core/agents/mailbox.d.ts.map +1 -0
  59. package/dist/core/agents/mailbox.js +325 -0
  60. package/dist/core/agents/qa-signal.d.ts +111 -0
  61. package/dist/core/agents/qa-signal.d.ts.map +1 -0
  62. package/dist/core/agents/qa-signal.js +231 -0
  63. package/dist/core/agents/reconcile-coordination-v3.d.ts +44 -0
  64. package/dist/core/agents/reconcile-coordination-v3.d.ts.map +1 -0
  65. package/dist/core/agents/reconcile-coordination-v3.js +58 -0
  66. package/dist/core/agents/render/prompt-context.d.ts +5 -0
  67. package/dist/core/agents/render/prompt-context.d.ts.map +1 -1
  68. package/dist/core/agents/render/prompt-context.js +34 -13
  69. package/dist/core/agents/render/session-context.d.ts +8 -0
  70. package/dist/core/agents/render/session-context.d.ts.map +1 -1
  71. package/dist/core/agents/render/session-context.js +34 -3
  72. package/dist/core/agents/rules/commit-conflict.d.ts.map +1 -1
  73. package/dist/core/agents/rules/commit-conflict.js +50 -1
  74. package/dist/core/agents/rules/stop-hook.d.ts.map +1 -1
  75. package/dist/core/agents/rules/stop-hook.js +6 -10
  76. package/dist/core/agents/session-finalizer-v3.js +4 -1
  77. package/dist/core/agents/session-name-display.d.ts +20 -5
  78. package/dist/core/agents/session-name-display.d.ts.map +1 -1
  79. package/dist/core/agents/session-name-display.js +67 -7
  80. package/dist/core/agents/state/heartbeat-reader.d.ts +7 -0
  81. package/dist/core/agents/state/heartbeat-reader.d.ts.map +1 -1
  82. package/dist/core/agents/state/heartbeat-writer.d.ts +11 -0
  83. package/dist/core/agents/state/heartbeat-writer.d.ts.map +1 -1
  84. package/dist/core/agents/state/heartbeat-writer.js +17 -0
  85. package/dist/core/agents/state/live-coordination-view.d.ts.map +1 -1
  86. package/dist/core/agents/state/live-coordination-view.js +1 -0
  87. package/dist/core/agents/state/live-coordination-writer.js +5 -0
  88. package/dist/core/artifacts/constants.d.ts +1 -1
  89. package/dist/core/artifacts/constants.js +1 -1
  90. package/dist/core/artifacts/index.d.ts +82 -6
  91. package/dist/core/artifacts/index.d.ts.map +1 -1
  92. package/dist/core/artifacts/index.js +410 -15
  93. package/dist/core/config.d.ts +20 -0
  94. package/dist/core/config.d.ts.map +1 -1
  95. package/dist/core/config.js +59 -0
  96. package/dist/core/diagnostics/advice.d.ts +9 -0
  97. package/dist/core/diagnostics/advice.d.ts.map +1 -0
  98. package/dist/core/diagnostics/advice.js +112 -0
  99. package/dist/core/diagnostics/bundle.d.ts +51 -0
  100. package/dist/core/diagnostics/bundle.d.ts.map +1 -0
  101. package/dist/core/diagnostics/bundle.js +968 -0
  102. package/dist/core/diagnostics/comparison.d.ts +5 -0
  103. package/dist/core/diagnostics/comparison.d.ts.map +1 -0
  104. package/dist/core/diagnostics/comparison.js +444 -0
  105. package/dist/core/diagnostics/contract.d.ts +245 -0
  106. package/dist/core/diagnostics/contract.d.ts.map +1 -0
  107. package/dist/core/diagnostics/contract.js +18 -0
  108. package/dist/core/diagnostics/identity.d.ts +3 -0
  109. package/dist/core/diagnostics/identity.d.ts.map +1 -0
  110. package/dist/core/diagnostics/identity.js +10 -0
  111. package/dist/core/diagnostics/index.d.ts +8 -0
  112. package/dist/core/diagnostics/index.d.ts.map +1 -0
  113. package/dist/core/diagnostics/index.js +7 -0
  114. package/dist/core/diagnostics/replay.d.ts +8 -0
  115. package/dist/core/diagnostics/replay.d.ts.map +1 -0
  116. package/dist/core/diagnostics/replay.js +213 -0
  117. package/dist/core/diagnostics/sanitize.d.ts +9 -0
  118. package/dist/core/diagnostics/sanitize.d.ts.map +1 -0
  119. package/dist/core/diagnostics/sanitize.js +84 -0
  120. package/dist/core/events/legacy-storage/compression.d.ts +13 -0
  121. package/dist/core/events/legacy-storage/compression.d.ts.map +1 -0
  122. package/dist/core/events/legacy-storage/compression.js +107 -0
  123. package/dist/core/events/legacy-storage/index.d.ts +1 -0
  124. package/dist/core/events/legacy-storage/index.d.ts.map +1 -1
  125. package/dist/core/events/legacy-storage/index.js +1 -0
  126. package/dist/core/events/v3/archive-retention.d.ts +33 -0
  127. package/dist/core/events/v3/archive-retention.d.ts.map +1 -0
  128. package/dist/core/events/v3/archive-retention.js +195 -0
  129. package/dist/core/events/v3/bootstrap.d.ts.map +1 -1
  130. package/dist/core/events/v3/bootstrap.js +10 -0
  131. package/dist/core/events/v3/capabilities.d.ts +1 -1
  132. package/dist/core/events/v3/capabilities.d.ts.map +1 -1
  133. package/dist/core/events/v3/capabilities.js +1 -0
  134. package/dist/core/events/v3/coordination-view.d.ts +3 -0
  135. package/dist/core/events/v3/coordination-view.d.ts.map +1 -1
  136. package/dist/core/events/v3/coordination-view.js +69 -8
  137. package/dist/core/events/v3/index.d.ts +1 -0
  138. package/dist/core/events/v3/index.d.ts.map +1 -1
  139. package/dist/core/events/v3/index.js +1 -0
  140. package/dist/core/events/v3/live-routing.d.ts.map +1 -1
  141. package/dist/core/events/v3/live-routing.js +1 -0
  142. package/dist/core/events/v3/producers/hook-base.d.ts +1 -1
  143. package/dist/core/events/v3/producers/hook-base.d.ts.map +1 -1
  144. package/dist/core/events/v3/producers/hook-base.js +13 -2
  145. package/dist/core/events/v3/producers/intake.d.ts.map +1 -1
  146. package/dist/core/events/v3/producers/intake.js +10 -2
  147. package/dist/core/events/v3/producers/recorder.d.ts +39 -0
  148. package/dist/core/events/v3/producers/recorder.d.ts.map +1 -1
  149. package/dist/core/events/v3/producers/recorder.js +350 -17
  150. package/dist/core/hooks/adapter/output.d.ts +8 -0
  151. package/dist/core/hooks/adapter/output.d.ts.map +1 -1
  152. package/dist/core/hooks/adapter/output.js +11 -1
  153. package/dist/core/hooks/cli.js +303 -219
  154. package/dist/core/hooks/health.d.ts +59 -0
  155. package/dist/core/hooks/health.d.ts.map +1 -0
  156. package/dist/core/hooks/health.js +84 -0
  157. package/dist/core/hooks/resolve/transcript.d.ts.map +1 -1
  158. package/dist/core/hooks/resolve/transcript.js +10 -3
  159. package/dist/core/hooks/session-name-presence.d.ts.map +1 -1
  160. package/dist/core/hooks/session-name-presence.js +4 -1
  161. package/dist/core/qa-artifacts.d.ts +20 -0
  162. package/dist/core/qa-artifacts.d.ts.map +1 -0
  163. package/dist/core/qa-artifacts.js +110 -0
  164. package/dist/core/resources/contract.d.ts +96 -0
  165. package/dist/core/resources/contract.d.ts.map +1 -0
  166. package/dist/core/resources/contract.js +2 -0
  167. package/dist/core/resources/index.d.ts +6 -0
  168. package/dist/core/resources/index.d.ts.map +1 -0
  169. package/dist/core/resources/index.js +5 -0
  170. package/dist/core/resources/sampler.d.ts +34 -0
  171. package/dist/core/resources/sampler.d.ts.map +1 -0
  172. package/dist/core/resources/sampler.js +458 -0
  173. package/dist/core/resources/service-status.d.ts +3 -0
  174. package/dist/core/resources/service-status.d.ts.map +1 -0
  175. package/dist/core/resources/service-status.js +48 -0
  176. package/dist/core/resources/service.d.ts +26 -0
  177. package/dist/core/resources/service.d.ts.map +1 -0
  178. package/dist/core/resources/service.js +292 -0
  179. package/dist/core/resources/storage.d.ts +13 -0
  180. package/dist/core/resources/storage.d.ts.map +1 -0
  181. package/dist/core/resources/storage.js +30 -0
  182. package/dist/core/storage/atomic-json.d.ts +3 -0
  183. package/dist/core/storage/atomic-json.d.ts.map +1 -0
  184. package/dist/core/storage/atomic-json.js +15 -0
  185. package/dist/core/storage/builtins.d.ts.map +1 -1
  186. package/dist/core/storage/builtins.js +45 -6
  187. package/dist/core/storage/logger.d.ts +2 -0
  188. package/dist/core/storage/logger.d.ts.map +1 -1
  189. package/dist/core/storage/logger.js +2 -0
  190. package/dist/core/storage/query.d.ts +14 -0
  191. package/dist/core/storage/query.d.ts.map +1 -1
  192. package/dist/core/storage/query.js +51 -0
  193. package/dist/core/supervisor/activity.d.ts +4 -0
  194. package/dist/core/supervisor/activity.d.ts.map +1 -0
  195. package/dist/core/supervisor/activity.js +141 -0
  196. package/dist/core/supervisor/contract.d.ts +268 -0
  197. package/dist/core/supervisor/contract.d.ts.map +1 -0
  198. package/dist/core/supervisor/contract.js +35 -0
  199. package/dist/core/supervisor/explanations.d.ts +4 -0
  200. package/dist/core/supervisor/explanations.d.ts.map +1 -0
  201. package/dist/core/supervisor/explanations.js +71 -0
  202. package/dist/core/supervisor/findings.d.ts +20 -0
  203. package/dist/core/supervisor/findings.d.ts.map +1 -0
  204. package/dist/core/supervisor/findings.js +399 -0
  205. package/dist/core/supervisor/history.d.ts +9 -0
  206. package/dist/core/supervisor/history.d.ts.map +1 -0
  207. package/dist/core/supervisor/history.js +46 -0
  208. package/dist/core/supervisor/hook-health-alerts.d.ts +14 -0
  209. package/dist/core/supervisor/hook-health-alerts.d.ts.map +1 -0
  210. package/dist/core/supervisor/hook-health-alerts.js +70 -0
  211. package/dist/core/supervisor/hook-health-storage.d.ts +5 -0
  212. package/dist/core/supervisor/hook-health-storage.d.ts.map +1 -0
  213. package/dist/core/supervisor/hook-health-storage.js +24 -0
  214. package/dist/core/supervisor/hook-health.d.ts +72 -0
  215. package/dist/core/supervisor/hook-health.d.ts.map +1 -0
  216. package/dist/core/supervisor/hook-health.js +203 -0
  217. package/dist/core/supervisor/hooks.d.ts +10 -0
  218. package/dist/core/supervisor/hooks.d.ts.map +1 -0
  219. package/dist/core/supervisor/hooks.js +28 -0
  220. package/dist/core/supervisor/index.d.ts +15 -0
  221. package/dist/core/supervisor/index.d.ts.map +1 -0
  222. package/dist/core/supervisor/index.js +14 -0
  223. package/dist/core/supervisor/log-feed.d.ts +9 -0
  224. package/dist/core/supervisor/log-feed.d.ts.map +1 -0
  225. package/dist/core/supervisor/log-feed.js +103 -0
  226. package/dist/core/supervisor/service.d.ts +29 -0
  227. package/dist/core/supervisor/service.d.ts.map +1 -0
  228. package/dist/core/supervisor/service.js +450 -0
  229. package/dist/core/supervisor/services.d.ts +11 -0
  230. package/dist/core/supervisor/services.d.ts.map +1 -0
  231. package/dist/core/supervisor/services.js +155 -0
  232. package/dist/core/supervisor/status.d.ts +3 -0
  233. package/dist/core/supervisor/status.d.ts.map +1 -0
  234. package/dist/core/supervisor/status.js +48 -0
  235. package/dist/core/supervisor/storage.d.ts +30 -0
  236. package/dist/core/supervisor/storage.d.ts.map +1 -0
  237. package/dist/core/supervisor/storage.js +73 -0
  238. package/dist/core/supervisor/timeline.d.ts +3 -0
  239. package/dist/core/supervisor/timeline.d.ts.map +1 -0
  240. package/dist/core/supervisor/timeline.js +87 -0
  241. package/dist/core/workflow/admission.d.ts +23 -0
  242. package/dist/core/workflow/admission.d.ts.map +1 -0
  243. package/dist/core/workflow/admission.js +136 -0
  244. package/dist/core/workflow/engine.d.ts.map +1 -1
  245. package/dist/core/workflow/engine.js +77 -2
  246. package/dist/core/workflow/index.d.ts +3 -2
  247. package/dist/core/workflow/index.d.ts.map +1 -1
  248. package/dist/core/workflow/index.js +2 -1
  249. package/dist/core/workflow/proof.d.ts +2 -1
  250. package/dist/core/workflow/proof.d.ts.map +1 -1
  251. package/dist/core/workflow/proof.js +39 -1
  252. package/dist/core/workflow/run-state.d.ts +3 -2
  253. package/dist/core/workflow/run-state.d.ts.map +1 -1
  254. package/dist/core/workflow/run-state.js +8 -1
  255. package/dist/core/workflow/types.d.ts +33 -1
  256. package/dist/core/workflow/types.d.ts.map +1 -1
  257. package/dist/core/workflow/types.js +2 -1
  258. package/dist/lib/admission.d.ts +71 -0
  259. package/dist/lib/admission.d.ts.map +1 -0
  260. package/dist/lib/admission.js +264 -0
  261. package/dist/lib/agent-browser/client.d.ts +1 -1
  262. package/dist/lib/agent-browser/client.d.ts.map +1 -1
  263. package/dist/lib/agent-browser/client.js +1 -5
  264. package/dist/lib/browser/capture-fidelity.d.ts +39 -0
  265. package/dist/lib/browser/capture-fidelity.d.ts.map +1 -0
  266. package/dist/lib/browser/capture-fidelity.js +84 -0
  267. package/dist/lib/browser/client.d.ts +46 -1
  268. package/dist/lib/browser/client.d.ts.map +1 -1
  269. package/dist/lib/browser/client.js +168 -9
  270. package/dist/lib/browser/critique.d.ts +38 -1
  271. package/dist/lib/browser/critique.d.ts.map +1 -1
  272. package/dist/lib/browser/critique.js +34 -6
  273. package/dist/lib/browser/index.d.ts +5 -1
  274. package/dist/lib/browser/index.d.ts.map +1 -1
  275. package/dist/lib/browser/index.js +4 -0
  276. package/dist/lib/browser/page-review-judge.d.ts +64 -0
  277. package/dist/lib/browser/page-review-judge.d.ts.map +1 -0
  278. package/dist/lib/browser/page-review-judge.js +270 -0
  279. package/dist/lib/browser/page-review-pack.d.ts +613 -0
  280. package/dist/lib/browser/page-review-pack.d.ts.map +1 -0
  281. package/dist/lib/browser/page-review-pack.js +1751 -0
  282. package/dist/lib/browser/qa-run-contracts.d.ts +377 -0
  283. package/dist/lib/browser/qa-run-contracts.d.ts.map +1 -0
  284. package/dist/lib/browser/qa-run-contracts.js +361 -0
  285. package/dist/lib/browser/qa-run.d.ts +141 -0
  286. package/dist/lib/browser/qa-run.d.ts.map +1 -0
  287. package/dist/lib/browser/qa-run.js +1283 -0
  288. package/dist/lib/browser/request-diagnostics.d.ts +13 -0
  289. package/dist/lib/browser/request-diagnostics.d.ts.map +1 -0
  290. package/dist/lib/browser/request-diagnostics.js +18 -0
  291. package/dist/lib/browser/tiling.d.ts +19 -0
  292. package/dist/lib/browser/tiling.d.ts.map +1 -1
  293. package/dist/lib/browser/tiling.js +28 -0
  294. package/dist/lib/cookies/client.d.ts +9 -0
  295. package/dist/lib/cookies/client.d.ts.map +1 -1
  296. package/dist/lib/cookies/client.js +197 -44
  297. package/dist/lib/cookies/extra.d.ts +18 -0
  298. package/dist/lib/cookies/extra.d.ts.map +1 -0
  299. package/dist/lib/cookies/extra.js +14 -0
  300. package/dist/lib/cookies/index.d.ts +2 -1
  301. package/dist/lib/cookies/index.d.ts.map +1 -1
  302. package/dist/lib/cookies/index.js +2 -1
  303. package/dist/lib/coord-root-id.d.ts +9 -0
  304. package/dist/lib/coord-root-id.d.ts.map +1 -0
  305. package/dist/lib/coord-root-id.js +17 -0
  306. package/dist/lib/durable-job.d.ts +124 -0
  307. package/dist/lib/durable-job.d.ts.map +1 -0
  308. package/dist/lib/durable-job.js +296 -0
  309. package/dist/lib/http/client.d.ts +7 -1
  310. package/dist/lib/http/client.d.ts.map +1 -1
  311. package/dist/lib/http/client.js +2 -0
  312. package/dist/lib/instructions/apply.d.ts +3 -1
  313. package/dist/lib/instructions/apply.d.ts.map +1 -1
  314. package/dist/lib/instructions/apply.js +74 -3
  315. package/dist/lib/instructions/templates.d.ts.map +1 -1
  316. package/dist/lib/instructions/templates.js +11 -2
  317. package/package.json +19 -2
  318. package/src/commander.ts +75 -1
  319. package/src/commands/admission.ts +699 -0
  320. package/src/commands/agents.ts +189 -50
  321. package/src/commands/artifacts.ts +132 -18
  322. package/src/commands/browse-ai.ts +10 -5
  323. package/src/commands/browse.ts +496 -20
  324. package/src/commands/diagnostics.ts +271 -0
  325. package/src/commands/fetch.ts +1 -0
  326. package/src/commands/files.ts +102 -2
  327. package/src/commands/governor.ts +8 -0
  328. package/src/commands/ledger-v3.ts +82 -0
  329. package/src/commands/qa-record.ts +682 -0
  330. package/src/commands/qa-run.ts +503 -0
  331. package/src/commands/qa-status.ts +608 -0
  332. package/src/commands/qa-verify.ts +238 -0
  333. package/src/commands/resources.ts +38 -0
  334. package/src/commands/review-pack.ts +1281 -0
  335. package/src/commands/supervisor.ts +141 -0
  336. package/src/commands/web.ts +19 -0
  337. package/src/commands/workflow.ts +8 -0
  338. package/src/core/agents/cli.ts +7 -3
  339. package/src/core/agents/health.ts +142 -0
  340. package/src/core/agents/mailbox.ts +373 -0
  341. package/src/core/agents/qa-signal.ts +261 -0
  342. package/src/core/agents/reconcile-coordination-v3.ts +77 -0
  343. package/src/core/agents/render/prompt-context.ts +42 -12
  344. package/src/core/agents/render/session-context.ts +37 -3
  345. package/src/core/agents/rules/commit-conflict.ts +58 -1
  346. package/src/core/agents/rules/stop-hook.ts +6 -12
  347. package/src/core/agents/session-finalizer-v3.ts +4 -1
  348. package/src/core/agents/session-name-display.ts +78 -7
  349. package/src/core/agents/state/heartbeat-reader.ts +7 -0
  350. package/src/core/agents/state/heartbeat-writer.ts +23 -0
  351. package/src/core/agents/state/live-coordination-view.ts +1 -0
  352. package/src/core/agents/state/live-coordination-writer.ts +5 -0
  353. package/src/core/artifacts/constants.ts +1 -1
  354. package/src/core/artifacts/index.ts +565 -30
  355. package/src/core/config.ts +134 -2
  356. package/src/core/diagnostics/advice.ts +147 -0
  357. package/src/core/diagnostics/bundle.ts +1250 -0
  358. package/src/core/diagnostics/comparison.ts +565 -0
  359. package/src/core/diagnostics/contract.ts +311 -0
  360. package/src/core/diagnostics/identity.ts +11 -0
  361. package/src/core/diagnostics/index.ts +7 -0
  362. package/src/core/diagnostics/replay.ts +314 -0
  363. package/src/core/diagnostics/sanitize.ts +97 -0
  364. package/src/core/events/legacy-storage/compression.ts +130 -0
  365. package/src/core/events/legacy-storage/index.ts +1 -0
  366. package/src/core/events/v3/archive-retention.ts +251 -0
  367. package/src/core/events/v3/bootstrap.ts +10 -0
  368. package/src/core/events/v3/capabilities.ts +2 -0
  369. package/src/core/events/v3/coordination-view.ts +100 -11
  370. package/src/core/events/v3/index.ts +9 -0
  371. package/src/core/events/v3/live-routing.ts +1 -0
  372. package/src/core/events/v3/producers/hook-base.ts +14 -2
  373. package/src/core/events/v3/producers/intake.ts +10 -2
  374. package/src/core/events/v3/producers/recorder.ts +420 -17
  375. package/src/core/hooks/adapter/output.ts +12 -1
  376. package/src/core/hooks/cli.ts +332 -247
  377. package/src/core/hooks/health.ts +146 -0
  378. package/src/core/hooks/resolve/transcript.ts +10 -3
  379. package/src/core/hooks/session-name-presence.ts +6 -1
  380. package/src/core/qa-artifacts.ts +126 -0
  381. package/src/core/resources/contract.ts +105 -0
  382. package/src/core/resources/index.ts +5 -0
  383. package/src/core/resources/sampler.ts +590 -0
  384. package/src/core/resources/service-status.ts +62 -0
  385. package/src/core/resources/service.ts +337 -0
  386. package/src/core/resources/storage.ts +44 -0
  387. package/src/core/storage/atomic-json.ts +15 -0
  388. package/src/core/storage/builtins.ts +62 -6
  389. package/src/core/storage/logger.ts +2 -0
  390. package/src/core/storage/query.ts +58 -0
  391. package/src/core/supervisor/activity.ts +193 -0
  392. package/src/core/supervisor/contract.ts +312 -0
  393. package/src/core/supervisor/explanations.ts +104 -0
  394. package/src/core/supervisor/findings.ts +788 -0
  395. package/src/core/supervisor/history.ts +61 -0
  396. package/src/core/supervisor/hook-health-alerts.ts +108 -0
  397. package/src/core/supervisor/hook-health-storage.ts +25 -0
  398. package/src/core/supervisor/hook-health.ts +301 -0
  399. package/src/core/supervisor/hooks.ts +35 -0
  400. package/src/core/supervisor/index.ts +14 -0
  401. package/src/core/supervisor/log-feed.ts +131 -0
  402. package/src/core/supervisor/service.ts +511 -0
  403. package/src/core/supervisor/services.ts +203 -0
  404. package/src/core/supervisor/status.ts +59 -0
  405. package/src/core/supervisor/storage.ts +126 -0
  406. package/src/core/supervisor/timeline.ts +133 -0
  407. package/src/core/workflow/admission.ts +215 -0
  408. package/src/core/workflow/engine.ts +93 -1
  409. package/src/core/workflow/index.ts +10 -0
  410. package/src/core/workflow/proof.ts +54 -1
  411. package/src/core/workflow/run-state.ts +13 -1
  412. package/src/core/workflow/types.ts +39 -1
  413. package/src/lib/admission.ts +347 -0
  414. package/src/lib/agent-browser/client.ts +2 -10
  415. package/src/lib/browser/capture-fidelity.ts +98 -0
  416. package/src/lib/browser/client.ts +212 -10
  417. package/src/lib/browser/critique.ts +62 -7
  418. package/src/lib/browser/index.ts +65 -0
  419. package/src/lib/browser/page-review-judge.ts +360 -0
  420. package/src/lib/browser/page-review-pack.ts +2384 -0
  421. package/src/lib/browser/qa-run-contracts.ts +743 -0
  422. package/src/lib/browser/qa-run.ts +1462 -0
  423. package/src/lib/browser/request-diagnostics.ts +27 -0
  424. package/src/lib/browser/tiling.ts +32 -0
  425. package/src/lib/cookies/client.ts +228 -42
  426. package/src/lib/cookies/extra.ts +28 -0
  427. package/src/lib/cookies/index.ts +2 -0
  428. package/src/lib/coord-root-id.ts +23 -0
  429. package/src/lib/durable-job.ts +407 -0
  430. package/src/lib/http/client.ts +13 -1
  431. package/src/lib/instructions/apply.ts +81 -2
  432. package/src/lib/instructions/templates.ts +11 -2
@@ -9,7 +9,7 @@
9
9
  */
10
10
 
11
11
  import { spawnSync } from "node:child_process";
12
- import { randomUUID } from "node:crypto";
12
+ import { createHash, randomUUID } from "node:crypto";
13
13
  import type { Stats } from "node:fs";
14
14
  import {
15
15
  existsSync,
@@ -18,6 +18,7 @@ import {
18
18
  readdirSync,
19
19
  readFileSync,
20
20
  renameSync,
21
+ rmdirSync,
21
22
  rmSync,
22
23
  writeFileSync,
23
24
  } from "node:fs";
@@ -27,6 +28,8 @@ import {
27
28
  artifactAutoCleanEnabled,
28
29
  artifactAutoCleanIntervalHours,
29
30
  artifactDefaultRetentionDays,
31
+ artifactMaxBytes,
32
+ artifactMaxUnitBytes,
30
33
  } from "../config.ts";
31
34
  import { ARTIFACT_MANIFEST, ARTIFACT_SCHEMA_VERSION, ARTIFACTS_DIR } from "./constants.ts";
32
35
 
@@ -38,8 +41,9 @@ export interface ArtifactActor {
38
41
  name?: string;
39
42
  }
40
43
 
41
- export interface ArtifactManifestV1 {
44
+ export interface ArtifactManifestV2 {
42
45
  schema_version: typeof ARTIFACT_SCHEMA_VERSION;
46
+ holds: ArtifactHold[];
43
47
  artifact_id: string;
44
48
  slug: string;
45
49
  purpose: string;
@@ -52,12 +56,38 @@ export interface ArtifactManifestV1 {
52
56
  };
53
57
  released_at?: string;
54
58
  released_by?: ArtifactActor;
59
+ oversize_acknowledged?: boolean;
60
+ }
61
+
62
+ export interface ArtifactHold {
63
+ id: string;
64
+ reason: string;
65
+ set_by: ArtifactActor;
66
+ set_at: string;
67
+ }
68
+
69
+ export interface ArtifactHoldInput {
70
+ id: string;
71
+ reason: string;
72
+ }
73
+
74
+ export function artifactCapabilities() {
75
+ return {
76
+ schema_version: ARTIFACT_SCHEMA_VERSION,
77
+ holds: true,
78
+ atomic_create_holds: true,
79
+ owner_scoped_unhold: true,
80
+ explicit_v1_migration: true,
81
+ } as const;
55
82
  }
56
83
 
57
84
  export type ArtifactClassification =
85
+ | "managed-held"
58
86
  | "managed-active"
59
87
  | "managed-current"
60
88
  | "managed-expired"
89
+ | "managed-oversize"
90
+ | "managed-over-budget"
61
91
  | "managed-tracked"
62
92
  | "invalid-manifest"
63
93
  | "unmanaged"
@@ -78,15 +108,22 @@ export interface ArtifactInventoryEntry {
78
108
  last_modified_at: string | null;
79
109
  expires_at: string | null;
80
110
  owner_instance_id: string | null;
111
+ oversize_acknowledged: boolean;
81
112
  }
82
113
 
83
114
  export interface ArtifactCreateInput {
84
115
  slug: string;
85
116
  purpose: string;
86
117
  retentionDays: number;
118
+ /** Sub-day retention. When set, it replaces `retentionDays` for this unit
119
+ * (a page review pack expires in minutes, not days). 1 to 5,256,000. */
120
+ retentionMinutes?: number;
87
121
  actor?: ArtifactActor;
122
+ /** Holds are persisted with the first manifest; a valid actor is required. */
123
+ holds?: ArtifactHoldInput[];
88
124
  now?: Date;
89
125
  id?: string;
126
+ big?: boolean;
90
127
  }
91
128
 
92
129
  export interface ArtifactMutationInput {
@@ -94,9 +131,18 @@ export interface ArtifactMutationInput {
94
131
  now?: Date;
95
132
  }
96
133
 
134
+ export interface ArtifactAdoptionResult {
135
+ candidates: Array<{ path: string; name: string; bytes: number; kind: "file" | "directory" }>;
136
+ candidate_bytes: number;
137
+ requires_big: boolean;
138
+ adopted_artifact_id: string | null;
139
+ adopted_path: string | null;
140
+ adopted_directories: number;
141
+ }
142
+
97
143
  interface ParsedManifest {
98
144
  ok: true;
99
- manifest: ArtifactManifestV1;
145
+ manifest: ArtifactManifestV2;
100
146
  }
101
147
 
102
148
  interface ManifestError {
@@ -116,18 +162,32 @@ export function configuredArtifactRetentionDays(repoRoot: string): number {
116
162
  export function createArtifact(
117
163
  repoRoot: string,
118
164
  input: ArtifactCreateInput,
119
- ): { path: string; manifest: ArtifactManifestV1 } {
165
+ ): { path: string; manifest: ArtifactManifestV2 } {
166
+ return withArtifactLock(repoRoot, () => createArtifactUnlocked(repoRoot, input));
167
+ }
168
+
169
+ function createArtifactUnlocked(
170
+ repoRoot: string,
171
+ input: ArtifactCreateInput,
172
+ ): { path: string; manifest: ArtifactManifestV2 } {
120
173
  const now = input.now ?? new Date();
121
174
  assertValidDate(now, "now");
122
175
  const slug = normalizeSlug(input.slug);
123
176
  if (!slug) throw new Error("slug must contain at least one ASCII letter or digit");
124
177
  const purpose = input.purpose.trim();
125
178
  if (!purpose) throw new Error("purpose must not be empty");
126
- const retentionDays = positiveDays(input.retentionDays);
179
+ const expiresAt =
180
+ input.retentionMinutes !== undefined
181
+ ? addMinutes(now, positiveMinutes(input.retentionMinutes))
182
+ : addDays(now, positiveDays(input.retentionDays));
127
183
  const artifactId = input.id ?? randomUUID();
128
184
  if (!isSafeId(artifactId)) {
129
185
  throw new Error("artifact id must use ASCII letters, digits, hyphens, or underscores");
130
186
  }
187
+ const holds = (input.holds ?? []).map((hold) => makeHold(hold, input.actor, now));
188
+ if (new Set(holds.map((hold) => hold.id)).size !== holds.length) {
189
+ throw new Error("duplicate hold id");
190
+ }
131
191
 
132
192
  const root = artifactsRoot(repoRoot);
133
193
  mkdirSync(root, { recursive: true });
@@ -135,16 +195,18 @@ export function createArtifact(
135
195
  const path = join(root, `${date}_${slug}_${artifactId.slice(0, 8)}`);
136
196
  mkdirSync(path);
137
197
 
138
- const manifest: ArtifactManifestV1 = {
198
+ const manifest: ArtifactManifestV2 = {
139
199
  schema_version: ARTIFACT_SCHEMA_VERSION,
200
+ holds,
140
201
  artifact_id: artifactId,
141
202
  slug,
142
203
  purpose,
143
204
  created_at: now.toISOString(),
144
205
  created_by: input.actor,
145
206
  retention: {
146
- expires_at: addDays(now, retentionDays).toISOString(),
207
+ expires_at: expiresAt.toISOString(),
147
208
  },
209
+ ...(input.big ? { oversize_acknowledged: true } : {}),
148
210
  };
149
211
  atomicWriteManifest(path, manifest);
150
212
  return { path, manifest };
@@ -177,21 +239,17 @@ export function inventoryArtifacts(
177
239
  for (const name of names) {
178
240
  rows.push(classifyArtifactPath(repoRoot, join(root, name), now, freshnessSeconds));
179
241
  }
180
- return rows;
242
+ return applyArtifactBudgets(repoRoot, rows);
181
243
  }
182
244
 
183
245
  export function showArtifact(
184
246
  repoRoot: string,
185
247
  ref: string,
186
248
  opts: { now?: Date; freshnessSeconds?: number } = {},
187
- ): { entry: ArtifactInventoryEntry; manifest: ArtifactManifestV1 } {
249
+ ): { entry: ArtifactInventoryEntry; manifest: ArtifactManifestV2 } {
188
250
  const path = resolveArtifactRef(repoRoot, ref);
189
- const entry = classifyArtifactPath(
190
- repoRoot,
191
- path,
192
- opts.now ?? new Date(),
193
- opts.freshnessSeconds ?? 600,
194
- );
251
+ const entry = inventoryArtifacts(repoRoot, opts).find((row) => row.path === path);
252
+ if (!entry) throw new Error(`artifact "${ref}" was not found`);
195
253
  const parsed = readManifest(path);
196
254
  if (!parsed.ok) throw new Error(parsed.reason);
197
255
  return { entry, manifest: parsed.manifest };
@@ -203,7 +261,19 @@ export function renewArtifact(
203
261
  days: number,
204
262
  reason: string,
205
263
  input: ArtifactMutationInput = {},
206
- ): ArtifactManifestV1 {
264
+ ): ArtifactManifestV2 {
265
+ return withArtifactLock(repoRoot, () =>
266
+ renewArtifactUnlocked(repoRoot, ref, days, reason, input),
267
+ );
268
+ }
269
+
270
+ function renewArtifactUnlocked(
271
+ repoRoot: string,
272
+ ref: string,
273
+ days: number,
274
+ reason: string,
275
+ input: ArtifactMutationInput,
276
+ ): ArtifactManifestV2 {
207
277
  const now = input.now ?? new Date();
208
278
  assertValidDate(now, "now");
209
279
  const retentionDays = positiveDays(days);
@@ -212,7 +282,7 @@ export function renewArtifact(
212
282
  const path = resolveArtifactRef(repoRoot, ref);
213
283
  const parsed = readManifest(path);
214
284
  if (!parsed.ok) throw new Error(parsed.reason);
215
- const manifest: ArtifactManifestV1 = {
285
+ const manifest: ArtifactManifestV2 = {
216
286
  ...parsed.manifest,
217
287
  retention: {
218
288
  expires_at: addDays(now, retentionDays).toISOString(),
@@ -228,13 +298,21 @@ export function releaseArtifact(
228
298
  repoRoot: string,
229
299
  ref: string,
230
300
  input: ArtifactMutationInput = {},
231
- ): ArtifactManifestV1 {
301
+ ): ArtifactManifestV2 {
302
+ return withArtifactLock(repoRoot, () => releaseArtifactUnlocked(repoRoot, ref, input));
303
+ }
304
+
305
+ function releaseArtifactUnlocked(
306
+ repoRoot: string,
307
+ ref: string,
308
+ input: ArtifactMutationInput,
309
+ ): ArtifactManifestV2 {
232
310
  const now = input.now ?? new Date();
233
311
  assertValidDate(now, "now");
234
312
  const path = resolveArtifactRef(repoRoot, ref);
235
313
  const parsed = readManifest(path);
236
314
  if (!parsed.ok) throw new Error(parsed.reason);
237
- const manifest: ArtifactManifestV1 = {
315
+ const manifest: ArtifactManifestV2 = {
238
316
  ...parsed.manifest,
239
317
  released_at: now.toISOString(),
240
318
  released_by: input.actor,
@@ -243,9 +321,201 @@ export function releaseArtifact(
243
321
  return manifest;
244
322
  }
245
323
 
324
+ export function holdArtifact(
325
+ repoRoot: string,
326
+ ref: string,
327
+ input: ArtifactHoldInput & { actor: ArtifactActor; now?: Date },
328
+ ): ArtifactManifestV2 {
329
+ const hold = makeHold(input, input.actor, input.now ?? new Date());
330
+ return withArtifactLock(repoRoot, () => {
331
+ const path = resolveArtifactRef(repoRoot, ref);
332
+ const parsed = readManifest(path);
333
+ if (!parsed.ok) throw new Error(parsed.reason);
334
+ const previous = parsed.manifest.holds.find((item) => item.id === hold.id);
335
+ if (previous) {
336
+ if (
337
+ previous.set_by.instance_id !== input.actor.instance_id ||
338
+ previous.reason !== hold.reason
339
+ ) {
340
+ throw new Error("hold id already exists with a different owner or reason");
341
+ }
342
+ return parsed.manifest;
343
+ }
344
+ const manifest = { ...parsed.manifest, holds: [...parsed.manifest.holds, hold] };
345
+ atomicWriteManifest(path, manifest);
346
+ return manifest;
347
+ });
348
+ }
349
+
350
+ export function unholdArtifact(
351
+ repoRoot: string,
352
+ ref: string,
353
+ id: string,
354
+ input: { actor: ArtifactActor; now?: Date },
355
+ ): ArtifactManifestV2 {
356
+ if (!validHoldId(id)) throw new Error("invalid hold id");
357
+ if (!validActor(input.actor)) throw new Error("a valid hold actor is required");
358
+ return withArtifactLock(repoRoot, () => {
359
+ const path = resolveArtifactRef(repoRoot, ref);
360
+ const parsed = readManifest(path);
361
+ if (!parsed.ok) throw new Error(parsed.reason);
362
+ const hold = parsed.manifest.holds.find((item) => item.id === id);
363
+ if (!hold) return parsed.manifest;
364
+ if (hold.set_by.instance_id !== input.actor.instance_id) {
365
+ throw new Error("only the hold owner may remove this hold");
366
+ }
367
+ const manifest = {
368
+ ...parsed.manifest,
369
+ holds: parsed.manifest.holds.filter((item) => item.id !== id),
370
+ };
371
+ atomicWriteManifest(path, manifest);
372
+ return manifest;
373
+ });
374
+ }
375
+
376
+ /** Adopt untracked loose files and legacy directories without changing directory paths. */
377
+ export function adoptUnmanagedArtifactFiles(
378
+ repoRoot: string,
379
+ input: {
380
+ yes?: boolean;
381
+ big?: boolean;
382
+ purpose: string;
383
+ retentionDays: number;
384
+ actor?: ArtifactActor;
385
+ now?: Date;
386
+ },
387
+ ): ArtifactAdoptionResult {
388
+ if (input.yes)
389
+ return withArtifactLock(repoRoot, () => adoptUnmanagedArtifactFilesUnlocked(repoRoot, input));
390
+ return adoptUnmanagedArtifactFilesUnlocked(repoRoot, input);
391
+ }
392
+
393
+ function adoptUnmanagedArtifactFilesUnlocked(
394
+ repoRoot: string,
395
+ input: {
396
+ yes?: boolean;
397
+ big?: boolean;
398
+ purpose: string;
399
+ retentionDays: number;
400
+ actor?: ArtifactActor;
401
+ now?: Date;
402
+ },
403
+ ): ArtifactAdoptionResult {
404
+ const now = input.now ?? new Date();
405
+ const candidates: ArtifactAdoptionResult["candidates"] = [];
406
+ for (const row of inventoryArtifacts(repoRoot, { now })) {
407
+ if (row.classification !== "unmanaged") continue;
408
+ try {
409
+ const stat = lstatSync(row.path);
410
+ if (stat.isSymbolicLink() || containsTrackedPath(repoRoot, row.path)) continue;
411
+ if (stat.isFile()) {
412
+ candidates.push({ path: row.path, name: row.name, bytes: stat.size, kind: "file" });
413
+ continue;
414
+ }
415
+ if (stat.isDirectory() && !existsSync(join(row.path, ARTIFACT_MANIFEST))) {
416
+ const bytes = safeTreeSize(row.path);
417
+ if (bytes !== null && normalizeSlug(row.name)) {
418
+ candidates.push({ path: row.path, name: row.name, bytes, kind: "directory" });
419
+ }
420
+ }
421
+ } catch {
422
+ // A racing or unreadable entry stays unmanaged.
423
+ }
424
+ }
425
+ const candidateBytes = candidates.reduce((sum, row) => sum + row.bytes, 0);
426
+ const fileBytes = candidates.reduce((sum, row) => sum + (row.kind === "file" ? row.bytes : 0), 0);
427
+ const maxUnitBytes = artifactMaxUnitBytes(repoRoot);
428
+ const requiresBig =
429
+ fileBytes > maxUnitBytes ||
430
+ candidates.some((row) => row.kind === "directory" && row.bytes > maxUnitBytes);
431
+ const preview: ArtifactAdoptionResult = {
432
+ candidates,
433
+ candidate_bytes: candidateBytes,
434
+ requires_big: requiresBig,
435
+ adopted_artifact_id: null,
436
+ adopted_path: null,
437
+ adopted_directories: 0,
438
+ };
439
+ if (!input.yes || candidates.length === 0) return preview;
440
+ if (requiresBig && !input.big) {
441
+ throw new Error("unmanaged adoption exceeds the per-bundle ceiling; repeat with --big");
442
+ }
443
+
444
+ // Revalidate every exact source before creating a destination. A changed,
445
+ // tracked, linked, or non-regular entry aborts the whole adoption.
446
+ for (const candidate of candidates) {
447
+ const stat = lstatSync(candidate.path);
448
+ const bytes = stat.isDirectory() ? safeTreeSize(candidate.path) : stat.size;
449
+ if (
450
+ stat.isSymbolicLink() ||
451
+ bytes !== candidate.bytes ||
452
+ containsTrackedPath(repoRoot, candidate.path)
453
+ ) {
454
+ throw new Error(`unmanaged entry changed before adoption: ${candidate.name}`);
455
+ }
456
+ if (candidate.kind === "file" ? !stat.isFile() : !stat.isDirectory()) {
457
+ throw new Error(`unmanaged entry changed before adoption: ${candidate.name}`);
458
+ }
459
+ }
460
+ const files = candidates.filter((candidate) => candidate.kind === "file");
461
+ const created = files.length
462
+ ? createArtifactUnlocked(repoRoot, {
463
+ slug: "adopted-unmanaged",
464
+ purpose: input.purpose,
465
+ retentionDays: input.retentionDays,
466
+ actor: input.actor,
467
+ now,
468
+ big: input.big,
469
+ })
470
+ : null;
471
+ for (const candidate of files) renameSync(candidate.path, join(created!.path, candidate.name));
472
+ const retentionDays = positiveDays(input.retentionDays);
473
+ const directories = candidates.filter((candidate) => candidate.kind === "directory");
474
+ for (const candidate of directories) {
475
+ atomicWriteManifest(candidate.path, {
476
+ schema_version: ARTIFACT_SCHEMA_VERSION,
477
+ holds: [],
478
+ artifact_id: randomUUID(),
479
+ slug: normalizeSlug(candidate.name),
480
+ purpose: `${input.purpose}: ${candidate.name}`,
481
+ created_at: now.toISOString(),
482
+ created_by: input.actor,
483
+ retention: { expires_at: addDays(now, retentionDays).toISOString() },
484
+ ...(input.big ? { oversize_acknowledged: true } : {}),
485
+ });
486
+ }
487
+ return {
488
+ ...preview,
489
+ adopted_artifact_id: created?.manifest.artifact_id ?? null,
490
+ adopted_path: created?.path ?? null,
491
+ adopted_directories: directories.length,
492
+ };
493
+ }
494
+
246
495
  export function cleanArtifacts(
247
496
  repoRoot: string,
248
497
  opts: { yes?: boolean; now?: Date; freshnessSeconds?: number } = {},
498
+ ): ArtifactInventoryEntry[] {
499
+ if (!opts.yes) return inventoryArtifacts(repoRoot, opts);
500
+ try {
501
+ return withArtifactLock(repoRoot, () => cleanArtifactsUnlocked(repoRoot, opts));
502
+ } catch (error) {
503
+ return inventoryArtifacts(repoRoot, opts).map((row) =>
504
+ row.action === "would-delete"
505
+ ? {
506
+ ...row,
507
+ classification: "unknown",
508
+ action: "keep",
509
+ reason: errorMessage("cleanup refused", error),
510
+ }
511
+ : row,
512
+ );
513
+ }
514
+ }
515
+
516
+ function cleanArtifactsUnlocked(
517
+ repoRoot: string,
518
+ opts: { yes?: boolean; now?: Date; freshnessSeconds?: number },
249
519
  ): ArtifactInventoryEntry[] {
250
520
  const now = opts.now ?? new Date();
251
521
  const freshnessSeconds = opts.freshnessSeconds ?? 600;
@@ -253,11 +523,24 @@ export function cleanArtifacts(
253
523
  if (!opts.yes) return rows;
254
524
 
255
525
  return rows.map((entry) => {
256
- if (entry.classification !== "managed-expired") return entry;
526
+ if (entry.action !== "would-delete") return entry;
257
527
  // Reclassify immediately before removal. A renewal, release-state change,
258
528
  // heartbeat, symlink swap, or tracked file added since inventory must win.
259
- const current = classifyArtifactPath(repoRoot, entry.path, now, freshnessSeconds);
260
- if (current.classification !== "managed-expired") return current;
529
+ const current = inventoryArtifacts(repoRoot, { now, freshnessSeconds }).find(
530
+ (row) => row.path === entry.path,
531
+ );
532
+ if (!current) {
533
+ return { ...entry, classification: "unknown", reason: "entry disappeared", action: "keep" };
534
+ }
535
+ if (
536
+ current.action !== "would-delete" ||
537
+ current.artifact_id !== entry.artifact_id ||
538
+ current.bytes !== entry.bytes ||
539
+ current.last_modified_at !== entry.last_modified_at ||
540
+ current.expires_at !== entry.expires_at
541
+ ) {
542
+ return current;
543
+ }
261
544
  try {
262
545
  const top = lstatSync(current.path);
263
546
  if (!top.isDirectory() || top.isSymbolicLink()) {
@@ -404,8 +687,17 @@ function classifyArtifactPath(
404
687
  last_modified_at: new Date(effectiveLastModifiedMs).toISOString(),
405
688
  expires_at: effectiveExpiresAt,
406
689
  owner_instance_id: manifest.created_by?.instance_id ?? null,
690
+ oversize_acknowledged: manifest.oversize_acknowledged === true,
407
691
  });
408
692
 
693
+ if (manifest.holds.length > 0) {
694
+ return {
695
+ ...base,
696
+ classification: "managed-held",
697
+ reason: `held: ${manifest.holds.map((hold) => hold.id).join(", ")}`,
698
+ action: "keep",
699
+ };
700
+ }
409
701
  if (base.bytes === null || lastModifiedMs === null) {
410
702
  return {
411
703
  ...base,
@@ -450,6 +742,59 @@ function classifyArtifactPath(
450
742
  };
451
743
  }
452
744
 
745
+ function applyArtifactBudgets(
746
+ repoRoot: string,
747
+ inputRows: ArtifactInventoryEntry[],
748
+ ): ArtifactInventoryEntry[] {
749
+ const maxUnitBytes = artifactMaxUnitBytes(repoRoot);
750
+ const maxBytes = artifactMaxBytes(repoRoot);
751
+ const rows = inputRows.map((row) => ({ ...row }));
752
+
753
+ for (const row of rows) {
754
+ if (
755
+ row.classification === "managed-current" &&
756
+ row.bytes !== null &&
757
+ row.bytes > maxUnitBytes &&
758
+ !row.oversize_acknowledged
759
+ ) {
760
+ row.classification = "managed-oversize";
761
+ row.reason = `bundle uses ${row.bytes} bytes, above the ${maxUnitBytes}-byte ceiling without --big`;
762
+ row.action = "would-delete";
763
+ }
764
+ }
765
+
766
+ const managedBytes = rows.reduce(
767
+ (sum, row) => sum + (row.artifact_id && row.bytes !== null ? row.bytes : 0),
768
+ 0,
769
+ );
770
+ let retainedBytes =
771
+ managedBytes -
772
+ rows.reduce(
773
+ (sum, row) => sum + (row.action === "would-delete" && row.bytes !== null ? row.bytes : 0),
774
+ 0,
775
+ );
776
+ if (retainedBytes <= maxBytes) return rows;
777
+
778
+ const candidates = rows
779
+ .filter(
780
+ (row) =>
781
+ row.classification === "managed-current" && row.action === "keep" && row.bytes !== null,
782
+ )
783
+ .sort((left, right) =>
784
+ `${left.expires_at ?? ""}\0${left.created_at ?? ""}\0${left.name}`.localeCompare(
785
+ `${right.expires_at ?? ""}\0${right.created_at ?? ""}\0${right.name}`,
786
+ ),
787
+ );
788
+ for (const row of candidates) {
789
+ if (retainedBytes <= maxBytes) break;
790
+ row.classification = "managed-over-budget";
791
+ row.reason = `repository artifact budget is ${maxBytes} bytes; earliest-expiring inactive bundles are removed first`;
792
+ row.action = "would-delete";
793
+ retainedBytes -= row.bytes ?? 0;
794
+ }
795
+ return rows;
796
+ }
797
+
453
798
  function readManifest(path: string): ParsedManifest | ManifestError {
454
799
  const manifestPath = join(path, ARTIFACT_MANIFEST);
455
800
  if (!existsSync(manifestPath)) {
@@ -457,17 +802,40 @@ function readManifest(path: string): ParsedManifest | ManifestError {
457
802
  }
458
803
  let value: unknown;
459
804
  try {
805
+ const unit = lstatSync(path);
806
+ const stat = lstatSync(manifestPath);
807
+ if (
808
+ !unit.isDirectory() ||
809
+ unit.isSymbolicLink() ||
810
+ !stat.isFile() ||
811
+ stat.isSymbolicLink() ||
812
+ stat.size > 1024 * 1024
813
+ ) {
814
+ return { ok: false, reason: "manifest must be a bounded regular file in a direct directory" };
815
+ }
460
816
  value = JSON.parse(readFileSync(manifestPath, "utf8"));
461
817
  } catch (error) {
462
818
  return { ok: false, reason: errorMessage("manifest is unreadable", error) };
463
819
  }
820
+ return parseArtifactManifest(value);
821
+ }
822
+
823
+ /** Validate only the current schema. Legacy conversion belongs to migrateArtifacts. */
824
+ export function parseArtifactManifest(value: unknown): ParsedManifest | ManifestError {
464
825
  if (!value || typeof value !== "object" || Array.isArray(value)) {
465
826
  return { ok: false, reason: "manifest must be a JSON object" };
466
827
  }
467
- const m = value as Partial<ArtifactManifestV1>;
828
+ const m = value as Partial<ArtifactManifestV2>;
468
829
  if (m.schema_version !== ARTIFACT_SCHEMA_VERSION) {
469
830
  return { ok: false, reason: `unsupported schema_version ${String(m.schema_version)}` };
470
831
  }
832
+ if (
833
+ !Array.isArray(m.holds) ||
834
+ m.holds.some((hold) => !validHold(hold)) ||
835
+ new Set(m.holds.map((hold) => hold.id)).size !== m.holds.length
836
+ ) {
837
+ return { ok: false, reason: "invalid holds" };
838
+ }
471
839
  if (!isSafeId(m.artifact_id)) return { ok: false, reason: "invalid artifact_id" };
472
840
  if (typeof m.slug !== "string" || !m.slug || normalizeSlug(m.slug) !== m.slug) {
473
841
  return { ok: false, reason: "invalid slug" };
@@ -495,17 +863,172 @@ function readManifest(path: string): ParsedManifest | ManifestError {
495
863
  if (m.released_by !== undefined && !validActor(m.released_by)) {
496
864
  return { ok: false, reason: "invalid released_by" };
497
865
  }
498
- return { ok: true, manifest: m as ArtifactManifestV1 };
866
+ if (m.oversize_acknowledged !== undefined && typeof m.oversize_acknowledged !== "boolean") {
867
+ return { ok: false, reason: "invalid oversize_acknowledged" };
868
+ }
869
+ return { ok: true, manifest: m as ArtifactManifestV2 };
499
870
  }
500
871
 
501
- function atomicWriteManifest(path: string, manifest: ArtifactManifestV1): void {
872
+ function atomicWriteManifest(path: string, manifest: ArtifactManifestV2): void {
502
873
  const target = join(path, ARTIFACT_MANIFEST);
503
874
  const tmp = `${target}.tmp.${process.pid}.${randomUUID().slice(0, 8)}`;
504
- writeFileSync(tmp, `${JSON.stringify(manifest, null, 2)}\n`, {
505
- encoding: "utf8",
506
- mode: 0o600,
507
- });
508
- renameSync(tmp, target);
875
+ try {
876
+ writeFileSync(tmp, `${JSON.stringify(manifest, null, 2)}\n`, {
877
+ encoding: "utf8",
878
+ mode: 0o600,
879
+ flag: "wx",
880
+ });
881
+ renameSync(tmp, target);
882
+ } finally {
883
+ rmSync(tmp, { force: true });
884
+ }
885
+ }
886
+
887
+ export interface ArtifactMigrationEntry {
888
+ path: string;
889
+ action: "keep" | "would-migrate" | "migrated";
890
+ reason: string;
891
+ preimage_path?: string;
892
+ }
893
+
894
+ /** Explicit, bounded v1 cutover. Preview never writes; every applied unit keeps its exact preimage. */
895
+ export function migrateArtifacts(
896
+ repoRoot: string,
897
+ opts: { yes?: boolean } = {},
898
+ ): ArtifactMigrationEntry[] {
899
+ const migrate = (): ArtifactMigrationEntry[] => {
900
+ const root = artifactsRoot(repoRoot);
901
+ if (!existsSync(root)) return [];
902
+ return readdirSync(root)
903
+ .sort()
904
+ .map((name): ArtifactMigrationEntry => {
905
+ const path = join(root, name);
906
+ try {
907
+ const stat = lstatSync(path);
908
+ if (stat.isSymbolicLink() || !stat.isDirectory())
909
+ throw new Error("not a direct directory");
910
+ if (containsTrackedPath(repoRoot, path))
911
+ throw new Error("artifact contains tracked files");
912
+ const target = join(path, ARTIFACT_MANIFEST);
913
+ const manifestStat = lstatSync(target);
914
+ if (
915
+ !manifestStat.isFile() ||
916
+ manifestStat.isSymbolicLink() ||
917
+ manifestStat.size > 1024 * 1024
918
+ ) {
919
+ throw new Error("manifest is not a bounded regular file");
920
+ }
921
+ const preimage = readFileSync(target, "utf8");
922
+ const legacy = JSON.parse(preimage);
923
+ if (legacy?.schema_version !== 1) {
924
+ return {
925
+ path,
926
+ action: "keep",
927
+ reason: `schema_version ${String(legacy?.schema_version)} is not a migration source`,
928
+ };
929
+ }
930
+ if (Object.hasOwn(legacy, "holds"))
931
+ throw new Error("v1 manifest unexpectedly contains holds");
932
+ const parsed = parseArtifactManifest({
933
+ ...legacy,
934
+ schema_version: ARTIFACT_SCHEMA_VERSION,
935
+ holds: [],
936
+ });
937
+ if (!parsed.ok) throw new Error(parsed.reason);
938
+ const digest = createHash("sha256").update(preimage).digest("hex");
939
+ const preimagePath = join(
940
+ resolve(repoRoot),
941
+ ".harnery/artifact-migrations",
942
+ `${digest}.v1.json`,
943
+ );
944
+ if (!opts.yes)
945
+ return {
946
+ path,
947
+ action: "would-migrate",
948
+ reason: "valid v1 manifest",
949
+ preimage_path: preimagePath,
950
+ };
951
+ mkdirSync(dirname(preimagePath), { recursive: true });
952
+ try {
953
+ writeFileSync(preimagePath, preimage, { flag: "wx", mode: 0o600 });
954
+ } catch (error) {
955
+ if (
956
+ (error as NodeJS.ErrnoException).code !== "EEXIST" ||
957
+ lstatSync(preimagePath).isSymbolicLink() ||
958
+ readFileSync(preimagePath, "utf8") !== preimage
959
+ )
960
+ throw error;
961
+ }
962
+ if (readFileSync(target, "utf8") !== preimage)
963
+ throw new Error("manifest changed before migration");
964
+ atomicWriteManifest(path, parsed.manifest);
965
+ return {
966
+ path,
967
+ action: "migrated",
968
+ reason: "v1 preimage preserved; identity and retention unchanged",
969
+ preimage_path: preimagePath,
970
+ };
971
+ } catch (error) {
972
+ return { path, action: "keep", reason: errorMessage("migration refused", error) };
973
+ }
974
+ });
975
+ };
976
+ return opts.yes ? withArtifactLock(repoRoot, migrate) : migrate();
977
+ }
978
+
979
+ /** A persistent directory lock fails closed on contention or a crashed owner. */
980
+ function withArtifactLock<T>(repoRoot: string, action: () => T): T {
981
+ const lock = join(resolve(repoRoot), ".harnery/artifacts-mutation.lock");
982
+ mkdirSync(dirname(lock), { recursive: true });
983
+ try {
984
+ mkdirSync(lock);
985
+ } catch (error) {
986
+ throw new Error(
987
+ errorMessage(
988
+ "artifact store mutation lock unavailable; retry after its owner finishes",
989
+ error,
990
+ ),
991
+ );
992
+ }
993
+ try {
994
+ return action();
995
+ } finally {
996
+ rmdirSync(lock);
997
+ }
998
+ }
999
+
1000
+ function validHoldId(value: unknown): value is string {
1001
+ return typeof value === "string" && /^[A-Za-z0-9][A-Za-z0-9_.:-]{0,127}$/.test(value);
1002
+ }
1003
+
1004
+ function validHold(value: unknown): value is ArtifactHold {
1005
+ if (!value || typeof value !== "object" || Array.isArray(value)) return false;
1006
+ const hold = value as Partial<ArtifactHold>;
1007
+ return (
1008
+ validHoldId(hold.id) &&
1009
+ typeof hold.reason === "string" &&
1010
+ !!hold.reason.trim() &&
1011
+ validActor(hold.set_by) &&
1012
+ validIso(hold.set_at)
1013
+ );
1014
+ }
1015
+
1016
+ function makeHold(
1017
+ input: ArtifactHoldInput,
1018
+ actor: ArtifactActor | undefined,
1019
+ now: Date,
1020
+ ): ArtifactHold {
1021
+ assertValidDate(now, "now");
1022
+ if (!validActor(actor)) throw new Error("a valid hold actor is required");
1023
+ if (!validHoldId(input.id)) throw new Error("invalid hold id");
1024
+ if (typeof input.reason !== "string" || !input.reason.trim())
1025
+ throw new Error("hold reason must not be empty");
1026
+ return {
1027
+ id: input.id,
1028
+ reason: input.reason.trim(),
1029
+ set_by: { ...actor },
1030
+ set_at: now.toISOString(),
1031
+ };
509
1032
  }
510
1033
 
511
1034
  function ownerLiveness(
@@ -604,6 +1127,7 @@ function rowFor(
604
1127
  last_modified_at: null,
605
1128
  expires_at: null,
606
1129
  owner_instance_id: null,
1130
+ oversize_acknowledged: false,
607
1131
  };
608
1132
  }
609
1133
 
@@ -629,6 +1153,17 @@ function addDays(date: Date, days: number): Date {
629
1153
  return new Date(date.getTime() + days * 24 * 60 * 60 * 1000);
630
1154
  }
631
1155
 
1156
+ function positiveMinutes(value: number): number {
1157
+ if (!Number.isInteger(value) || value <= 0 || value > 3650 * 24 * 60) {
1158
+ throw new Error("retention minutes must be between 1 and 5256000");
1159
+ }
1160
+ return value;
1161
+ }
1162
+
1163
+ function addMinutes(date: Date, minutes: number): Date {
1164
+ return new Date(date.getTime() + minutes * 60 * 1000);
1165
+ }
1166
+
632
1167
  function isSafeId(value: unknown): value is string {
633
1168
  return (
634
1169
  typeof value === "string" &&