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
@@ -9,12 +9,16 @@
9
9
  */
10
10
  import { spawnSync } from "node:child_process";
11
11
  import { createHash, randomUUID } from "node:crypto";
12
- import { existsSync, lstatSync, mkdirSync, readdirSync, readFileSync, renameSync, rmdirSync, rmSync, writeFileSync, } from "node:fs";
12
+ import { existsSync, lstatSync, mkdirSync, readdirSync, readFileSync, renameSync, rmSync, writeFileSync, } from "node:fs";
13
13
  import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
14
+ import { isDeepStrictEqual } from "node:util";
14
15
  import { readLiveCoordinationRow } from "../agents/state/live-coordination-view.js";
15
- import { artifactAutoCleanEnabled, artifactAutoCleanIntervalHours, artifactDefaultRetentionDays, artifactMaxBytes, artifactMaxUnitBytes, } from "../config.js";
16
+ import { artifactAutoCleanEnabled, artifactAutoCleanIntervalHours, artifactDefaultRetentionDays, artifactMaxBytes, artifactMaxUnitBytes, coordFreshnessSeconds, resolveBinName, } from "../config.js";
17
+ import { artifactRootEntries, readArtifactActivity, validArtifactActivity, } from "./activity.js";
16
18
  import { ARTIFACT_MANIFEST, ARTIFACT_SCHEMA_VERSION, ARTIFACTS_DIR } from "./constants.js";
19
+ import { withArtifactLock } from "./mutation-lock.js";
17
20
  export { ARTIFACT_MANIFEST, ARTIFACT_SCHEMA_VERSION, ARTIFACTS_DIR } from "./constants.js";
21
+ export { ARTIFACT_DELIVERY_AUTO_ITEM_LIMIT, ARTIFACT_DELIVERY_MANIFEST, ARTIFACT_DELIVERY_SCHEMA_VERSION, parseArtifactDeliverySpec, readArtifactDeliveryManifest, renderArtifactDeliveryCard, resolveArtifactDeliveryManifest, writeArtifactDeliveryManifest, } from "./delivery-card.js";
18
22
  export function artifactCapabilities() {
19
23
  return {
20
24
  schema_version: ARTIFACT_SCHEMA_VERSION,
@@ -22,6 +26,8 @@ export function artifactCapabilities() {
22
26
  atomic_create_holds: true,
23
27
  owner_scoped_unhold: true,
24
28
  explicit_v1_migration: true,
29
+ minute_retention: true,
30
+ discard_after_review: true,
25
31
  };
26
32
  }
27
33
  export function artifactsRoot(repoRoot) {
@@ -72,7 +78,7 @@ function createArtifactUnlocked(repoRoot, input) {
72
78
  },
73
79
  ...(input.big ? { oversize_acknowledged: true } : {}),
74
80
  };
75
- atomicWriteManifest(path, manifest);
81
+ atomicWriteManifest(path, manifest, now);
76
82
  return { path, manifest };
77
83
  }
78
84
  export function inventoryArtifacts(repoRoot, opts = {}) {
@@ -113,7 +119,9 @@ export function renewArtifact(repoRoot, ref, days, reason, input = {}) {
113
119
  function renewArtifactUnlocked(repoRoot, ref, days, reason, input) {
114
120
  const now = input.now ?? new Date();
115
121
  assertValidDate(now, "now");
116
- const retentionDays = positiveDays(days);
122
+ const expiresAt = typeof days === "number"
123
+ ? addDays(now, positiveDays(days))
124
+ : addMinutes(now, positiveMinutes(days.minutes));
117
125
  const why = reason.trim();
118
126
  if (!why)
119
127
  throw new Error("renewal reason must not be empty");
@@ -124,17 +132,66 @@ function renewArtifactUnlocked(repoRoot, ref, days, reason, input) {
124
132
  const manifest = {
125
133
  ...parsed.manifest,
126
134
  retention: {
127
- expires_at: addDays(now, retentionDays).toISOString(),
135
+ expires_at: expiresAt.toISOString(),
128
136
  renewed_at: now.toISOString(),
129
137
  reason: why,
130
138
  },
131
139
  };
132
- atomicWriteManifest(path, manifest);
140
+ atomicWriteManifest(path, manifest, now);
133
141
  return manifest;
134
142
  }
135
143
  export function releaseArtifact(repoRoot, ref, input = {}) {
136
144
  return withArtifactLock(repoRoot, () => releaseArtifactUnlocked(repoRoot, ref, input));
137
145
  }
146
+ /** Retire reviewed evidence without deleting it or extending an earlier deadline. */
147
+ export function discardArtifact(repoRoot, ref, reason, input = {}) {
148
+ return withArtifactLock(repoRoot, () => {
149
+ const now = input.now ?? new Date();
150
+ assertValidDate(now, "now");
151
+ const minutes = positiveMinutes(input.minutes ?? 60);
152
+ const why = reason.trim();
153
+ if (!why)
154
+ throw new Error("discard requires a reason confirming the files are no longer needed");
155
+ const path = resolveArtifactRef(repoRoot, ref);
156
+ const entry = classifyArtifactPath(repoRoot, path, now, coordFreshnessSeconds(repoRoot));
157
+ if (!["managed-current", "managed-expired", "managed-active"].includes(entry.classification) ||
158
+ (entry.classification === "managed-active" &&
159
+ entry.owner_instance_id !== input.actor?.instance_id))
160
+ throw new Error(`cannot discard artifact: ${entry.reason}`);
161
+ const parsed = readManifest(path);
162
+ if (!parsed.ok)
163
+ throw new Error(parsed.reason);
164
+ const manifest = parsed.manifest;
165
+ const remaining = Date.parse(entry.expires_at) - now.getTime();
166
+ const priorWindow = Date.parse(manifest.retention.expires_at) -
167
+ Date.parse(manifest.retention.renewed_at ?? manifest.created_at);
168
+ const windowMs = Math.min(minutes * 60_000, remaining, priorWindow);
169
+ const updated = {
170
+ ...manifest,
171
+ retention: {
172
+ ...manifest.retention,
173
+ ...(windowMs > 0
174
+ ? {
175
+ renewed_at: now.toISOString(),
176
+ expires_at: new Date(now.getTime() + windowMs).toISOString(),
177
+ }
178
+ : {}),
179
+ reason: why,
180
+ },
181
+ released_at: now.toISOString(),
182
+ released_by: input.actor,
183
+ };
184
+ atomicWriteManifest(path, updated, now);
185
+ return updated;
186
+ });
187
+ }
188
+ /** Advice only: a successful check does not establish that its evidence is disposable. */
189
+ export function artifactReviewGuidance(repoRoot, ref) {
190
+ const target = isAbsolute(ref) ? basename(ref) : ref;
191
+ if (!/^[A-Za-z0-9_.-]+$/.test(target))
192
+ throw new Error("review guidance requires an artifact id or directory name");
193
+ 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.`;
194
+ }
138
195
  function releaseArtifactUnlocked(repoRoot, ref, input) {
139
196
  const now = input.now ?? new Date();
140
197
  assertValidDate(now, "now");
@@ -147,7 +204,7 @@ function releaseArtifactUnlocked(repoRoot, ref, input) {
147
204
  released_at: now.toISOString(),
148
205
  released_by: input.actor,
149
206
  };
150
- atomicWriteManifest(path, manifest);
207
+ atomicWriteManifest(path, manifest, now);
151
208
  return manifest;
152
209
  }
153
210
  export function holdArtifact(repoRoot, ref, input) {
@@ -166,7 +223,7 @@ export function holdArtifact(repoRoot, ref, input) {
166
223
  return parsed.manifest;
167
224
  }
168
225
  const manifest = { ...parsed.manifest, holds: [...parsed.manifest.holds, hold] };
169
- atomicWriteManifest(path, manifest);
226
+ atomicWriteManifest(path, manifest, input.now);
170
227
  return manifest;
171
228
  });
172
229
  }
@@ -190,7 +247,7 @@ export function unholdArtifact(repoRoot, ref, id, input) {
190
247
  ...parsed.manifest,
191
248
  holds: parsed.manifest.holds.filter((item) => item.id !== id),
192
249
  };
193
- atomicWriteManifest(path, manifest);
250
+ atomicWriteManifest(path, manifest, input.now);
194
251
  return manifest;
195
252
  });
196
253
  }
@@ -310,17 +367,27 @@ export function cleanArtifacts(repoRoot, opts = {}) {
310
367
  }
311
368
  }
312
369
  function cleanArtifactsUnlocked(repoRoot, opts) {
370
+ const started = performance.now();
313
371
  const now = opts.now ?? new Date();
314
372
  const freshnessSeconds = opts.freshnessSeconds ?? 600;
315
373
  const rows = inventoryArtifacts(repoRoot, { now, freshnessSeconds });
316
374
  if (!opts.yes)
317
375
  return rows;
376
+ let attempted = 0;
318
377
  return rows.map((entry) => {
319
378
  if (entry.action !== "would-delete")
320
379
  return entry;
380
+ if (attempted >= (opts.maxDeletes ?? Infinity) ||
381
+ (attempted > 0 && performance.now() - started >= (opts.timeBudgetMs ?? Infinity)))
382
+ return entry;
383
+ attempted++;
321
384
  // Reclassify immediately before removal. A renewal, release-state change,
322
385
  // heartbeat, symlink swap, or tracked file added since inventory must win.
323
- const current = inventoryArtifacts(repoRoot, { now, freshnessSeconds }).find((row) => row.path === entry.path);
386
+ // Only repository-budget eviction needs a new whole-store plan. Expiry and
387
+ // per-unit size are local decisions; recheck all their guards on the target.
388
+ const current = entry.classification === "managed-over-budget"
389
+ ? inventoryArtifacts(repoRoot, { now, freshnessSeconds }).find((row) => row.path === entry.path)
390
+ : applyArtifactUnitBudget(repoRoot, classifyArtifactPath(repoRoot, entry.path, now, freshnessSeconds));
324
391
  if (!current) {
325
392
  return { ...entry, classification: "unknown", reason: "entry disappeared", action: "keep" };
326
393
  }
@@ -357,18 +424,28 @@ function cleanArtifactsUnlocked(repoRoot, opts) {
357
424
  /** Sibling of the artifacts root so the stamp never appears in the inventory scan. */
358
425
  const AUTO_CLEAN_STAMP = ".harnery/artifacts-auto-clean.json";
359
426
  /**
360
- * Throttled expired-artifact sweep, fired as a SessionStart effect.
427
+ * Throttled expired-artifact sweep, fired at SessionStart and before new work.
361
428
  *
362
429
  * Retention was previously enforced only when someone remembered to run
363
430
  * `artifacts clean --yes`, so expired workspaces accumulated indefinitely on
364
- * busy hosts. This runs the exact same guarded deletion (only
365
- * `managed-expired` entries, each re-classified immediately before removal;
431
+ * busy hosts. This runs the exact same guarded deletion (expired or over-budget
432
+ * managed entries, each re-classified immediately before removal;
366
433
  * unmanaged and legacy directories are never touched) at most once per
367
- * interval (default 24h), claim-first via a stamp file so concurrent session
368
- * starts do not double-sweep. Disable with `artifacts.auto_clean: false` or
434
+ * interval (default 1h after completion, 1m between partial/failed slices).
435
+ * The owner-aware lock serializes callers; interrupted attempts can retry.
436
+ * Disable with `artifacts.auto_clean: false` or
369
437
  * `HARNERY_ARTIFACT_AUTO_CLEAN=0`.
370
438
  */
371
439
  export function autoCleanArtifacts(repoRoot, opts = {}) {
440
+ if (!existsSync(artifactsRoot(repoRoot))) {
441
+ return { ran: false, reason: "no-root", deleted: 0, bytes: 0 };
442
+ }
443
+ if (!artifactAutoCleanEnabled(repoRoot)) {
444
+ return { ran: false, reason: "disabled", deleted: 0, bytes: 0 };
445
+ }
446
+ return withArtifactLock(repoRoot, () => autoCleanArtifactsUnlocked(repoRoot, opts));
447
+ }
448
+ function autoCleanArtifactsUnlocked(repoRoot, opts) {
372
449
  const now = opts.now ?? new Date();
373
450
  assertValidDate(now, "now");
374
451
  if (!existsSync(artifactsRoot(repoRoot))) {
@@ -381,23 +458,64 @@ export function autoCleanArtifacts(repoRoot, opts = {}) {
381
458
  const intervalMs = artifactAutoCleanIntervalHours() * 60 * 60 * 1000;
382
459
  try {
383
460
  const stamp = JSON.parse(readFileSync(stampPath, "utf8"));
384
- const last = Date.parse(stamp.last_run_at ?? "");
385
- if (Number.isFinite(last) && now.getTime() - last < intervalMs) {
461
+ const last = Date.parse(stamp.last_completed_at ?? "");
462
+ if ((stamp.status === "completed" &&
463
+ Number.isFinite(last) &&
464
+ now.getTime() - last < intervalMs) ||
465
+ (["partial", "failed"].includes(stamp.status ?? "") &&
466
+ Date.parse(stamp.retry_after ?? "") > now.getTime())) {
386
467
  return { ran: false, reason: "fresh", deleted: 0, bytes: 0 };
387
468
  }
388
469
  }
389
470
  catch {
390
471
  // Missing or unreadable stamp: sweep now and write a fresh one.
391
472
  }
392
- // Claim first: a crash mid-sweep costs one skipped interval, while claiming
393
- // after would let two concurrent session starts both walk the store.
394
- writeFileSync(stampPath, `${JSON.stringify({ last_run_at: now.toISOString() }, null, 2)}\n`);
395
- const rows = cleanArtifacts(repoRoot, { yes: true, now });
396
- const deletedRows = rows.filter((row) => row.action === "deleted");
397
- const deleted = deletedRows.length;
398
- const bytes = deletedRows.reduce((sum, row) => sum + (row.bytes ?? 0), 0);
399
- writeFileSync(stampPath, `${JSON.stringify({ last_run_at: now.toISOString(), deleted, bytes }, null, 2)}\n`);
400
- return { ran: true, reason: "swept", deleted, bytes };
473
+ const maxDeletes = opts.maxDeletes ?? 10;
474
+ const timeBudgetMs = opts.timeBudgetMs ?? 5000;
475
+ if (!Number.isSafeInteger(maxDeletes) ||
476
+ maxDeletes < 1 ||
477
+ !Number.isFinite(timeBudgetMs) ||
478
+ timeBudgetMs < 0)
479
+ throw new Error("cleanup limits must allow at least one deletion and a nonnegative time budget");
480
+ const attempt = { last_attempt_at: now.toISOString() };
481
+ const writeStamp = (state) => {
482
+ const temp = `${stampPath}.${randomUUID()}.tmp`;
483
+ writeFileSync(temp, `${JSON.stringify(state, null, 2)}\n`);
484
+ renameSync(temp, stampPath);
485
+ };
486
+ writeStamp({ ...attempt, status: "running" });
487
+ try {
488
+ const rows = cleanArtifactsUnlocked(repoRoot, { yes: true, now, maxDeletes, timeBudgetMs });
489
+ const deletedRows = rows.filter((row) => row.action === "deleted");
490
+ const deleted = deletedRows.length;
491
+ const bytes = deletedRows.reduce((sum, row) => sum + (row.bytes ?? 0), 0);
492
+ const remaining = rows.filter((row) => row.action === "would-delete").length;
493
+ const failures = rows
494
+ .filter((row) => row.classification === "unknown")
495
+ .map((row) => ({ name: row.name, reason: row.reason }));
496
+ const status = failures.length ? "failed" : remaining ? "partial" : "completed";
497
+ writeStamp({
498
+ ...attempt,
499
+ status,
500
+ deleted,
501
+ bytes,
502
+ remaining,
503
+ failures,
504
+ ...(status === "completed"
505
+ ? { last_completed_at: now.toISOString() }
506
+ : { retry_after: new Date(now.getTime() + 60_000).toISOString() }),
507
+ });
508
+ return { ran: true, reason: status === "completed" ? "swept" : status, deleted, bytes };
509
+ }
510
+ catch (error) {
511
+ writeStamp({
512
+ ...attempt,
513
+ status: "failed",
514
+ error: errorMessage("cleanup failed", error),
515
+ retry_after: new Date(now.getTime() + 60_000).toISOString(),
516
+ });
517
+ throw error;
518
+ }
401
519
  }
402
520
  export function resolveArtifactRef(repoRoot, ref) {
403
521
  const root = artifactsRoot(repoRoot);
@@ -446,7 +564,7 @@ function classifyArtifactPath(repoRoot, path, now, freshnessSeconds) {
446
564
  }
447
565
  const manifest = parsed.manifest;
448
566
  const bytes = safeTreeSize(path);
449
- const lastModifiedMs = safeTreeLastModified(path, now);
567
+ const lastModifiedMs = safeTreeLastModified(path, now, manifest.activity);
450
568
  const retentionAnchorMs = Date.parse(manifest.retention.renewed_at ?? manifest.created_at);
451
569
  const retentionWindowMs = Date.parse(manifest.retention.expires_at) - retentionAnchorMs;
452
570
  const effectiveLastModifiedMs = Math.max(retentionAnchorMs, lastModifiedMs ?? 0);
@@ -513,20 +631,24 @@ function classifyArtifactPath(repoRoot, path, now, freshnessSeconds) {
513
631
  action: "would-delete",
514
632
  };
515
633
  }
516
- function applyArtifactBudgets(repoRoot, inputRows) {
634
+ function applyArtifactUnitBudget(repoRoot, row) {
517
635
  const maxUnitBytes = artifactMaxUnitBytes(repoRoot);
518
- const maxBytes = artifactMaxBytes(repoRoot);
519
- const rows = inputRows.map((row) => ({ ...row }));
520
- for (const row of rows) {
521
- if (row.classification === "managed-current" &&
522
- row.bytes !== null &&
523
- row.bytes > maxUnitBytes &&
524
- !row.oversize_acknowledged) {
525
- row.classification = "managed-oversize";
526
- row.reason = `bundle uses ${row.bytes} bytes, above the ${maxUnitBytes}-byte ceiling without --big`;
527
- row.action = "would-delete";
528
- }
636
+ if (row.classification === "managed-current" &&
637
+ row.bytes !== null &&
638
+ row.bytes > maxUnitBytes &&
639
+ !row.oversize_acknowledged) {
640
+ return {
641
+ ...row,
642
+ classification: "managed-oversize",
643
+ action: "would-delete",
644
+ reason: `bundle uses ${row.bytes} bytes, above the ${maxUnitBytes}-byte ceiling without --big`,
645
+ };
529
646
  }
647
+ return row;
648
+ }
649
+ function applyArtifactBudgets(repoRoot, inputRows) {
650
+ const maxBytes = artifactMaxBytes(repoRoot);
651
+ const rows = inputRows.map((row) => applyArtifactUnitBudget(repoRoot, { ...row }));
530
652
  const managedBytes = rows.reduce((sum, row) => sum + (row.artifact_id && row.bytes !== null ? row.bytes : 0), 0);
531
653
  let retainedBytes = managedBytes -
532
654
  rows.reduce((sum, row) => sum + (row.action === "would-delete" && row.bytes !== null ? row.bytes : 0), 0);
@@ -614,10 +736,19 @@ export function parseArtifactManifest(value) {
614
736
  if (m.oversize_acknowledged !== undefined && typeof m.oversize_acknowledged !== "boolean") {
615
737
  return { ok: false, reason: "invalid oversize_acknowledged" };
616
738
  }
739
+ if (m.activity !== undefined && !validArtifactActivity(m.activity)) {
740
+ return { ok: false, reason: "invalid activity checkpoint" };
741
+ }
617
742
  return { ok: true, manifest: m };
618
743
  }
619
- function atomicWriteManifest(path, manifest) {
744
+ function atomicWriteManifest(path, manifest, now = new Date()) {
620
745
  const target = join(path, ARTIFACT_MANIFEST);
746
+ manifest.activity = readArtifactActivity(path, now, existsSync(target)
747
+ ? manifest.activity
748
+ : {
749
+ last_changed_at: manifest.created_at,
750
+ root_entries_sha256: artifactRootEntries(path),
751
+ });
621
752
  const tmp = `${target}.tmp.${process.pid}.${randomUUID().slice(0, 8)}`;
622
753
  try {
623
754
  writeFileSync(tmp, `${JSON.stringify(manifest, null, 2)}\n`, {
@@ -693,7 +824,7 @@ export function migrateArtifacts(repoRoot, opts = {}) {
693
824
  }
694
825
  if (readFileSync(target, "utf8") !== preimage)
695
826
  throw new Error("manifest changed before migration");
696
- atomicWriteManifest(path, parsed.manifest);
827
+ atomicWriteManifest(path, parsed.manifest, opts.now);
697
828
  return {
698
829
  path,
699
830
  action: "migrated",
@@ -708,22 +839,120 @@ export function migrateArtifacts(repoRoot, opts = {}) {
708
839
  };
709
840
  return opts.yes ? withArtifactLock(repoRoot, migrate) : migrate();
710
841
  }
711
- /** A persistent directory lock fails closed on contention or a crashed owner. */
712
- function withArtifactLock(repoRoot, action) {
713
- const lock = join(resolve(repoRoot), ".harnery/artifacts-mutation.lock");
714
- mkdirSync(dirname(lock), { recursive: true });
715
- try {
716
- mkdirSync(lock);
717
- }
718
- catch (error) {
719
- throw new Error(errorMessage("artifact store mutation lock unavailable; retry after its owner finishes", error));
720
- }
721
- try {
722
- return action();
723
- }
724
- finally {
725
- rmdirSync(lock);
726
- }
842
+ /** Repair only a provable, untouched v1-to-v2 migration. Never delete payloads.
843
+ * Old migrations saved the original manifest but not the original root stat.
844
+ * The exact preimage and coincident root/manifest/receipt timestamps bound this
845
+ * correction. Any later root change or metadata mutation requires manual review.
846
+ */
847
+ export function repairArtifactActivity(repoRoot, opts = {}) {
848
+ const repair = () => {
849
+ const now = opts.now ?? new Date();
850
+ assertValidDate(now, "now");
851
+ const preimageRoot = join(resolve(repoRoot), ".harnery/artifact-migrations");
852
+ const preimages = new Map();
853
+ if (existsSync(preimageRoot) && !lstatSync(preimageRoot).isSymbolicLink()) {
854
+ for (const name of readdirSync(preimageRoot)) {
855
+ if (!/^[a-f0-9]{64}\.v1\.json$/.test(name))
856
+ continue;
857
+ const file = join(preimageRoot, name);
858
+ try {
859
+ const stat = lstatSync(file);
860
+ if (!stat.isFile() || stat.isSymbolicLink() || stat.size > 1024 * 1024)
861
+ continue;
862
+ const bytes = readFileSync(file, "utf8");
863
+ if (`${createHash("sha256").update(bytes).digest("hex")}.v1.json` !== name)
864
+ continue;
865
+ const old = JSON.parse(bytes);
866
+ if (old?.schema_version !== 1 ||
867
+ Object.hasOwn(old, "holds") ||
868
+ Object.hasOwn(old, "activity"))
869
+ continue;
870
+ const upgraded = { ...old, schema_version: ARTIFACT_SCHEMA_VERSION, holds: [] };
871
+ const parsed = parseArtifactManifest(upgraded);
872
+ if (!parsed.ok)
873
+ continue;
874
+ const items = preimages.get(parsed.manifest.artifact_id) ?? [];
875
+ items.push({ value: upgraded, path: file, mtime: stat.mtimeMs });
876
+ preimages.set(parsed.manifest.artifact_id, items);
877
+ }
878
+ catch {
879
+ // Unverifiable preimages confer no repair authority.
880
+ }
881
+ }
882
+ }
883
+ const root = artifactsRoot(repoRoot);
884
+ if (!existsSync(root))
885
+ return [];
886
+ return readdirSync(root)
887
+ .sort()
888
+ .map((name) => {
889
+ const path = join(root, name);
890
+ try {
891
+ const parsed = readManifest(path);
892
+ if (!parsed.ok)
893
+ throw new Error(parsed.reason);
894
+ const manifest = parsed.manifest;
895
+ if (manifest.activity)
896
+ return { path, action: "keep", reason: "activity already recorded" };
897
+ const source = preimages
898
+ .get(manifest.artifact_id)
899
+ ?.find((item) => isDeepStrictEqual(item.value, manifest));
900
+ if (!source)
901
+ throw new Error("no exact migration preimage");
902
+ if (containsTrackedPath(repoRoot, path))
903
+ throw new Error("artifact contains tracked files");
904
+ const target = join(path, ARTIFACT_MANIFEST);
905
+ const before = readFileSync(target, "utf8");
906
+ const unitStat = lstatSync(path);
907
+ const manifestStat = lstatSync(target);
908
+ if (unitStat.mtimeMs !== manifestStat.mtimeMs ||
909
+ unitStat.ctimeMs !== manifestStat.ctimeMs ||
910
+ manifestStat.mtimeMs !== manifestStat.ctimeMs ||
911
+ manifestStat.mtimeMs < source.mtime ||
912
+ manifestStat.mtimeMs - source.mtime > 5000)
913
+ throw new Error("root or manifest changed outside the recorded migration");
914
+ const anchor = manifest.retention.renewed_at ?? manifest.created_at;
915
+ const oldActivity = readArtifactActivity(path, now);
916
+ const activity = readArtifactActivity(path, now, {
917
+ last_changed_at: anchor,
918
+ root_entries_sha256: artifactRootEntries(path),
919
+ });
920
+ const windowMs = Date.parse(manifest.retention.expires_at) - Date.parse(anchor);
921
+ const expiry = (changedAt) => new Date(Math.max(Date.parse(anchor), Date.parse(changedAt)) + windowMs).toISOString();
922
+ const row = {
923
+ path,
924
+ action: opts.yes ? "repaired" : "would-repair",
925
+ reason: "exact migration preimage; unchanged root; payload activity and retention preserved",
926
+ previous_expires_at: expiry(oldActivity.last_changed_at),
927
+ repaired_expires_at: expiry(activity.last_changed_at),
928
+ };
929
+ if (!opts.yes)
930
+ return row;
931
+ const receipt = join(preimageRoot, `${manifest.artifact_id}.${randomUUID()}.activity-repair.json`);
932
+ writeFileSync(receipt, `${JSON.stringify({
933
+ schema_version: 1,
934
+ repaired_at: now.toISOString(),
935
+ path: relative(repoRoot, path),
936
+ preimage_path: relative(repoRoot, source.path),
937
+ original_manifest: before,
938
+ activity,
939
+ previous_expires_at: row.previous_expires_at,
940
+ repaired_expires_at: row.repaired_expires_at,
941
+ }, null, 2)}\n`, { flag: "wx", mode: 0o600 });
942
+ const current = lstatSync(path);
943
+ if (readFileSync(target, "utf8") !== before ||
944
+ current.mtimeMs !== unitStat.mtimeMs ||
945
+ current.ctimeMs !== unitStat.ctimeMs)
946
+ throw new Error("artifact changed before repair");
947
+ atomicWriteManifest(path, { ...manifest, activity }, now);
948
+ return { ...row, receipt_path: receipt };
949
+ }
950
+ catch (error) {
951
+ return { path, action: "keep", reason: errorMessage("activity repair refused", error) };
952
+ }
953
+ });
954
+ };
955
+ return opts.yes ? withArtifactLock(repoRoot, repair) : repair();
727
956
  }
728
957
  function validHoldId(value) {
729
958
  return typeof value === "string" && /^[A-Za-z0-9][A-Za-z0-9_.:-]{0,127}$/.test(value);
@@ -805,26 +1034,9 @@ function safeTreeSize(path) {
805
1034
  * symlinks. A small future tolerance protects a write racing the inventory
806
1035
  * scan; timestamps farther ahead are ignored as clock-skewed metadata.
807
1036
  */
808
- function safeTreeLastModified(path, now) {
809
- const nowMs = now.getTime();
810
- const futureToleranceMs = 5 * 60 * 1000;
1037
+ function safeTreeLastModified(path, now, activity) {
811
1038
  try {
812
- const st = lstatSync(path);
813
- let latest = 0;
814
- for (const timestamp of [st.mtimeMs, st.ctimeMs]) {
815
- if (Number.isFinite(timestamp) && timestamp <= nowMs + futureToleranceMs) {
816
- latest = Math.max(latest, Math.min(timestamp, nowMs));
817
- }
818
- }
819
- if (st.isSymbolicLink() || !st.isDirectory())
820
- return latest;
821
- for (const child of readdirSync(path)) {
822
- const childLatest = safeTreeLastModified(join(path, child), now);
823
- if (childLatest === null)
824
- return null;
825
- latest = Math.max(latest, childLatest);
826
- }
827
- return latest;
1039
+ return Date.parse(readArtifactActivity(path, now, activity).last_changed_at);
828
1040
  }
829
1041
  catch {
830
1042
  return null;
@@ -0,0 +1,2 @@
1
+ export declare function withArtifactLock<T>(repoRoot: string, action: () => T): T;
2
+ //# sourceMappingURL=mutation-lock.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mutation-lock.d.ts","sourceRoot":"","sources":["../../../src/core/artifacts/mutation-lock.ts"],"names":[],"mappings":"AAqEA,wBAAgB,gBAAgB,CAAC,CAAC,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,GAAG,CAAC,CAoCxE"}
@@ -0,0 +1,91 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { lstatSync, mkdirSync, readdirSync, readFileSync, rmdirSync, rmSync, unlinkSync, writeFileSync, } from "node:fs";
3
+ import { hostname } from "node:os";
4
+ import { dirname, join, resolve } from "node:path";
5
+ import { processStartToken } from "../agents/state/proc-start.js";
6
+ /** Age is never authority to evict an owner. Unknown owners fail closed. */
7
+ function reclaimDeadOwner(lock) {
8
+ try {
9
+ const stat = lstatSync(lock);
10
+ if (!stat.isDirectory() || stat.isSymbolicLink())
11
+ return false;
12
+ const names = readdirSync(lock);
13
+ if (names.length !== 1 || !/^owner-[0-9a-f-]{36}\.json$/.test(names[0]))
14
+ return false;
15
+ const ownerPath = join(lock, names[0]);
16
+ const ownerStat = lstatSync(ownerPath);
17
+ if (!ownerStat.isFile() || ownerStat.isSymbolicLink() || ownerStat.size > 4096)
18
+ return false;
19
+ const owner = JSON.parse(readFileSync(ownerPath, "utf8"));
20
+ if (owner.version !== 1 ||
21
+ owner.host !== hostname() ||
22
+ !Number.isSafeInteger(owner.pid) ||
23
+ owner.pid <= 0 ||
24
+ !(owner.process_start === null || typeof owner.process_start === "string"))
25
+ return false;
26
+ let dead = false;
27
+ try {
28
+ process.kill(owner.pid, 0);
29
+ const current = processStartToken(owner.pid);
30
+ // Wall-clock ps lstart output can drift even for a live process. Only
31
+ // boot-scoped procfs ticks establish PID reuse; other probes need ESRCH.
32
+ const bootScoped = /^l[0-9a-f]{8}\.\d+$/;
33
+ dead =
34
+ typeof owner.process_start === "string" &&
35
+ bootScoped.test(owner.process_start) &&
36
+ typeof current === "string" &&
37
+ bootScoped.test(current) &&
38
+ current !== owner.process_start;
39
+ }
40
+ catch (error) {
41
+ dead = error.code === "ESRCH";
42
+ }
43
+ if (!dead)
44
+ return false;
45
+ // Only the contender that removes this unique owner's file may remove the
46
+ // directory. A losing reaper must not unlink a replacement owner's lock.
47
+ unlinkSync(ownerPath);
48
+ rmdirSync(lock);
49
+ return true;
50
+ }
51
+ catch {
52
+ return false;
53
+ }
54
+ }
55
+ export function withArtifactLock(repoRoot, action) {
56
+ // Probe before mkdir so a slow platform process probe cannot hold an
57
+ // ownerless lock open while other writers are trying to acquire it.
58
+ const owner = {
59
+ version: 1,
60
+ host: hostname(),
61
+ pid: process.pid,
62
+ process_start: processStartToken(process.pid),
63
+ acquired_at: new Date().toISOString(),
64
+ };
65
+ const lock = join(resolve(repoRoot), ".harnery/artifacts-mutation.lock");
66
+ mkdirSync(dirname(lock), { recursive: true });
67
+ try {
68
+ mkdirSync(lock);
69
+ }
70
+ catch {
71
+ if (!reclaimDeadOwner(lock)) {
72
+ throw new Error("artifact store mutation lock unavailable; a live or unverifiable owner holds it. " +
73
+ "Dead local owners recover automatically; an empty or invalid lock requires explicit inspection.");
74
+ }
75
+ // Another caller may win after recovery. Never retry by removing its lock.
76
+ mkdirSync(lock);
77
+ }
78
+ const ownerPath = join(lock, `owner-${randomUUID()}.json`);
79
+ try {
80
+ writeFileSync(ownerPath, JSON.stringify(owner), { flag: "wx" });
81
+ return action();
82
+ }
83
+ finally {
84
+ // A crash before publication or between these two calls leaves an empty
85
+ // directory. It cannot be distinguished safely from an initializing owner.
86
+ // Publication may have failed before creating a file. Remove only our
87
+ // unique file, then the empty directory; never recursively delete contents.
88
+ rmSync(ownerPath, { force: true });
89
+ rmdirSync(lock);
90
+ }
91
+ }
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Freshness helpers shared by `harn backup snapshot` and the session-start
3
+ * scheduler. The host cache is a local, best-effort record of this machine's
4
+ * newest known snapshot: the scheduler reads it to skip a remote restic round
5
+ * trip when the host is plainly current, and restic stays the authority
6
+ * whenever the cache is missing, stale, or written by another host.
7
+ */
8
+ export declare function parseBackupDuration(value: string): number | null;
9
+ /** Newest snapshot time (epoch ms) in `restic snapshots --json` output, or null. */
10
+ export declare function newestSnapshotTime(output: string): number | null;
11
+ export interface HostSnapshotCache {
12
+ host: string;
13
+ snapshotAt: number;
14
+ }
15
+ export declare function hostSnapshotCachePath(harneryDir: string): string;
16
+ export declare function readHostSnapshotCache(harneryDir: string): HostSnapshotCache | null;
17
+ export declare function recordHostSnapshot(harneryDir: string, snapshotAt: number): void;
18
+ //# sourceMappingURL=host-snapshot.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"host-snapshot.d.ts","sourceRoot":"","sources":["../../../src/core/backup/host-snapshot.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAMH,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAShE;AAED,oFAAoF;AACpF,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAWhE;AAED,MAAM,WAAW,iBAAiB;IAChC,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,wBAAgB,qBAAqB,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,CAEhE;AAED,wBAAgB,qBAAqB,CAAC,UAAU,EAAE,MAAM,GAAG,iBAAiB,GAAG,IAAI,CAalF;AAED,wBAAgB,kBAAkB,CAAC,UAAU,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,IAAI,CAc/E"}