harnery 0.38.0 → 0.40.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 (378) hide show
  1. package/dist/commands/agents.d.ts.map +1 -1
  2. package/dist/commands/agents.js +32 -8
  3. package/dist/commands/artifacts.d.ts.map +1 -1
  4. package/dist/commands/artifacts.js +97 -6
  5. package/dist/commands/backup.d.ts +16 -0
  6. package/dist/commands/backup.d.ts.map +1 -1
  7. package/dist/commands/backup.js +239 -33
  8. package/dist/commands/browse.d.ts +8 -0
  9. package/dist/commands/browse.d.ts.map +1 -1
  10. package/dist/commands/browse.js +466 -231
  11. package/dist/commands/diagnostics.d.ts.map +1 -1
  12. package/dist/commands/diagnostics.js +6 -3
  13. package/dist/commands/ledger-v3.d.ts.map +1 -1
  14. package/dist/commands/ledger-v3.js +20 -4
  15. package/dist/commands/qa-run.d.ts.map +1 -1
  16. package/dist/commands/qa-run.js +26 -3
  17. package/dist/commands/qa-status.d.ts.map +1 -1
  18. package/dist/commands/qa-status.js +7 -2
  19. package/dist/commands/resources.d.ts.map +1 -1
  20. package/dist/commands/resources.js +23 -3
  21. package/dist/commands/review-pack.d.ts.map +1 -1
  22. package/dist/commands/review-pack.js +98 -23
  23. package/dist/commands/tunnel.d.ts.map +1 -1
  24. package/dist/commands/tunnel.js +2 -2
  25. package/dist/core/adapters/profiles.d.ts +1 -1
  26. package/dist/core/agents/canonical-emit.d.ts +3 -2
  27. package/dist/core/agents/canonical-emit.d.ts.map +1 -1
  28. package/dist/core/agents/canonical-emit.js +2 -0
  29. package/dist/core/agents/cli.js +13 -6
  30. package/dist/core/agents/live-authority-v3.d.ts.map +1 -1
  31. package/dist/core/agents/live-authority-v3.js +5 -3
  32. package/dist/core/agents/live-lifecycle-v3.d.ts +2 -2
  33. package/dist/core/agents/live-lifecycle-v3.d.ts.map +1 -1
  34. package/dist/core/agents/render/prompt-context.d.ts +7 -5
  35. package/dist/core/agents/render/prompt-context.d.ts.map +1 -1
  36. package/dist/core/agents/render/prompt-context.js +63 -8
  37. package/dist/core/agents/session-finalizer-v3.d.ts +2 -2
  38. package/dist/core/agents/session-finalizer-v3.d.ts.map +1 -1
  39. package/dist/core/agents/state/live-coordination-view.d.ts +3 -3
  40. package/dist/core/agents/state/live-coordination-view.d.ts.map +1 -1
  41. package/dist/core/agents/state/live-coordination-view.js +2 -1
  42. package/dist/core/agents/state/names.d.ts +35 -2
  43. package/dist/core/agents/state/names.d.ts.map +1 -1
  44. package/dist/core/agents/state/names.js +80 -5
  45. package/dist/core/agents/state/stale-sweep.d.ts +2 -1
  46. package/dist/core/agents/state/stale-sweep.d.ts.map +1 -1
  47. package/dist/core/agents/state/stale-sweep.js +23 -6
  48. package/dist/core/agents/turn-elapsed.d.ts +51 -0
  49. package/dist/core/agents/turn-elapsed.d.ts.map +1 -0
  50. package/dist/core/agents/turn-elapsed.js +91 -0
  51. package/dist/core/artifacts/activity.d.ts +14 -0
  52. package/dist/core/artifacts/activity.d.ts.map +1 -0
  53. package/dist/core/artifacts/activity.js +52 -0
  54. package/dist/core/artifacts/delivery-card.d.ts +39 -0
  55. package/dist/core/artifacts/delivery-card.d.ts.map +1 -0
  56. package/dist/core/artifacts/delivery-card.js +282 -0
  57. package/dist/core/artifacts/index.d.ts +42 -7
  58. package/dist/core/artifacts/index.d.ts.map +1 -1
  59. package/dist/core/artifacts/index.js +288 -76
  60. package/dist/core/artifacts/mutation-lock.d.ts +2 -0
  61. package/dist/core/artifacts/mutation-lock.d.ts.map +1 -0
  62. package/dist/core/artifacts/mutation-lock.js +91 -0
  63. package/dist/core/backup/host-snapshot.d.ts +18 -0
  64. package/dist/core/backup/host-snapshot.d.ts.map +1 -0
  65. package/dist/core/backup/host-snapshot.js +64 -0
  66. package/dist/core/config.d.ts +20 -5
  67. package/dist/core/config.d.ts.map +1 -1
  68. package/dist/core/config.js +44 -10
  69. package/dist/core/diagnostics/advice.d.ts +38 -3
  70. package/dist/core/diagnostics/advice.d.ts.map +1 -1
  71. package/dist/core/diagnostics/advice.js +103 -99
  72. package/dist/core/diagnostics/bundle.d.ts.map +1 -1
  73. package/dist/core/diagnostics/bundle.js +111 -47
  74. package/dist/core/diagnostics/comparison.d.ts.map +1 -1
  75. package/dist/core/diagnostics/comparison.js +3 -2
  76. package/dist/core/diagnostics/contract.d.ts +15 -31
  77. package/dist/core/diagnostics/contract.d.ts.map +1 -1
  78. package/dist/core/diagnostics/contract.js +2 -5
  79. package/dist/core/diagnostics/index.d.ts +2 -0
  80. package/dist/core/diagnostics/index.d.ts.map +1 -1
  81. package/dist/core/diagnostics/index.js +2 -0
  82. package/dist/core/diagnostics/pressure-contract.d.ts +260 -0
  83. package/dist/core/diagnostics/pressure-contract.d.ts.map +1 -0
  84. package/dist/core/diagnostics/pressure-contract.js +70 -0
  85. package/dist/core/diagnostics/pressure.d.ts +25 -0
  86. package/dist/core/diagnostics/pressure.d.ts.map +1 -0
  87. package/dist/core/diagnostics/pressure.js +1138 -0
  88. package/dist/core/diagnostics/replay.d.ts.map +1 -1
  89. package/dist/core/diagnostics/replay.js +90 -10
  90. package/dist/core/events/v3/adapter-id.d.ts +6 -0
  91. package/dist/core/events/v3/adapter-id.d.ts.map +1 -0
  92. package/dist/core/events/v3/adapter-id.js +7 -0
  93. package/dist/core/events/v3/archive-retention.d.ts.map +1 -1
  94. package/dist/core/events/v3/archive-retention.js +3 -2
  95. package/dist/core/events/v3/bootstrap.d.ts +41 -4
  96. package/dist/core/events/v3/bootstrap.d.ts.map +1 -1
  97. package/dist/core/events/v3/bootstrap.js +120 -17
  98. package/dist/core/events/v3/capabilities.d.ts +16 -9
  99. package/dist/core/events/v3/capabilities.d.ts.map +1 -1
  100. package/dist/core/events/v3/capabilities.js +37 -2
  101. package/dist/core/events/v3/capability-drift.d.ts +2 -2
  102. package/dist/core/events/v3/capability-drift.d.ts.map +1 -1
  103. package/dist/core/events/v3/control-witness.d.ts +48 -5
  104. package/dist/core/events/v3/control-witness.d.ts.map +1 -1
  105. package/dist/core/events/v3/control-witness.js +91 -36
  106. package/dist/core/events/v3/control.d.ts.map +1 -1
  107. package/dist/core/events/v3/control.js +22 -2
  108. package/dist/core/events/v3/index.d.ts +2 -1
  109. package/dist/core/events/v3/index.d.ts.map +1 -1
  110. package/dist/core/events/v3/index.js +2 -1
  111. package/dist/core/events/v3/live-feed.d.ts +7 -1
  112. package/dist/core/events/v3/live-feed.d.ts.map +1 -1
  113. package/dist/core/events/v3/live-feed.js +21 -3
  114. package/dist/core/events/v3/live-routing.d.ts.map +1 -1
  115. package/dist/core/events/v3/live-routing.js +10 -2
  116. package/dist/core/events/v3/producers/command-recorder.d.ts +2 -2
  117. package/dist/core/events/v3/producers/command-recorder.d.ts.map +1 -1
  118. package/dist/core/events/v3/producers/command-recorder.js +2 -1
  119. package/dist/core/events/v3/producers/coordination-recorder.d.ts +2 -2
  120. package/dist/core/events/v3/producers/coordination-recorder.d.ts.map +1 -1
  121. package/dist/core/events/v3/producers/coordination-recorder.js +2 -1
  122. package/dist/core/events/v3/producers/hook-base.d.ts +2 -2
  123. package/dist/core/events/v3/producers/hook-base.d.ts.map +1 -1
  124. package/dist/core/events/v3/producers/hook-base.js +15 -5
  125. package/dist/core/events/v3/producers/hook.d.ts.map +1 -1
  126. package/dist/core/events/v3/producers/hook.js +17 -4
  127. package/dist/core/events/v3/producers/intake.d.ts +4 -4
  128. package/dist/core/events/v3/producers/intake.d.ts.map +1 -1
  129. package/dist/core/events/v3/producers/intake.js +3 -2
  130. package/dist/core/events/v3/producers/producer-state-observer.d.ts.map +1 -1
  131. package/dist/core/events/v3/producers/producer-state-observer.js +2 -1
  132. package/dist/core/events/v3/producers/recorder.d.ts +16 -5
  133. package/dist/core/events/v3/producers/recorder.d.ts.map +1 -1
  134. package/dist/core/events/v3/producers/recorder.js +31 -9
  135. package/dist/core/events/v3/reader.d.ts.map +1 -1
  136. package/dist/core/events/v3/reader.js +10 -13
  137. package/dist/core/events/v3/runtime-build.d.ts.map +1 -1
  138. package/dist/core/events/v3/runtime-build.js +3 -2
  139. package/dist/core/events/v3/runtime-telemetry-capabilities.d.ts +2 -2
  140. package/dist/core/events/v3/runtime-telemetry-capabilities.d.ts.map +1 -1
  141. package/dist/core/events/v3/turn-telemetry.d.ts +2 -2
  142. package/dist/core/events/v3/turn-telemetry.d.ts.map +1 -1
  143. package/dist/core/hooks/adapter/output.d.ts +16 -14
  144. package/dist/core/hooks/adapter/output.d.ts.map +1 -1
  145. package/dist/core/hooks/adapter/output.js +36 -38
  146. package/dist/core/hooks/backup-schedule.d.ts +34 -0
  147. package/dist/core/hooks/backup-schedule.d.ts.map +1 -0
  148. package/dist/core/hooks/backup-schedule.js +114 -0
  149. package/dist/core/hooks/cli.js +61 -18
  150. package/dist/core/hooks/codex-wsl-bridge.d.ts +2 -0
  151. package/dist/core/hooks/codex-wsl-bridge.d.ts.map +1 -1
  152. package/dist/core/hooks/codex-wsl-bridge.js +18 -4
  153. package/dist/core/hooks/index.d.ts +1 -0
  154. package/dist/core/hooks/index.d.ts.map +1 -1
  155. package/dist/core/hooks/index.js +1 -0
  156. package/dist/core/hooks/prompt-context/contract.d.ts +61 -0
  157. package/dist/core/hooks/prompt-context/contract.d.ts.map +1 -0
  158. package/dist/core/hooks/prompt-context/contract.js +102 -0
  159. package/dist/core/hooks/prompt-context/index.d.ts +5 -0
  160. package/dist/core/hooks/prompt-context/index.d.ts.map +1 -0
  161. package/dist/core/hooks/prompt-context/index.js +2 -0
  162. package/dist/core/hooks/prompt-context/runner.d.ts +24 -0
  163. package/dist/core/hooks/prompt-context/runner.d.ts.map +1 -0
  164. package/dist/core/hooks/prompt-context/runner.js +249 -0
  165. package/dist/core/qa-artifacts.d.ts +3 -0
  166. package/dist/core/qa-artifacts.d.ts.map +1 -1
  167. package/dist/core/qa-artifacts.js +25 -3
  168. package/dist/core/resources/contract.d.ts +69 -3
  169. package/dist/core/resources/contract.d.ts.map +1 -1
  170. package/dist/core/resources/contract.js +1 -1
  171. package/dist/core/resources/index.d.ts +1 -0
  172. package/dist/core/resources/index.d.ts.map +1 -1
  173. package/dist/core/resources/index.js +1 -0
  174. package/dist/core/resources/native.d.ts +36 -0
  175. package/dist/core/resources/native.d.ts.map +1 -0
  176. package/dist/core/resources/native.js +418 -0
  177. package/dist/core/resources/sampler.d.ts +2 -0
  178. package/dist/core/resources/sampler.d.ts.map +1 -1
  179. package/dist/core/resources/sampler.js +31 -1
  180. package/dist/core/resources/status.d.ts +61 -0
  181. package/dist/core/resources/status.d.ts.map +1 -0
  182. package/dist/core/resources/status.js +536 -0
  183. package/dist/core/resources/system.d.ts +27 -0
  184. package/dist/core/resources/system.d.ts.map +1 -0
  185. package/dist/core/resources/system.js +439 -0
  186. package/dist/core/semantic/contract.d.ts +13 -13
  187. package/dist/core/semantic/service.d.ts +2 -0
  188. package/dist/core/semantic/service.d.ts.map +1 -1
  189. package/dist/core/semantic/service.js +27 -4
  190. package/dist/core/storage/builtins.js +41 -0
  191. package/dist/core/supervisor/contract.d.ts +41 -3
  192. package/dist/core/supervisor/contract.d.ts.map +1 -1
  193. package/dist/core/supervisor/contract.js +3 -2
  194. package/dist/core/supervisor/findings.d.ts +3 -0
  195. package/dist/core/supervisor/findings.d.ts.map +1 -1
  196. package/dist/core/supervisor/findings.js +88 -0
  197. package/dist/core/supervisor/history.d.ts.map +1 -1
  198. package/dist/core/supervisor/history.js +14 -0
  199. package/dist/core/supervisor/ledger-state.d.ts +55 -0
  200. package/dist/core/supervisor/ledger-state.d.ts.map +1 -0
  201. package/dist/core/supervisor/ledger-state.js +0 -0
  202. package/dist/core/supervisor/service.d.ts.map +1 -1
  203. package/dist/core/supervisor/service.js +120 -7
  204. package/dist/core/supervisor/storage.d.ts +3 -1
  205. package/dist/core/supervisor/storage.d.ts.map +1 -1
  206. package/dist/core/supervisor/storage.js +4 -0
  207. package/dist/core/workflow/admission.d.ts +3 -0
  208. package/dist/core/workflow/admission.d.ts.map +1 -1
  209. package/dist/core/workflow/admission.js +28 -16
  210. package/dist/core/workflow/engine.js +2 -2
  211. package/dist/core/workflow/live-session-v3.d.ts.map +1 -1
  212. package/dist/core/workflow/live-session-v3.js +6 -1
  213. package/dist/core/workflow/proof.d.ts.map +1 -1
  214. package/dist/core/workflow/proof.js +4 -3
  215. package/dist/lib/browser/capture-fidelity.d.ts +7 -0
  216. package/dist/lib/browser/capture-fidelity.d.ts.map +1 -1
  217. package/dist/lib/browser/capture-fidelity.js +17 -0
  218. package/dist/lib/browser/client.d.ts +9 -0
  219. package/dist/lib/browser/client.d.ts.map +1 -1
  220. package/dist/lib/browser/client.js +85 -37
  221. package/dist/lib/browser/index.d.ts +3 -1
  222. package/dist/lib/browser/index.d.ts.map +1 -1
  223. package/dist/lib/browser/index.js +2 -1
  224. package/dist/lib/browser/page-review-budget.d.ts +13 -0
  225. package/dist/lib/browser/page-review-budget.d.ts.map +1 -0
  226. package/dist/lib/browser/page-review-budget.js +251 -0
  227. package/dist/lib/browser/page-review-contracts.d.ts +127 -0
  228. package/dist/lib/browser/page-review-contracts.d.ts.map +1 -0
  229. package/dist/lib/browser/page-review-contracts.js +5 -0
  230. package/dist/lib/browser/page-review-evidence.d.ts +48 -0
  231. package/dist/lib/browser/page-review-evidence.d.ts.map +1 -0
  232. package/dist/lib/browser/page-review-evidence.js +340 -0
  233. package/dist/lib/browser/page-review-judge.d.ts +1 -1
  234. package/dist/lib/browser/page-review-judge.d.ts.map +1 -1
  235. package/dist/lib/browser/page-review-judge.js +46 -38
  236. package/dist/lib/browser/page-review-pack.d.ts +44 -22
  237. package/dist/lib/browser/page-review-pack.d.ts.map +1 -1
  238. package/dist/lib/browser/page-review-pack.js +116 -36
  239. package/dist/lib/browser/page-review-source.d.ts +12 -0
  240. package/dist/lib/browser/page-review-source.d.ts.map +1 -0
  241. package/dist/lib/browser/page-review-source.js +22 -0
  242. package/dist/lib/browser/qa-run-contracts.d.ts +35 -9
  243. package/dist/lib/browser/qa-run-contracts.d.ts.map +1 -1
  244. package/dist/lib/browser/qa-run-contracts.js +48 -14
  245. package/dist/lib/browser/qa-run.d.ts.map +1 -1
  246. package/dist/lib/browser/qa-run.js +215 -68
  247. package/dist/lib/browser/qa-snapshot.d.ts +6 -0
  248. package/dist/lib/browser/qa-snapshot.d.ts.map +1 -1
  249. package/dist/lib/browser/qa-snapshot.js +259 -48
  250. package/dist/lib/browser/review-capture-transaction.d.ts +13 -0
  251. package/dist/lib/browser/review-capture-transaction.d.ts.map +1 -0
  252. package/dist/lib/browser/review-capture-transaction.js +79 -0
  253. package/dist/lib/browser/standalone-html.d.ts.map +1 -1
  254. package/dist/lib/browser/standalone-html.js +4 -2
  255. package/dist/lib/browser/thumbnail-association.d.ts +34 -0
  256. package/dist/lib/browser/thumbnail-association.d.ts.map +1 -0
  257. package/dist/lib/browser/thumbnail-association.js +158 -0
  258. package/dist/lib/browser/tiling.d.ts +30 -0
  259. package/dist/lib/browser/tiling.d.ts.map +1 -1
  260. package/dist/lib/browser/tiling.js +38 -0
  261. package/dist/lib/docs-metadata-managed.d.ts.map +1 -1
  262. package/dist/lib/docs-metadata-managed.js +4 -1
  263. package/dist/lib/identities/assume.d.ts.map +1 -1
  264. package/dist/lib/identities/assume.js +7 -1
  265. package/dist/lib/instructions/templates.d.ts.map +1 -1
  266. package/dist/lib/instructions/templates.js +1 -0
  267. package/dist/lib/local-file-url.d.ts +12 -0
  268. package/dist/lib/local-file-url.d.ts.map +1 -1
  269. package/dist/lib/local-file-url.js +16 -2
  270. package/dist/lib/tunnel/state.d.ts +19 -4
  271. package/dist/lib/tunnel/state.d.ts.map +1 -1
  272. package/dist/lib/tunnel/state.js +53 -13
  273. package/package.json +7 -4
  274. package/schemas/config.schema.json +69 -2
  275. package/src/commands/agents.ts +42 -9
  276. package/src/commands/artifacts.ts +130 -6
  277. package/src/commands/backup.ts +287 -34
  278. package/src/commands/browse.ts +564 -267
  279. package/src/commands/diagnostics.ts +6 -3
  280. package/src/commands/ledger-v3.ts +20 -3
  281. package/src/commands/qa-run.ts +37 -3
  282. package/src/commands/qa-status.ts +11 -2
  283. package/src/commands/resources.ts +23 -3
  284. package/src/commands/review-pack.ts +165 -62
  285. package/src/commands/tunnel.ts +2 -1
  286. package/src/core/agents/canonical-emit.ts +4 -2
  287. package/src/core/agents/cli.ts +14 -6
  288. package/src/core/agents/live-authority-v3.ts +7 -3
  289. package/src/core/agents/live-lifecycle-v3.ts +2 -2
  290. package/src/core/agents/render/prompt-context.ts +68 -14
  291. package/src/core/agents/session-finalizer-v3.ts +2 -2
  292. package/src/core/agents/state/live-coordination-view.ts +4 -4
  293. package/src/core/agents/state/names.ts +92 -7
  294. package/src/core/agents/state/stale-sweep.ts +22 -8
  295. package/src/core/agents/turn-elapsed.ts +135 -0
  296. package/src/core/artifacts/activity.ts +71 -0
  297. package/src/core/artifacts/delivery-card.ts +375 -0
  298. package/src/core/artifacts/index.ts +385 -91
  299. package/src/core/artifacts/mutation-lock.ts +106 -0
  300. package/src/core/backup/host-snapshot.ts +76 -0
  301. package/src/core/config.ts +66 -13
  302. package/src/core/diagnostics/advice.ts +148 -120
  303. package/src/core/diagnostics/bundle.ts +138 -52
  304. package/src/core/diagnostics/comparison.ts +6 -2
  305. package/src/core/diagnostics/contract.ts +39 -42
  306. package/src/core/diagnostics/index.ts +2 -0
  307. package/src/core/diagnostics/pressure-contract.ts +294 -0
  308. package/src/core/diagnostics/pressure.ts +1361 -0
  309. package/src/core/diagnostics/replay.ts +116 -11
  310. package/src/core/events/v3/adapter-id.ts +12 -0
  311. package/src/core/events/v3/archive-retention.ts +3 -6
  312. package/src/core/events/v3/bootstrap.ts +163 -16
  313. package/src/core/events/v3/capabilities.ts +75 -25
  314. package/src/core/events/v3/capability-drift.ts +2 -2
  315. package/src/core/events/v3/control-witness.ts +172 -41
  316. package/src/core/events/v3/control.ts +34 -2
  317. package/src/core/events/v3/index.ts +3 -0
  318. package/src/core/events/v3/live-feed.ts +23 -5
  319. package/src/core/events/v3/live-routing.ts +14 -2
  320. package/src/core/events/v3/producers/command-recorder.ts +4 -4
  321. package/src/core/events/v3/producers/coordination-recorder.ts +4 -4
  322. package/src/core/events/v3/producers/hook-base.ts +21 -16
  323. package/src/core/events/v3/producers/hook.ts +21 -4
  324. package/src/core/events/v3/producers/intake.ts +6 -6
  325. package/src/core/events/v3/producers/producer-state-observer.ts +2 -1
  326. package/src/core/events/v3/producers/recorder.ts +50 -21
  327. package/src/core/events/v3/reader.ts +14 -13
  328. package/src/core/events/v3/runtime-build.ts +3 -2
  329. package/src/core/events/v3/runtime-telemetry-capabilities.ts +3 -3
  330. package/src/core/events/v3/turn-telemetry.ts +4 -4
  331. package/src/core/hooks/adapter/output.ts +50 -36
  332. package/src/core/hooks/backup-schedule.ts +147 -0
  333. package/src/core/hooks/cli.ts +65 -14
  334. package/src/core/hooks/codex-wsl-bridge.ts +21 -5
  335. package/src/core/hooks/index.ts +1 -0
  336. package/src/core/hooks/prompt-context/contract.ts +177 -0
  337. package/src/core/hooks/prompt-context/index.ts +22 -0
  338. package/src/core/hooks/prompt-context/runner.ts +303 -0
  339. package/src/core/qa-artifacts.ts +29 -3
  340. package/src/core/resources/contract.ts +76 -3
  341. package/src/core/resources/index.ts +1 -0
  342. package/src/core/resources/native.ts +481 -0
  343. package/src/core/resources/sampler.ts +46 -1
  344. package/src/core/resources/status.ts +717 -0
  345. package/src/core/resources/system.ts +530 -0
  346. package/src/core/semantic/service.ts +37 -3
  347. package/src/core/storage/builtins.ts +41 -0
  348. package/src/core/supervisor/contract.ts +45 -3
  349. package/src/core/supervisor/findings.ts +159 -0
  350. package/src/core/supervisor/history.ts +15 -0
  351. package/src/core/supervisor/ledger-state.ts +0 -0
  352. package/src/core/supervisor/service.ts +133 -5
  353. package/src/core/supervisor/storage.ts +7 -0
  354. package/src/core/workflow/admission.ts +33 -17
  355. package/src/core/workflow/engine.ts +2 -2
  356. package/src/core/workflow/live-session-v3.ts +6 -8
  357. package/src/core/workflow/proof.ts +4 -3
  358. package/src/lib/browser/capture-fidelity.ts +23 -0
  359. package/src/lib/browser/client.ts +96 -44
  360. package/src/lib/browser/index.ts +15 -1
  361. package/src/lib/browser/page-review-budget.ts +309 -0
  362. package/src/lib/browser/page-review-contracts.ts +129 -0
  363. package/src/lib/browser/page-review-evidence.ts +465 -0
  364. package/src/lib/browser/page-review-judge.ts +56 -40
  365. package/src/lib/browser/page-review-pack.ts +181 -43
  366. package/src/lib/browser/page-review-source.ts +30 -0
  367. package/src/lib/browser/qa-run-contracts.ts +62 -18
  368. package/src/lib/browser/qa-run.ts +259 -71
  369. package/src/lib/browser/qa-snapshot.ts +289 -48
  370. package/src/lib/browser/review-capture-transaction.ts +106 -0
  371. package/src/lib/browser/standalone-html.ts +4 -2
  372. package/src/lib/browser/thumbnail-association.ts +190 -0
  373. package/src/lib/browser/tiling.ts +63 -0
  374. package/src/lib/docs-metadata-managed.ts +4 -1
  375. package/src/lib/identities/assume.ts +12 -4
  376. package/src/lib/instructions/templates.ts +1 -0
  377. package/src/lib/local-file-url.ts +17 -2
  378. package/src/lib/tunnel/state.ts +70 -13
@@ -18,11 +18,11 @@ import {
18
18
  readdirSync,
19
19
  readFileSync,
20
20
  renameSync,
21
- rmdirSync,
22
21
  rmSync,
23
22
  writeFileSync,
24
23
  } from "node:fs";
25
24
  import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
25
+ import { isDeepStrictEqual } from "node:util";
26
26
  import { readLiveCoordinationRow } from "../agents/state/live-coordination-view.ts";
27
27
  import {
28
28
  artifactAutoCleanEnabled,
@@ -30,10 +30,36 @@ import {
30
30
  artifactDefaultRetentionDays,
31
31
  artifactMaxBytes,
32
32
  artifactMaxUnitBytes,
33
+ coordFreshnessSeconds,
34
+ resolveBinName,
33
35
  } from "../config.ts";
36
+ import {
37
+ type ArtifactActivity,
38
+ artifactRootEntries,
39
+ readArtifactActivity,
40
+ validArtifactActivity,
41
+ } from "./activity.ts";
34
42
  import { ARTIFACT_MANIFEST, ARTIFACT_SCHEMA_VERSION, ARTIFACTS_DIR } from "./constants.ts";
43
+ import { withArtifactLock } from "./mutation-lock.ts";
35
44
 
36
45
  export { ARTIFACT_MANIFEST, ARTIFACT_SCHEMA_VERSION, ARTIFACTS_DIR } from "./constants.ts";
46
+ export type {
47
+ ArtifactDeliveryCard,
48
+ ArtifactDeliveryItem,
49
+ ArtifactDeliveryManifest,
50
+ ArtifactDeliveryPath,
51
+ ArtifactDeliveryUrl,
52
+ } from "./delivery-card.ts";
53
+ export {
54
+ ARTIFACT_DELIVERY_AUTO_ITEM_LIMIT,
55
+ ARTIFACT_DELIVERY_MANIFEST,
56
+ ARTIFACT_DELIVERY_SCHEMA_VERSION,
57
+ parseArtifactDeliverySpec,
58
+ readArtifactDeliveryManifest,
59
+ renderArtifactDeliveryCard,
60
+ resolveArtifactDeliveryManifest,
61
+ writeArtifactDeliveryManifest,
62
+ } from "./delivery-card.ts";
37
63
 
38
64
  export interface ArtifactActor {
39
65
  instance_id: string;
@@ -57,6 +83,7 @@ export interface ArtifactManifestV2 {
57
83
  released_at?: string;
58
84
  released_by?: ArtifactActor;
59
85
  oversize_acknowledged?: boolean;
86
+ activity?: ArtifactActivity;
60
87
  }
61
88
 
62
89
  export interface ArtifactHold {
@@ -78,6 +105,8 @@ export function artifactCapabilities() {
78
105
  atomic_create_holds: true,
79
106
  owner_scoped_unhold: true,
80
107
  explicit_v1_migration: true,
108
+ minute_retention: true,
109
+ discard_after_review: true,
81
110
  } as const;
82
111
  }
83
112
 
@@ -208,7 +237,7 @@ function createArtifactUnlocked(
208
237
  },
209
238
  ...(input.big ? { oversize_acknowledged: true } : {}),
210
239
  };
211
- atomicWriteManifest(path, manifest);
240
+ atomicWriteManifest(path, manifest, now);
212
241
  return { path, manifest };
213
242
  }
214
243
 
@@ -258,7 +287,7 @@ export function showArtifact(
258
287
  export function renewArtifact(
259
288
  repoRoot: string,
260
289
  ref: string,
261
- days: number,
290
+ days: number | { minutes: number },
262
291
  reason: string,
263
292
  input: ArtifactMutationInput = {},
264
293
  ): ArtifactManifestV2 {
@@ -270,13 +299,16 @@ export function renewArtifact(
270
299
  function renewArtifactUnlocked(
271
300
  repoRoot: string,
272
301
  ref: string,
273
- days: number,
302
+ days: number | { minutes: number },
274
303
  reason: string,
275
304
  input: ArtifactMutationInput,
276
305
  ): ArtifactManifestV2 {
277
306
  const now = input.now ?? new Date();
278
307
  assertValidDate(now, "now");
279
- const retentionDays = positiveDays(days);
308
+ const expiresAt =
309
+ typeof days === "number"
310
+ ? addDays(now, positiveDays(days))
311
+ : addMinutes(now, positiveMinutes(days.minutes));
280
312
  const why = reason.trim();
281
313
  if (!why) throw new Error("renewal reason must not be empty");
282
314
  const path = resolveArtifactRef(repoRoot, ref);
@@ -285,12 +317,12 @@ function renewArtifactUnlocked(
285
317
  const manifest: ArtifactManifestV2 = {
286
318
  ...parsed.manifest,
287
319
  retention: {
288
- expires_at: addDays(now, retentionDays).toISOString(),
320
+ expires_at: expiresAt.toISOString(),
289
321
  renewed_at: now.toISOString(),
290
322
  reason: why,
291
323
  },
292
324
  };
293
- atomicWriteManifest(path, manifest);
325
+ atomicWriteManifest(path, manifest, now);
294
326
  return manifest;
295
327
  }
296
328
 
@@ -302,6 +334,64 @@ export function releaseArtifact(
302
334
  return withArtifactLock(repoRoot, () => releaseArtifactUnlocked(repoRoot, ref, input));
303
335
  }
304
336
 
337
+ /** Retire reviewed evidence without deleting it or extending an earlier deadline. */
338
+ export function discardArtifact(
339
+ repoRoot: string,
340
+ ref: string,
341
+ reason: string,
342
+ input: ArtifactMutationInput & { minutes?: number } = {},
343
+ ): ArtifactManifestV2 {
344
+ return withArtifactLock(repoRoot, () => {
345
+ const now = input.now ?? new Date();
346
+ assertValidDate(now, "now");
347
+ const minutes = positiveMinutes(input.minutes ?? 60);
348
+ const why = reason.trim();
349
+ if (!why)
350
+ throw new Error("discard requires a reason confirming the files are no longer needed");
351
+ const path = resolveArtifactRef(repoRoot, ref);
352
+ const entry = classifyArtifactPath(repoRoot, path, now, coordFreshnessSeconds(repoRoot));
353
+ if (
354
+ !["managed-current", "managed-expired", "managed-active"].includes(entry.classification) ||
355
+ (entry.classification === "managed-active" &&
356
+ entry.owner_instance_id !== input.actor?.instance_id)
357
+ )
358
+ throw new Error(`cannot discard artifact: ${entry.reason}`);
359
+ const parsed = readManifest(path);
360
+ if (!parsed.ok) throw new Error(parsed.reason);
361
+ const manifest = parsed.manifest;
362
+ const remaining = Date.parse(entry.expires_at!) - now.getTime();
363
+ const priorWindow =
364
+ Date.parse(manifest.retention.expires_at) -
365
+ Date.parse(manifest.retention.renewed_at ?? manifest.created_at);
366
+ const windowMs = Math.min(minutes * 60_000, remaining, priorWindow);
367
+ const updated: ArtifactManifestV2 = {
368
+ ...manifest,
369
+ retention: {
370
+ ...manifest.retention,
371
+ ...(windowMs > 0
372
+ ? {
373
+ renewed_at: now.toISOString(),
374
+ expires_at: new Date(now.getTime() + windowMs).toISOString(),
375
+ }
376
+ : {}),
377
+ reason: why,
378
+ },
379
+ released_at: now.toISOString(),
380
+ released_by: input.actor,
381
+ };
382
+ atomicWriteManifest(path, updated, now);
383
+ return updated;
384
+ });
385
+ }
386
+
387
+ /** Advice only: a successful check does not establish that its evidence is disposable. */
388
+ export function artifactReviewGuidance(repoRoot: string, ref: string): string {
389
+ const target = isAbsolute(ref) ? basename(ref) : ref;
390
+ if (!/^[A-Za-z0-9_.-]+$/.test(target))
391
+ throw new Error("review guidance requires an artifact id or directory name");
392
+ return `After reviewing these files, if they are disposable or superseded and no review, handoff, failure investigation, or final evidence depends on them, run ${resolveBinName(repoRoot)} artifacts discard ${target} --reason "Reviewed; no longer needed" (60-minute grace). Otherwise retain them; use a hold for pending review.`;
393
+ }
394
+
305
395
  function releaseArtifactUnlocked(
306
396
  repoRoot: string,
307
397
  ref: string,
@@ -317,7 +407,7 @@ function releaseArtifactUnlocked(
317
407
  released_at: now.toISOString(),
318
408
  released_by: input.actor,
319
409
  };
320
- atomicWriteManifest(path, manifest);
410
+ atomicWriteManifest(path, manifest, now);
321
411
  return manifest;
322
412
  }
323
413
 
@@ -342,7 +432,7 @@ export function holdArtifact(
342
432
  return parsed.manifest;
343
433
  }
344
434
  const manifest = { ...parsed.manifest, holds: [...parsed.manifest.holds, hold] };
345
- atomicWriteManifest(path, manifest);
435
+ atomicWriteManifest(path, manifest, input.now);
346
436
  return manifest;
347
437
  });
348
438
  }
@@ -368,7 +458,7 @@ export function unholdArtifact(
368
458
  ...parsed.manifest,
369
459
  holds: parsed.manifest.holds.filter((item) => item.id !== id),
370
460
  };
371
- atomicWriteManifest(path, manifest);
461
+ atomicWriteManifest(path, manifest, input.now);
372
462
  return manifest;
373
463
  });
374
464
  }
@@ -515,20 +605,42 @@ export function cleanArtifacts(
515
605
 
516
606
  function cleanArtifactsUnlocked(
517
607
  repoRoot: string,
518
- opts: { yes?: boolean; now?: Date; freshnessSeconds?: number },
608
+ opts: {
609
+ yes?: boolean;
610
+ now?: Date;
611
+ freshnessSeconds?: number;
612
+ maxDeletes?: number;
613
+ timeBudgetMs?: number;
614
+ },
519
615
  ): ArtifactInventoryEntry[] {
616
+ const started = performance.now();
520
617
  const now = opts.now ?? new Date();
521
618
  const freshnessSeconds = opts.freshnessSeconds ?? 600;
522
619
  const rows = inventoryArtifacts(repoRoot, { now, freshnessSeconds });
523
620
  if (!opts.yes) return rows;
524
621
 
622
+ let attempted = 0;
525
623
  return rows.map((entry) => {
526
624
  if (entry.action !== "would-delete") return entry;
625
+ if (
626
+ attempted >= (opts.maxDeletes ?? Infinity) ||
627
+ (attempted > 0 && performance.now() - started >= (opts.timeBudgetMs ?? Infinity))
628
+ )
629
+ return entry;
630
+ attempted++;
527
631
  // Reclassify immediately before removal. A renewal, release-state change,
528
632
  // heartbeat, symlink swap, or tracked file added since inventory must win.
529
- const current = inventoryArtifacts(repoRoot, { now, freshnessSeconds }).find(
530
- (row) => row.path === entry.path,
531
- );
633
+ // Only repository-budget eviction needs a new whole-store plan. Expiry and
634
+ // per-unit size are local decisions; recheck all their guards on the target.
635
+ const current =
636
+ entry.classification === "managed-over-budget"
637
+ ? inventoryArtifacts(repoRoot, { now, freshnessSeconds }).find(
638
+ (row) => row.path === entry.path,
639
+ )
640
+ : applyArtifactUnitBudget(
641
+ repoRoot,
642
+ classifyArtifactPath(repoRoot, entry.path, now, freshnessSeconds),
643
+ );
532
644
  if (!current) {
533
645
  return { ...entry, classification: "unknown", reason: "entry disappeared", action: "keep" };
534
646
  }
@@ -569,26 +681,40 @@ const AUTO_CLEAN_STAMP = ".harnery/artifacts-auto-clean.json";
569
681
 
570
682
  export interface ArtifactAutoCleanResult {
571
683
  ran: boolean;
572
- reason: "swept" | "disabled" | "fresh" | "no-root";
684
+ reason: "swept" | "partial" | "failed" | "disabled" | "fresh" | "no-root";
573
685
  deleted: number;
574
686
  bytes: number;
575
687
  }
576
688
 
577
689
  /**
578
- * Throttled expired-artifact sweep, fired as a SessionStart effect.
690
+ * Throttled expired-artifact sweep, fired at SessionStart and before new work.
579
691
  *
580
692
  * Retention was previously enforced only when someone remembered to run
581
693
  * `artifacts clean --yes`, so expired workspaces accumulated indefinitely on
582
- * busy hosts. This runs the exact same guarded deletion (only
583
- * `managed-expired` entries, each re-classified immediately before removal;
694
+ * busy hosts. This runs the exact same guarded deletion (expired or over-budget
695
+ * managed entries, each re-classified immediately before removal;
584
696
  * unmanaged and legacy directories are never touched) at most once per
585
- * interval (default 24h), claim-first via a stamp file so concurrent session
586
- * starts do not double-sweep. Disable with `artifacts.auto_clean: false` or
697
+ * interval (default 1h after completion, 1m between partial/failed slices).
698
+ * The owner-aware lock serializes callers; interrupted attempts can retry.
699
+ * Disable with `artifacts.auto_clean: false` or
587
700
  * `HARNERY_ARTIFACT_AUTO_CLEAN=0`.
588
701
  */
589
702
  export function autoCleanArtifacts(
590
703
  repoRoot: string,
591
- opts: { now?: Date } = {},
704
+ opts: { now?: Date; maxDeletes?: number; timeBudgetMs?: number } = {},
705
+ ): ArtifactAutoCleanResult {
706
+ if (!existsSync(artifactsRoot(repoRoot))) {
707
+ return { ran: false, reason: "no-root", deleted: 0, bytes: 0 };
708
+ }
709
+ if (!artifactAutoCleanEnabled(repoRoot)) {
710
+ return { ran: false, reason: "disabled", deleted: 0, bytes: 0 };
711
+ }
712
+ return withArtifactLock(repoRoot, () => autoCleanArtifactsUnlocked(repoRoot, opts));
713
+ }
714
+
715
+ function autoCleanArtifactsUnlocked(
716
+ repoRoot: string,
717
+ opts: { now?: Date; maxDeletes?: number; timeBudgetMs?: number },
592
718
  ): ArtifactAutoCleanResult {
593
719
  const now = opts.now ?? new Date();
594
720
  assertValidDate(now, "now");
@@ -601,26 +727,73 @@ export function autoCleanArtifacts(
601
727
  const stampPath = join(resolve(repoRoot), AUTO_CLEAN_STAMP);
602
728
  const intervalMs = artifactAutoCleanIntervalHours() * 60 * 60 * 1000;
603
729
  try {
604
- const stamp = JSON.parse(readFileSync(stampPath, "utf8")) as { last_run_at?: string };
605
- const last = Date.parse(stamp.last_run_at ?? "");
606
- if (Number.isFinite(last) && now.getTime() - last < intervalMs) {
730
+ const stamp = JSON.parse(readFileSync(stampPath, "utf8")) as {
731
+ status?: string;
732
+ last_completed_at?: string;
733
+ retry_after?: string;
734
+ };
735
+ const last = Date.parse(stamp.last_completed_at ?? "");
736
+ if (
737
+ (stamp.status === "completed" &&
738
+ Number.isFinite(last) &&
739
+ now.getTime() - last < intervalMs) ||
740
+ (["partial", "failed"].includes(stamp.status ?? "") &&
741
+ Date.parse(stamp.retry_after ?? "") > now.getTime())
742
+ ) {
607
743
  return { ran: false, reason: "fresh", deleted: 0, bytes: 0 };
608
744
  }
609
745
  } catch {
610
746
  // Missing or unreadable stamp: sweep now and write a fresh one.
611
747
  }
612
- // Claim first: a crash mid-sweep costs one skipped interval, while claiming
613
- // after would let two concurrent session starts both walk the store.
614
- writeFileSync(stampPath, `${JSON.stringify({ last_run_at: now.toISOString() }, null, 2)}\n`);
615
- const rows = cleanArtifacts(repoRoot, { yes: true, now });
616
- const deletedRows = rows.filter((row) => row.action === "deleted");
617
- const deleted = deletedRows.length;
618
- const bytes = deletedRows.reduce((sum, row) => sum + (row.bytes ?? 0), 0);
619
- writeFileSync(
620
- stampPath,
621
- `${JSON.stringify({ last_run_at: now.toISOString(), deleted, bytes }, null, 2)}\n`,
622
- );
623
- return { ran: true, reason: "swept", deleted, bytes };
748
+ const maxDeletes = opts.maxDeletes ?? 10;
749
+ const timeBudgetMs = opts.timeBudgetMs ?? 5000;
750
+ if (
751
+ !Number.isSafeInteger(maxDeletes) ||
752
+ maxDeletes < 1 ||
753
+ !Number.isFinite(timeBudgetMs) ||
754
+ timeBudgetMs < 0
755
+ )
756
+ throw new Error(
757
+ "cleanup limits must allow at least one deletion and a nonnegative time budget",
758
+ );
759
+ const attempt = { last_attempt_at: now.toISOString() };
760
+ const writeStamp = (state: object) => {
761
+ const temp = `${stampPath}.${randomUUID()}.tmp`;
762
+ writeFileSync(temp, `${JSON.stringify(state, null, 2)}\n`);
763
+ renameSync(temp, stampPath);
764
+ };
765
+ writeStamp({ ...attempt, status: "running" });
766
+ try {
767
+ const rows = cleanArtifactsUnlocked(repoRoot, { yes: true, now, maxDeletes, timeBudgetMs });
768
+ const deletedRows = rows.filter((row) => row.action === "deleted");
769
+ const deleted = deletedRows.length;
770
+ const bytes = deletedRows.reduce((sum, row) => sum + (row.bytes ?? 0), 0);
771
+ const remaining = rows.filter((row) => row.action === "would-delete").length;
772
+ const failures = rows
773
+ .filter((row) => row.classification === "unknown")
774
+ .map((row) => ({ name: row.name, reason: row.reason }));
775
+ const status = failures.length ? "failed" : remaining ? "partial" : "completed";
776
+ writeStamp({
777
+ ...attempt,
778
+ status,
779
+ deleted,
780
+ bytes,
781
+ remaining,
782
+ failures,
783
+ ...(status === "completed"
784
+ ? { last_completed_at: now.toISOString() }
785
+ : { retry_after: new Date(now.getTime() + 60_000).toISOString() }),
786
+ });
787
+ return { ran: true, reason: status === "completed" ? "swept" : status, deleted, bytes };
788
+ } catch (error) {
789
+ writeStamp({
790
+ ...attempt,
791
+ status: "failed",
792
+ error: errorMessage("cleanup failed", error),
793
+ retry_after: new Date(now.getTime() + 60_000).toISOString(),
794
+ });
795
+ throw error;
796
+ }
624
797
  }
625
798
 
626
799
  export function resolveArtifactRef(repoRoot: string, ref: string): string {
@@ -674,7 +847,7 @@ function classifyArtifactPath(
674
847
  }
675
848
  const manifest = parsed.manifest;
676
849
  const bytes = safeTreeSize(path);
677
- const lastModifiedMs = safeTreeLastModified(path, now);
850
+ const lastModifiedMs = safeTreeLastModified(path, now, manifest.activity);
678
851
  const retentionAnchorMs = Date.parse(manifest.retention.renewed_at ?? manifest.created_at);
679
852
  const retentionWindowMs = Date.parse(manifest.retention.expires_at) - retentionAnchorMs;
680
853
  const effectiveLastModifiedMs = Math.max(retentionAnchorMs, lastModifiedMs ?? 0);
@@ -742,26 +915,33 @@ function classifyArtifactPath(
742
915
  };
743
916
  }
744
917
 
918
+ function applyArtifactUnitBudget(
919
+ repoRoot: string,
920
+ row: ArtifactInventoryEntry,
921
+ ): ArtifactInventoryEntry {
922
+ const maxUnitBytes = artifactMaxUnitBytes(repoRoot);
923
+ if (
924
+ row.classification === "managed-current" &&
925
+ row.bytes !== null &&
926
+ row.bytes > maxUnitBytes &&
927
+ !row.oversize_acknowledged
928
+ ) {
929
+ return {
930
+ ...row,
931
+ classification: "managed-oversize",
932
+ action: "would-delete",
933
+ reason: `bundle uses ${row.bytes} bytes, above the ${maxUnitBytes}-byte ceiling without --big`,
934
+ };
935
+ }
936
+ return row;
937
+ }
938
+
745
939
  function applyArtifactBudgets(
746
940
  repoRoot: string,
747
941
  inputRows: ArtifactInventoryEntry[],
748
942
  ): ArtifactInventoryEntry[] {
749
- const maxUnitBytes = artifactMaxUnitBytes(repoRoot);
750
943
  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
- }
944
+ const rows = inputRows.map((row) => applyArtifactUnitBudget(repoRoot, { ...row }));
765
945
 
766
946
  const managedBytes = rows.reduce(
767
947
  (sum, row) => sum + (row.artifact_id && row.bytes !== null ? row.bytes : 0),
@@ -866,11 +1046,24 @@ export function parseArtifactManifest(value: unknown): ParsedManifest | Manifest
866
1046
  if (m.oversize_acknowledged !== undefined && typeof m.oversize_acknowledged !== "boolean") {
867
1047
  return { ok: false, reason: "invalid oversize_acknowledged" };
868
1048
  }
1049
+ if (m.activity !== undefined && !validArtifactActivity(m.activity)) {
1050
+ return { ok: false, reason: "invalid activity checkpoint" };
1051
+ }
869
1052
  return { ok: true, manifest: m as ArtifactManifestV2 };
870
1053
  }
871
1054
 
872
- function atomicWriteManifest(path: string, manifest: ArtifactManifestV2): void {
1055
+ function atomicWriteManifest(path: string, manifest: ArtifactManifestV2, now = new Date()): void {
873
1056
  const target = join(path, ARTIFACT_MANIFEST);
1057
+ manifest.activity = readArtifactActivity(
1058
+ path,
1059
+ now,
1060
+ existsSync(target)
1061
+ ? manifest.activity
1062
+ : {
1063
+ last_changed_at: manifest.created_at,
1064
+ root_entries_sha256: artifactRootEntries(path),
1065
+ },
1066
+ );
874
1067
  const tmp = `${target}.tmp.${process.pid}.${randomUUID().slice(0, 8)}`;
875
1068
  try {
876
1069
  writeFileSync(tmp, `${JSON.stringify(manifest, null, 2)}\n`, {
@@ -894,7 +1087,7 @@ export interface ArtifactMigrationEntry {
894
1087
  /** Explicit, bounded v1 cutover. Preview never writes; every applied unit keeps its exact preimage. */
895
1088
  export function migrateArtifacts(
896
1089
  repoRoot: string,
897
- opts: { yes?: boolean } = {},
1090
+ opts: { yes?: boolean; now?: Date } = {},
898
1091
  ): ArtifactMigrationEntry[] {
899
1092
  const migrate = (): ArtifactMigrationEntry[] => {
900
1093
  const root = artifactsRoot(repoRoot);
@@ -961,7 +1154,7 @@ export function migrateArtifacts(
961
1154
  }
962
1155
  if (readFileSync(target, "utf8") !== preimage)
963
1156
  throw new Error("manifest changed before migration");
964
- atomicWriteManifest(path, parsed.manifest);
1157
+ atomicWriteManifest(path, parsed.manifest, opts.now);
965
1158
  return {
966
1159
  path,
967
1160
  action: "migrated",
@@ -976,25 +1169,141 @@ export function migrateArtifacts(
976
1169
  return opts.yes ? withArtifactLock(repoRoot, migrate) : migrate();
977
1170
  }
978
1171
 
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
- }
1172
+ export interface ArtifactActivityRepairEntry {
1173
+ path: string;
1174
+ action: "keep" | "would-repair" | "repaired";
1175
+ reason: string;
1176
+ previous_expires_at?: string;
1177
+ repaired_expires_at?: string;
1178
+ receipt_path?: string;
1179
+ }
1180
+
1181
+ /** Repair only a provable, untouched v1-to-v2 migration. Never delete payloads.
1182
+ * Old migrations saved the original manifest but not the original root stat.
1183
+ * The exact preimage and coincident root/manifest/receipt timestamps bound this
1184
+ * correction. Any later root change or metadata mutation requires manual review.
1185
+ */
1186
+ export function repairArtifactActivity(
1187
+ repoRoot: string,
1188
+ opts: { yes?: boolean; now?: Date } = {},
1189
+ ): ArtifactActivityRepairEntry[] {
1190
+ const repair = (): ArtifactActivityRepairEntry[] => {
1191
+ const now = opts.now ?? new Date();
1192
+ assertValidDate(now, "now");
1193
+ const preimageRoot = join(resolve(repoRoot), ".harnery/artifact-migrations");
1194
+ const preimages = new Map<string, { value: unknown; path: string; mtime: number }[]>();
1195
+ if (existsSync(preimageRoot) && !lstatSync(preimageRoot).isSymbolicLink()) {
1196
+ for (const name of readdirSync(preimageRoot)) {
1197
+ if (!/^[a-f0-9]{64}\.v1\.json$/.test(name)) continue;
1198
+ const file = join(preimageRoot, name);
1199
+ try {
1200
+ const stat = lstatSync(file);
1201
+ if (!stat.isFile() || stat.isSymbolicLink() || stat.size > 1024 * 1024) continue;
1202
+ const bytes = readFileSync(file, "utf8");
1203
+ if (`${createHash("sha256").update(bytes).digest("hex")}.v1.json` !== name) continue;
1204
+ const old = JSON.parse(bytes);
1205
+ if (
1206
+ old?.schema_version !== 1 ||
1207
+ Object.hasOwn(old, "holds") ||
1208
+ Object.hasOwn(old, "activity")
1209
+ )
1210
+ continue;
1211
+ const upgraded = { ...old, schema_version: ARTIFACT_SCHEMA_VERSION, holds: [] };
1212
+ const parsed = parseArtifactManifest(upgraded);
1213
+ if (!parsed.ok) continue;
1214
+ const items = preimages.get(parsed.manifest.artifact_id) ?? [];
1215
+ items.push({ value: upgraded, path: file, mtime: stat.mtimeMs });
1216
+ preimages.set(parsed.manifest.artifact_id, items);
1217
+ } catch {
1218
+ // Unverifiable preimages confer no repair authority.
1219
+ }
1220
+ }
1221
+ }
1222
+ const root = artifactsRoot(repoRoot);
1223
+ if (!existsSync(root)) return [];
1224
+ return readdirSync(root)
1225
+ .sort()
1226
+ .map((name): ArtifactActivityRepairEntry => {
1227
+ const path = join(root, name);
1228
+ try {
1229
+ const parsed = readManifest(path);
1230
+ if (!parsed.ok) throw new Error(parsed.reason);
1231
+ const manifest = parsed.manifest;
1232
+ if (manifest.activity)
1233
+ return { path, action: "keep", reason: "activity already recorded" };
1234
+ const source = preimages
1235
+ .get(manifest.artifact_id)
1236
+ ?.find((item) => isDeepStrictEqual(item.value, manifest));
1237
+ if (!source) throw new Error("no exact migration preimage");
1238
+ if (containsTrackedPath(repoRoot, path))
1239
+ throw new Error("artifact contains tracked files");
1240
+ const target = join(path, ARTIFACT_MANIFEST);
1241
+ const before = readFileSync(target, "utf8");
1242
+ const unitStat = lstatSync(path);
1243
+ const manifestStat = lstatSync(target);
1244
+ if (
1245
+ unitStat.mtimeMs !== manifestStat.mtimeMs ||
1246
+ unitStat.ctimeMs !== manifestStat.ctimeMs ||
1247
+ manifestStat.mtimeMs !== manifestStat.ctimeMs ||
1248
+ manifestStat.mtimeMs < source.mtime ||
1249
+ manifestStat.mtimeMs - source.mtime > 5000
1250
+ )
1251
+ throw new Error("root or manifest changed outside the recorded migration");
1252
+ const anchor = manifest.retention.renewed_at ?? manifest.created_at;
1253
+ const oldActivity = readArtifactActivity(path, now);
1254
+ const activity = readArtifactActivity(path, now, {
1255
+ last_changed_at: anchor,
1256
+ root_entries_sha256: artifactRootEntries(path),
1257
+ });
1258
+ const windowMs = Date.parse(manifest.retention.expires_at) - Date.parse(anchor);
1259
+ const expiry = (changedAt: string) =>
1260
+ new Date(Math.max(Date.parse(anchor), Date.parse(changedAt)) + windowMs).toISOString();
1261
+ const row: ArtifactActivityRepairEntry = {
1262
+ path,
1263
+ action: opts.yes ? "repaired" : "would-repair",
1264
+ reason:
1265
+ "exact migration preimage; unchanged root; payload activity and retention preserved",
1266
+ previous_expires_at: expiry(oldActivity.last_changed_at),
1267
+ repaired_expires_at: expiry(activity.last_changed_at),
1268
+ };
1269
+ if (!opts.yes) return row;
1270
+ const receipt = join(
1271
+ preimageRoot,
1272
+ `${manifest.artifact_id}.${randomUUID()}.activity-repair.json`,
1273
+ );
1274
+ writeFileSync(
1275
+ receipt,
1276
+ `${JSON.stringify(
1277
+ {
1278
+ schema_version: 1,
1279
+ repaired_at: now.toISOString(),
1280
+ path: relative(repoRoot, path),
1281
+ preimage_path: relative(repoRoot, source.path),
1282
+ original_manifest: before,
1283
+ activity,
1284
+ previous_expires_at: row.previous_expires_at,
1285
+ repaired_expires_at: row.repaired_expires_at,
1286
+ },
1287
+ null,
1288
+ 2,
1289
+ )}\n`,
1290
+ { flag: "wx", mode: 0o600 },
1291
+ );
1292
+ const current = lstatSync(path);
1293
+ if (
1294
+ readFileSync(target, "utf8") !== before ||
1295
+ current.mtimeMs !== unitStat.mtimeMs ||
1296
+ current.ctimeMs !== unitStat.ctimeMs
1297
+ )
1298
+ throw new Error("artifact changed before repair");
1299
+ atomicWriteManifest(path, { ...manifest, activity }, now);
1300
+ return { ...row, receipt_path: receipt };
1301
+ } catch (error) {
1302
+ return { path, action: "keep", reason: errorMessage("activity repair refused", error) };
1303
+ }
1304
+ });
1305
+ };
1306
+ return opts.yes ? withArtifactLock(repoRoot, repair) : repair();
998
1307
  }
999
1308
 
1000
1309
  function validHoldId(value: unknown): value is string {
@@ -1082,24 +1391,9 @@ function safeTreeSize(path: string): number | null {
1082
1391
  * symlinks. A small future tolerance protects a write racing the inventory
1083
1392
  * scan; timestamps farther ahead are ignored as clock-skewed metadata.
1084
1393
  */
1085
- function safeTreeLastModified(path: string, now: Date): number | null {
1086
- const nowMs = now.getTime();
1087
- const futureToleranceMs = 5 * 60 * 1000;
1394
+ function safeTreeLastModified(path: string, now: Date, activity?: ArtifactActivity): number | null {
1088
1395
  try {
1089
- const st = lstatSync(path);
1090
- let latest = 0;
1091
- for (const timestamp of [st.mtimeMs, st.ctimeMs]) {
1092
- if (Number.isFinite(timestamp) && timestamp <= nowMs + futureToleranceMs) {
1093
- latest = Math.max(latest, Math.min(timestamp, nowMs));
1094
- }
1095
- }
1096
- if (st.isSymbolicLink() || !st.isDirectory()) return latest;
1097
- for (const child of readdirSync(path)) {
1098
- const childLatest = safeTreeLastModified(join(path, child), now);
1099
- if (childLatest === null) return null;
1100
- latest = Math.max(latest, childLatest);
1101
- }
1102
- return latest;
1396
+ return Date.parse(readArtifactActivity(path, now, activity).last_changed_at);
1103
1397
  } catch {
1104
1398
  return null;
1105
1399
  }