harnery 0.45.1 → 0.46.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 (332) hide show
  1. package/dist/commander.d.ts +13 -0
  2. package/dist/commander.d.ts.map +1 -1
  3. package/dist/commands/admission.d.ts.map +1 -1
  4. package/dist/commands/admission.js +214 -27
  5. package/dist/commands/agents.d.ts +4 -0
  6. package/dist/commands/agents.d.ts.map +1 -1
  7. package/dist/commands/agents.js +102 -25
  8. package/dist/commands/browse.d.ts +3 -0
  9. package/dist/commands/browse.d.ts.map +1 -1
  10. package/dist/commands/browse.js +172 -8
  11. package/dist/commands/fetch.js +1 -0
  12. package/dist/commands/ledger-v3.d.ts.map +1 -1
  13. package/dist/commands/ledger-v3.js +8 -2
  14. package/dist/commands/presence.d.ts.map +1 -1
  15. package/dist/commands/presence.js +40 -0
  16. package/dist/commands/qa-run.d.ts.map +1 -1
  17. package/dist/commands/qa-run.js +4 -3
  18. package/dist/commands/review-pack.d.ts.map +1 -1
  19. package/dist/commands/review-pack.js +33 -6
  20. package/dist/commands/workflow.d.ts.map +1 -1
  21. package/dist/commands/workflow.js +6 -45
  22. package/dist/core/agents/coord-client.d.ts.map +1 -1
  23. package/dist/core/agents/coord-client.js +0 -18
  24. package/dist/core/agents/index.d.ts +1 -0
  25. package/dist/core/agents/index.d.ts.map +1 -1
  26. package/dist/core/agents/index.js +1 -0
  27. package/dist/core/agents/live-authority-v3.d.ts.map +1 -1
  28. package/dist/core/agents/live-authority-v3.js +11 -4
  29. package/dist/core/agents/render/session-context.d.ts +0 -6
  30. package/dist/core/agents/render/session-context.d.ts.map +1 -1
  31. package/dist/core/agents/render/session-context.js +1 -6
  32. package/dist/core/agents/session-events.d.ts.map +1 -1
  33. package/dist/core/agents/session-events.js +12 -4
  34. package/dist/core/agents/state/heartbeat-reader.d.ts +3 -0
  35. package/dist/core/agents/state/heartbeat-reader.d.ts.map +1 -1
  36. package/dist/core/agents/state/live-coordination-view.d.ts +3 -1
  37. package/dist/core/agents/state/live-coordination-view.d.ts.map +1 -1
  38. package/dist/core/agents/state/live-coordination-view.js +6 -2
  39. package/dist/core/agents/state/parent-reference-contract.d.ts +16 -0
  40. package/dist/core/agents/state/parent-reference-contract.d.ts.map +1 -0
  41. package/dist/core/agents/state/parent-reference-contract.js +1 -0
  42. package/dist/core/agents/state/parent-reference.d.ts +18 -0
  43. package/dist/core/agents/state/parent-reference.d.ts.map +1 -0
  44. package/dist/core/agents/state/parent-reference.js +119 -0
  45. package/dist/core/agents/state/pidmap.js +2 -2
  46. package/dist/core/agents/state/proc-start.d.ts +20 -31
  47. package/dist/core/agents/state/proc-start.d.ts.map +1 -1
  48. package/dist/core/agents/state/proc-start.js +37 -70
  49. package/dist/core/agents/state/service-process.d.ts +61 -0
  50. package/dist/core/agents/state/service-process.d.ts.map +1 -0
  51. package/dist/core/agents/state/service-process.js +143 -0
  52. package/dist/core/config.d.ts +39 -0
  53. package/dist/core/config.d.ts.map +1 -1
  54. package/dist/core/config.js +63 -0
  55. package/dist/core/context/index.d.ts +6 -4
  56. package/dist/core/context/index.d.ts.map +1 -1
  57. package/dist/core/context/index.js +194 -256
  58. package/dist/core/context/observation.d.ts +9 -9
  59. package/dist/core/context/observation.d.ts.map +1 -1
  60. package/dist/core/context/observation.js +37 -6
  61. package/dist/core/diagnostics/path-identity.d.ts +1 -1
  62. package/dist/core/diagnostics/path-identity.d.ts.map +1 -1
  63. package/dist/core/diagnostics/path-identity.js +51 -6
  64. package/dist/core/events/v3/canonical.js +43 -14
  65. package/dist/core/events/v3/index.d.ts +1 -1
  66. package/dist/core/events/v3/index.d.ts.map +1 -1
  67. package/dist/core/events/v3/index.js +1 -1
  68. package/dist/core/events/v3/producers/recorder.d.ts +34 -0
  69. package/dist/core/events/v3/producers/recorder.d.ts.map +1 -1
  70. package/dist/core/events/v3/producers/recorder.js +117 -13
  71. package/dist/core/events/v3/reader.d.ts.map +1 -1
  72. package/dist/core/events/v3/reader.js +53 -13
  73. package/dist/core/events/v3/runtime-build.d.ts +14 -0
  74. package/dist/core/events/v3/runtime-build.d.ts.map +1 -1
  75. package/dist/core/events/v3/runtime-build.js +44 -0
  76. package/dist/core/events/v3/validate.d.ts.map +1 -1
  77. package/dist/core/events/v3/validate.js +36 -1
  78. package/dist/core/governor/index.d.ts +2 -1
  79. package/dist/core/governor/index.d.ts.map +1 -1
  80. package/dist/core/governor/index.js +2 -1
  81. package/dist/core/governor/service-read.d.ts +3 -10
  82. package/dist/core/governor/service-read.d.ts.map +1 -1
  83. package/dist/core/governor/service-read.js +10 -94
  84. package/dist/core/governor/service.d.ts +3 -7
  85. package/dist/core/governor/service.d.ts.map +1 -1
  86. package/dist/core/governor/service.js +17 -182
  87. package/dist/core/hooks/adapter/native-parent.d.ts +23 -0
  88. package/dist/core/hooks/adapter/native-parent.d.ts.map +1 -0
  89. package/dist/core/hooks/adapter/native-parent.js +91 -0
  90. package/dist/core/hooks/cli.js +79 -4
  91. package/dist/core/hooks/resolve/owner.d.ts +0 -11
  92. package/dist/core/hooks/resolve/owner.d.ts.map +1 -1
  93. package/dist/core/hooks/resolve/owner.js +2 -25
  94. package/dist/core/hooks/shell-admission.d.ts +84 -0
  95. package/dist/core/hooks/shell-admission.d.ts.map +1 -0
  96. package/dist/core/hooks/shell-admission.js +622 -0
  97. package/dist/core/hooks/shell-commit-tree.d.ts +2 -0
  98. package/dist/core/hooks/shell-commit-tree.d.ts.map +1 -0
  99. package/dist/core/hooks/shell-commit-tree.js +104 -0
  100. package/dist/core/inbox/contract.d.ts +12 -0
  101. package/dist/core/inbox/contract.d.ts.map +1 -1
  102. package/dist/core/inbox/contract.js +16 -0
  103. package/dist/core/inbox/peer-messages.d.ts +41 -16
  104. package/dist/core/inbox/peer-messages.d.ts.map +1 -1
  105. package/dist/core/inbox/peer-messages.js +372 -15
  106. package/dist/core/inbox/service.d.ts +8 -1
  107. package/dist/core/inbox/service.d.ts.map +1 -1
  108. package/dist/core/inbox/service.js +83 -3
  109. package/dist/core/inbox/targets.d.ts +17 -3
  110. package/dist/core/inbox/targets.d.ts.map +1 -1
  111. package/dist/core/inbox/targets.js +77 -2
  112. package/dist/core/presence/git.d.ts +18 -12
  113. package/dist/core/presence/git.d.ts.map +1 -1
  114. package/dist/core/presence/git.js +32 -38
  115. package/dist/core/presence/index.d.ts +2 -2
  116. package/dist/core/presence/index.d.ts.map +1 -1
  117. package/dist/core/presence/index.js +8 -6
  118. package/dist/core/presence/mail.d.ts +133 -0
  119. package/dist/core/presence/mail.d.ts.map +1 -0
  120. package/dist/core/presence/mail.js +464 -0
  121. package/dist/core/resources/sampler.js +2 -2
  122. package/dist/core/resources/service-status.d.ts +2 -14
  123. package/dist/core/resources/service-status.d.ts.map +1 -1
  124. package/dist/core/resources/service-status.js +3 -23
  125. package/dist/core/resources/service.d.ts +3 -7
  126. package/dist/core/resources/service.d.ts.map +1 -1
  127. package/dist/core/resources/service.js +12 -101
  128. package/dist/core/resources/status.d.ts.map +1 -1
  129. package/dist/core/resources/status.js +3 -2
  130. package/dist/core/semantic/contract.d.ts +5 -5
  131. package/dist/core/semantic/service.d.ts.map +1 -1
  132. package/dist/core/semantic/service.js +9 -2
  133. package/dist/core/servers/index.d.ts +0 -2
  134. package/dist/core/servers/index.d.ts.map +1 -1
  135. package/dist/core/servers/index.js +3 -18
  136. package/dist/core/storage/modes.d.ts +4 -0
  137. package/dist/core/storage/modes.d.ts.map +1 -1
  138. package/dist/core/storage/modes.js +4 -0
  139. package/dist/core/storage/segments.d.ts.map +1 -1
  140. package/dist/core/storage/segments.js +5 -4
  141. package/dist/core/supervisor/service.d.ts +4 -7
  142. package/dist/core/supervisor/service.d.ts.map +1 -1
  143. package/dist/core/supervisor/service.js +23 -122
  144. package/dist/core/supervisor/services.js +2 -2
  145. package/dist/core/supervisor/status.d.ts +3 -17
  146. package/dist/core/supervisor/status.d.ts.map +1 -1
  147. package/dist/core/supervisor/status.js +7 -70
  148. package/dist/core/workflow/engine.js +2 -1
  149. package/dist/core/workflow/index.d.ts +3 -4
  150. package/dist/core/workflow/index.d.ts.map +1 -1
  151. package/dist/core/workflow/index.js +2 -2
  152. package/dist/core/workflow/proof.d.ts.map +1 -1
  153. package/dist/core/workflow/proof.js +30 -74
  154. package/dist/core/workflow/resume-lock.d.ts +17 -0
  155. package/dist/core/workflow/resume-lock.d.ts.map +1 -0
  156. package/dist/core/workflow/resume-lock.js +158 -0
  157. package/dist/core/workflow/run-state.d.ts +0 -21
  158. package/dist/core/workflow/run-state.d.ts.map +1 -1
  159. package/dist/core/workflow/run-state.js +3 -174
  160. package/dist/core/workflow/workspaces/leases.d.ts +4 -0
  161. package/dist/core/workflow/workspaces/leases.d.ts.map +1 -1
  162. package/dist/core/workflow/workspaces/leases.js +1 -1
  163. package/dist/core/workflow/workspaces/local-git.js +91 -210
  164. package/dist/core/workflow/workspaces/ownership.d.ts +0 -7
  165. package/dist/core/workflow/workspaces/ownership.d.ts.map +1 -1
  166. package/dist/core/workflow/workspaces/ownership.js +3 -130
  167. package/dist/core/workflow/workspaces/reclaim.d.ts +4 -7
  168. package/dist/core/workflow/workspaces/reclaim.d.ts.map +1 -1
  169. package/dist/core/workflow/workspaces/reclaim.js +0 -4
  170. package/dist/core/workflow/workspaces/recovery.d.ts +1 -1
  171. package/dist/core/workflow/workspaces/recovery.d.ts.map +1 -1
  172. package/dist/core/workflow/workspaces/recovery.js +1 -1
  173. package/dist/core/workflow/workspaces/state.d.ts.map +1 -1
  174. package/dist/core/workflow/workspaces/state.js +1 -4
  175. package/dist/lib/admission-history.d.ts +96 -0
  176. package/dist/lib/admission-history.d.ts.map +1 -0
  177. package/dist/lib/admission-history.js +196 -0
  178. package/dist/lib/admission.d.ts +56 -7
  179. package/dist/lib/admission.d.ts.map +1 -1
  180. package/dist/lib/admission.js +161 -19
  181. package/dist/lib/browser/client.d.ts +18 -0
  182. package/dist/lib/browser/client.d.ts.map +1 -1
  183. package/dist/lib/browser/client.js +100 -12
  184. package/dist/lib/browser/content-checks.d.ts +3 -1
  185. package/dist/lib/browser/content-checks.d.ts.map +1 -1
  186. package/dist/lib/browser/content-checks.js +10 -2
  187. package/dist/lib/browser/critique.d.ts +32 -0
  188. package/dist/lib/browser/critique.d.ts.map +1 -1
  189. package/dist/lib/browser/critique.js +72 -3
  190. package/dist/lib/browser/geometry.d.ts +2 -0
  191. package/dist/lib/browser/geometry.d.ts.map +1 -1
  192. package/dist/lib/browser/geometry.js +61 -15
  193. package/dist/lib/browser/index.d.ts +1 -1
  194. package/dist/lib/browser/index.d.ts.map +1 -1
  195. package/dist/lib/browser/index.js +1 -1
  196. package/dist/lib/browser/page-review-contracts.d.ts +6 -1
  197. package/dist/lib/browser/page-review-contracts.d.ts.map +1 -1
  198. package/dist/lib/browser/page-review-contracts.js +6 -1
  199. package/dist/lib/browser/page-review-judge.d.ts +3 -1
  200. package/dist/lib/browser/page-review-judge.d.ts.map +1 -1
  201. package/dist/lib/browser/page-review-judge.js +41 -11
  202. package/dist/lib/browser/page-review-pack.d.ts +3 -0
  203. package/dist/lib/browser/page-review-pack.d.ts.map +1 -1
  204. package/dist/lib/browser/page-review-pack.js +5 -2
  205. package/dist/lib/browser/qa-run-contracts.d.ts +11 -0
  206. package/dist/lib/browser/qa-run-contracts.d.ts.map +1 -1
  207. package/dist/lib/browser/qa-run-contracts.js +11 -4
  208. package/dist/lib/browser/qa-run.d.ts.map +1 -1
  209. package/dist/lib/browser/qa-run.js +73 -16
  210. package/dist/lib/browser/runts.d.ts +4 -2
  211. package/dist/lib/browser/runts.d.ts.map +1 -1
  212. package/dist/lib/browser/runts.js +84 -33
  213. package/dist/lib/browser/windows-chrome.d.ts +14 -0
  214. package/dist/lib/browser/windows-chrome.d.ts.map +1 -1
  215. package/dist/lib/browser/windows-chrome.js +42 -0
  216. package/dist/lib/browser/windows-login-sync.d.ts +110 -0
  217. package/dist/lib/browser/windows-login-sync.d.ts.map +1 -0
  218. package/dist/lib/browser/windows-login-sync.js +171 -0
  219. package/dist/lib/cookies/client.d.ts +30 -5
  220. package/dist/lib/cookies/client.d.ts.map +1 -1
  221. package/dist/lib/cookies/client.js +55 -10
  222. package/dist/lib/cookies/index.d.ts +1 -1
  223. package/dist/lib/cookies/index.d.ts.map +1 -1
  224. package/dist/lib/cookies/index.js +1 -1
  225. package/dist/lib/docs-metadata-sync.d.ts.map +1 -1
  226. package/dist/lib/docs-metadata-sync.js +41 -2
  227. package/dist/lib/durable-job.d.ts +8 -0
  228. package/dist/lib/durable-job.d.ts.map +1 -1
  229. package/dist/lib/durable-job.js +10 -0
  230. package/dist/lib/http/client.d.ts +9 -8
  231. package/dist/lib/http/client.d.ts.map +1 -1
  232. package/dist/lib/http/client.js +44 -2
  233. package/dist/lib/instructions/templates.js +4 -4
  234. package/package.json +2 -2
  235. package/src/commander.ts +13 -0
  236. package/src/commands/admission.ts +272 -29
  237. package/src/commands/agents.ts +132 -27
  238. package/src/commands/browse.ts +208 -6
  239. package/src/commands/fetch.ts +1 -0
  240. package/src/commands/ledger-v3.ts +8 -2
  241. package/src/commands/presence.ts +49 -0
  242. package/src/commands/qa-run.ts +4 -3
  243. package/src/commands/review-pack.ts +41 -5
  244. package/src/commands/workflow.ts +8 -58
  245. package/src/core/agents/coord-client.ts +0 -20
  246. package/src/core/agents/index.ts +6 -0
  247. package/src/core/agents/live-authority-v3.ts +11 -4
  248. package/src/core/agents/render/session-context.ts +1 -7
  249. package/src/core/agents/session-events.ts +18 -3
  250. package/src/core/agents/state/heartbeat-reader.ts +3 -0
  251. package/src/core/agents/state/live-coordination-view.ts +13 -6
  252. package/src/core/agents/state/parent-reference-contract.ts +14 -0
  253. package/src/core/agents/state/parent-reference.ts +179 -0
  254. package/src/core/agents/state/pidmap.ts +2 -2
  255. package/src/core/agents/state/proc-start.ts +41 -69
  256. package/src/core/agents/state/service-process.ts +217 -0
  257. package/src/core/config.ts +85 -0
  258. package/src/core/context/index.ts +207 -272
  259. package/src/core/context/observation.ts +40 -9
  260. package/src/core/diagnostics/path-identity.ts +52 -6
  261. package/src/core/events/v3/canonical.ts +46 -13
  262. package/src/core/events/v3/index.ts +3 -0
  263. package/src/core/events/v3/producers/recorder.ts +151 -13
  264. package/src/core/events/v3/reader.ts +55 -11
  265. package/src/core/events/v3/runtime-build.ts +62 -0
  266. package/src/core/events/v3/validate.ts +42 -1
  267. package/src/core/governor/index.ts +1 -2
  268. package/src/core/governor/service-read.ts +15 -114
  269. package/src/core/governor/service.ts +36 -220
  270. package/src/core/hooks/adapter/native-parent.ts +109 -0
  271. package/src/core/hooks/cli.ts +83 -4
  272. package/src/core/hooks/resolve/owner.ts +2 -25
  273. package/src/core/hooks/shell-admission.ts +634 -0
  274. package/src/core/hooks/shell-commit-tree.ts +102 -0
  275. package/src/core/inbox/contract.ts +18 -0
  276. package/src/core/inbox/peer-messages.ts +501 -15
  277. package/src/core/inbox/service.ts +108 -3
  278. package/src/core/inbox/targets.ts +94 -3
  279. package/src/core/presence/git.ts +45 -37
  280. package/src/core/presence/index.ts +16 -8
  281. package/src/core/presence/mail.ts +620 -0
  282. package/src/core/resources/sampler.ts +3 -2
  283. package/src/core/resources/service-status.ts +6 -35
  284. package/src/core/resources/service.ts +23 -133
  285. package/src/core/resources/status.ts +3 -2
  286. package/src/core/semantic/service.ts +8 -2
  287. package/src/core/servers/index.ts +3 -23
  288. package/src/core/storage/modes.ts +6 -0
  289. package/src/core/storage/segments.ts +5 -4
  290. package/src/core/supervisor/service.ts +33 -155
  291. package/src/core/supervisor/services.ts +2 -2
  292. package/src/core/supervisor/status.ts +10 -85
  293. package/src/core/workflow/engine.ts +1 -1
  294. package/src/core/workflow/index.ts +2 -13
  295. package/src/core/workflow/proof.ts +35 -83
  296. package/src/core/workflow/resume-lock.ts +179 -0
  297. package/src/core/workflow/run-state.ts +3 -207
  298. package/src/core/workflow/workspaces/leases.ts +1 -1
  299. package/src/core/workflow/workspaces/local-git.ts +120 -239
  300. package/src/core/workflow/workspaces/ownership.ts +3 -158
  301. package/src/core/workflow/workspaces/reclaim.ts +3 -9
  302. package/src/core/workflow/workspaces/recovery.ts +1 -1
  303. package/src/core/workflow/workspaces/state.ts +1 -5
  304. package/src/lib/admission-history.ts +267 -0
  305. package/src/lib/admission.ts +217 -19
  306. package/src/lib/browser/client.ts +147 -35
  307. package/src/lib/browser/content-checks.ts +14 -3
  308. package/src/lib/browser/critique.ts +113 -3
  309. package/src/lib/browser/geometry.ts +63 -14
  310. package/src/lib/browser/index.ts +5 -0
  311. package/src/lib/browser/page-review-contracts.ts +6 -1
  312. package/src/lib/browser/page-review-judge.ts +58 -13
  313. package/src/lib/browser/page-review-pack.ts +13 -2
  314. package/src/lib/browser/qa-run-contracts.ts +30 -4
  315. package/src/lib/browser/qa-run.ts +78 -17
  316. package/src/lib/browser/runts.ts +85 -37
  317. package/src/lib/browser/windows-chrome.ts +50 -0
  318. package/src/lib/browser/windows-login-sync.ts +275 -0
  319. package/src/lib/cookies/client.ts +62 -10
  320. package/src/lib/cookies/index.ts +3 -0
  321. package/src/lib/docs-metadata-sync.ts +44 -2
  322. package/src/lib/durable-job.ts +15 -0
  323. package/src/lib/http/client.ts +63 -1
  324. package/src/lib/instructions/templates.ts +4 -4
  325. package/dist/core/agents/state/process-handle.d.ts +0 -26
  326. package/dist/core/agents/state/process-handle.d.ts.map +0 -1
  327. package/dist/core/agents/state/process-handle.js +0 -121
  328. package/dist/core/workflow/recovery-audit.d.ts +0 -30
  329. package/dist/core/workflow/recovery-audit.d.ts.map +0 -1
  330. package/dist/core/workflow/recovery-audit.js +0 -116
  331. package/src/core/agents/state/process-handle.ts +0 -165
  332. package/src/core/workflow/recovery-audit.ts +0 -181
@@ -0,0 +1,275 @@
1
+ import { createHash } from "node:crypto";
2
+ import {
3
+ closeSync,
4
+ existsSync,
5
+ mkdirSync,
6
+ openSync,
7
+ readFileSync,
8
+ renameSync,
9
+ rmSync,
10
+ statSync,
11
+ writeFileSync,
12
+ } from "node:fs";
13
+ import { basename, dirname, join } from "node:path";
14
+ import type { Cookie } from "../cookies/client.ts";
15
+ import { CookieJar, mergeCookies } from "../cookies/index.ts";
16
+ import type { WindowsChromeHarvest } from "./windows-chrome.ts";
17
+
18
+ /**
19
+ * Keep the shared cookie store current with the WSL sign-in profile.
20
+ *
21
+ * Under WSL, `browse --login` signs in through the Windows host's Chrome on a
22
+ * dedicated profile and copies that site's cookies into the cookie store when
23
+ * the window closes. People also keep that profile open as an ordinary
24
+ * browser and sign in to other sites there later. Nothing copies those
25
+ * sign-ins when the window is closed outside a waiting `--login` run, so a
26
+ * later headless run would show the site signed out.
27
+ *
28
+ * Before a headless run, this module compares the profile's cookie database
29
+ * modification time with the time recorded at the last copy for the run's
30
+ * domain. When the profile changed and no Chrome is running on it, the
31
+ * domain's cookies are read (the same headless read `--login` uses) and
32
+ * merged into the store. A profile that is open is never touched. A small
33
+ * owner-only state file next to the store records, per domain, the database
34
+ * time and a fingerprint of the cookies last read. When a read returns the
35
+ * same cookies as last time, nothing is merged, so cookies a headless run
36
+ * refreshed in the store are not replaced with the profile's older copies.
37
+ */
38
+
39
+ export interface DomainSyncRecord {
40
+ /** When the profile's cookies for the domain were last read. */
41
+ syncedAt: string;
42
+ /** Cookie database modification time right after that read. */
43
+ cookieDbMtimeMs: number;
44
+ /** SHA-256 over the cookies read, so an unchanged read merges nothing. */
45
+ fingerprint: string;
46
+ /** Number of cookies read. */
47
+ cookies: number;
48
+ }
49
+
50
+ export interface LoginSyncState {
51
+ version: 1;
52
+ domains: Record<string, DomainSyncRecord>;
53
+ }
54
+
55
+ /** The sync-state file for a cookie store: `<store>.login-sync.json`, beside it. */
56
+ export function loginSyncStatePath(storePath: string): string {
57
+ const name = basename(storePath).replace(/\.json$/i, "");
58
+ return join(dirname(storePath), `${name}.login-sync.json`);
59
+ }
60
+
61
+ export function readLoginSyncState(statePath: string): LoginSyncState {
62
+ try {
63
+ const parsed = JSON.parse(readFileSync(statePath, "utf8")) as Partial<LoginSyncState>;
64
+ if (parsed && parsed.version === 1 && parsed.domains && typeof parsed.domains === "object") {
65
+ return { version: 1, domains: parsed.domains };
66
+ }
67
+ } catch {
68
+ // Missing or unreadable state means nothing has been copied yet.
69
+ }
70
+ return { version: 1, domains: {} };
71
+ }
72
+
73
+ /** Write the state file owner-only, replacing it atomically. */
74
+ export function writeLoginSyncState(statePath: string, state: LoginSyncState): void {
75
+ mkdirSync(dirname(statePath), { recursive: true, mode: 0o700 });
76
+ const tmp = `${statePath}.${process.pid}.tmp`;
77
+ writeFileSync(tmp, `${JSON.stringify(state, null, 2)}\n`, { mode: 0o600 });
78
+ renameSync(tmp, statePath);
79
+ }
80
+
81
+ /** Order-independent fingerprint of a cookie set. Values are hashed, never stored. */
82
+ export function cookieFingerprint(cookies: Cookie[]): string {
83
+ const rows = cookies
84
+ .map((c) => JSON.stringify([c.name, c.domain, c.path, c.value, c.expires]))
85
+ .sort();
86
+ return createHash("sha256").update(rows.join("\n")).digest("hex");
87
+ }
88
+
89
+ export type LoginSyncDecision =
90
+ /** No cookie database: the profile has never been used. */
91
+ | "no-profile"
92
+ /** The database has not changed since the last read for this domain. */
93
+ | "current"
94
+ /** The database is newer, but Chrome is open on the profile. */
95
+ | "open"
96
+ /** The database is newer (or the read is forced) and the profile is closed. */
97
+ | "sync";
98
+
99
+ /**
100
+ * Decide what to do before reading. `running` is consulted only when the
101
+ * database is newer, since checking for Chrome costs a PowerShell call.
102
+ */
103
+ export function decideLoginSync(input: {
104
+ cookieDbMtimeMs: number | undefined;
105
+ record: DomainSyncRecord | undefined;
106
+ running: () => boolean;
107
+ force?: boolean;
108
+ }): LoginSyncDecision {
109
+ if (input.cookieDbMtimeMs === undefined) return "no-profile";
110
+ if (!input.force && input.record && input.cookieDbMtimeMs <= input.record.cookieDbMtimeMs)
111
+ return "current";
112
+ return input.running() ? "open" : "sync";
113
+ }
114
+
115
+ export interface LoginSyncDeps {
116
+ /** Newest modification time of the profile's cookie database, or `undefined`. */
117
+ cookieDbMtimeMs: () => number | undefined;
118
+ /** Ids of Chrome processes running on the profile. */
119
+ profileProcesses: () => number[];
120
+ /** Read the profile's cookies for `domain`; the profile must be closed. */
121
+ harvest: (domain: string) => WindowsChromeHarvest;
122
+ now?: () => Date;
123
+ }
124
+
125
+ export interface LoginSyncResult {
126
+ status: "no-profile" | "current" | "open" | "busy" | "synced" | "unchanged";
127
+ domain: string;
128
+ /** Cookies read from the profile (0 when nothing was read). */
129
+ cookies: number;
130
+ /** Cookies merged into the store (0 unless `synced`). */
131
+ merged: number;
132
+ sessionCookies: number;
133
+ /** Cookie names merged, sorted. Never values. */
134
+ names: string[];
135
+ /** Chrome process ids on the profile when `open`. */
136
+ processes: number[];
137
+ }
138
+
139
+ /**
140
+ * Copy the profile's cookies for `domain` into the store when the profile
141
+ * changed since the last copy and is closed. `force` skips the time check
142
+ * and merges whatever is read. Throws only on read failures; the caller
143
+ * decides whether that is a warning.
144
+ */
145
+ export function syncLoginCookies(input: {
146
+ domain: string;
147
+ storePath: string;
148
+ deps: LoginSyncDeps;
149
+ force?: boolean;
150
+ }): LoginSyncResult {
151
+ const { domain, storePath, deps } = input;
152
+ const base: LoginSyncResult = {
153
+ status: "current",
154
+ domain,
155
+ cookies: 0,
156
+ merged: 0,
157
+ sessionCookies: 0,
158
+ names: [],
159
+ processes: [],
160
+ };
161
+ const statePath = loginSyncStatePath(storePath);
162
+ const state = readLoginSyncState(statePath);
163
+ const record = state.domains[domain];
164
+ let processes: number[] = [];
165
+ const decision = decideLoginSync({
166
+ cookieDbMtimeMs: deps.cookieDbMtimeMs(),
167
+ record,
168
+ force: input.force,
169
+ running: () => {
170
+ processes = deps.profileProcesses();
171
+ return processes.length > 0;
172
+ },
173
+ });
174
+ if (decision === "no-profile") return { ...base, status: "no-profile" };
175
+ if (decision === "current") return base;
176
+ if (decision === "open") return { ...base, status: "open", processes };
177
+
178
+ const release = acquireSyncLock(statePath);
179
+ if (!release) return { ...base, status: "busy" };
180
+ try {
181
+ const { cookies } = deps.harvest(domain);
182
+ const fingerprint = cookieFingerprint(cookies);
183
+ const changed = input.force || !record || record.fingerprint !== fingerprint;
184
+ let merged: Cookie[] = [];
185
+ if (changed && cookies.length > 0) {
186
+ const jar = new CookieJar({ path: storePath, source: "harn-browse" });
187
+ jar.save(mergeCookies(jar.load(), cookies));
188
+ merged = cookies;
189
+ }
190
+ const after = readLoginSyncState(statePath);
191
+ after.domains[domain] = {
192
+ syncedAt: (deps.now?.() ?? new Date()).toISOString(),
193
+ cookieDbMtimeMs: deps.cookieDbMtimeMs() ?? 0,
194
+ fingerprint,
195
+ cookies: cookies.length,
196
+ };
197
+ writeLoginSyncState(statePath, after);
198
+ return {
199
+ ...base,
200
+ status: merged.length > 0 ? "synced" : "unchanged",
201
+ cookies: cookies.length,
202
+ merged: merged.length,
203
+ sessionCookies: merged.filter((c) => c.expires <= 0).length,
204
+ names: [...new Set(merged.map((c) => c.name))].sort(),
205
+ };
206
+ } finally {
207
+ release();
208
+ }
209
+ }
210
+
211
+ /**
212
+ * Record a read that happened elsewhere (the `--login` flow), so the next
213
+ * headless run does not read the same cookies again.
214
+ */
215
+ export function recordLoginSync(input: {
216
+ domain: string;
217
+ storePath: string;
218
+ cookies: Cookie[];
219
+ cookieDbMtimeMs: number | undefined;
220
+ now?: Date;
221
+ }): void {
222
+ const statePath = loginSyncStatePath(input.storePath);
223
+ const state = readLoginSyncState(statePath);
224
+ state.domains[input.domain] = {
225
+ syncedAt: (input.now ?? new Date()).toISOString(),
226
+ cookieDbMtimeMs: input.cookieDbMtimeMs ?? 0,
227
+ fingerprint: cookieFingerprint(input.cookies),
228
+ cookies: input.cookies.length,
229
+ };
230
+ writeLoginSyncState(statePath, state);
231
+ }
232
+
233
+ /** Newest mtime among a profile's cookie database locations, or `undefined`. */
234
+ export function profileCookieDbMtimeMs(
235
+ profileLinux: string,
236
+ stat: (path: string) => number | undefined = fileMtimeMs,
237
+ ): number | undefined {
238
+ const times = [
239
+ join(profileLinux, "Default", "Network", "Cookies"),
240
+ join(profileLinux, "Default", "Cookies"),
241
+ ]
242
+ .map((path) => stat(path))
243
+ .filter((t): t is number => t !== undefined);
244
+ return times.length > 0 ? Math.max(...times) : undefined;
245
+ }
246
+
247
+ function fileMtimeMs(path: string): number | undefined {
248
+ try {
249
+ return statSync(path).mtimeMs;
250
+ } catch {
251
+ return undefined;
252
+ }
253
+ }
254
+
255
+ const LOCK_STALE_MS = 3 * 60_000;
256
+
257
+ /**
258
+ * One read at a time: a second headless Chrome on the profile would refuse
259
+ * to start. A lock older than three minutes is treated as abandoned.
260
+ */
261
+ function acquireSyncLock(statePath: string): (() => void) | null {
262
+ const lockPath = `${statePath}.lock`;
263
+ mkdirSync(dirname(lockPath), { recursive: true, mode: 0o700 });
264
+ for (let attempt = 0; attempt < 2; attempt++) {
265
+ try {
266
+ closeSync(openSync(lockPath, "wx", 0o600));
267
+ return () => rmSync(lockPath, { force: true });
268
+ } catch {
269
+ const age = existsSync(lockPath) ? Date.now() - (fileMtimeMs(lockPath) ?? 0) : 0;
270
+ if (age < LOCK_STALE_MS) return null;
271
+ rmSync(lockPath, { force: true });
272
+ }
273
+ }
274
+ return null;
275
+ }
@@ -284,9 +284,9 @@ export class CookieJar {
284
284
  }
285
285
 
286
286
  /**
287
- * Read + filter helper. Drops expired cookies. Optional domain filter
288
- * uses the same `domainMatches` logic Playwright/CDP do (leading dot
289
- * means subdomain match).
287
+ * Read + filter helper. Drops expired cookies. The optional domain filter
288
+ * uses `domainMatches` (equal host or any subdomain); it is a query, not a
289
+ * delivery rule. `header(url)` uses the stricter `cookieSendsToHost`.
290
290
  */
291
291
  list(filter: { domain?: string } = {}): Cookie[] {
292
292
  const store = this.load();
@@ -451,14 +451,62 @@ export function mergeCookies(store: CookieStore, incoming: Cookie[]): CookieStor
451
451
  return { ...store, cookies: Array.from(map.values()) };
452
452
  }
453
453
 
454
- /** RFC 6265 domain match. Leading dot in cookie domain means subdomain match. */
454
+ /**
455
+ * Filter-style domain match used by `list({ domain })` and `clear({ domain })`:
456
+ * a cookie domain "matches" a hostname when they are equal or the hostname is
457
+ * a subdomain of it, whether or not the cookie is host-only. Never use this to
458
+ * decide whether a cookie may be SENT to a host; use `cookieSendsToHost`.
459
+ */
455
460
  export function domainMatches(cookieDomain: string, hostname: string): boolean {
456
- const cd = cookieDomain.startsWith(".") ? cookieDomain.slice(1) : cookieDomain;
457
- if (hostname === cd) return true;
458
- if (hostname.endsWith(`.${cd}`)) return true;
461
+ const cd = (cookieDomain.startsWith(".") ? cookieDomain.slice(1) : cookieDomain).toLowerCase();
462
+ const host = hostname.toLowerCase();
463
+ if (host === cd) return true;
464
+ if (host.endsWith(`.${cd}`)) return true;
459
465
  return false;
460
466
  }
461
467
 
468
+ /**
469
+ * RFC 6265 delivery rule. A cookie domain with a leading dot is a domain
470
+ * cookie and goes to the domain and every subdomain. A cookie domain without
471
+ * one is host-only (how CDP, Playwright and `Set-Cookie` without `Domain`
472
+ * record it) and goes to that exact host and nowhere else.
473
+ */
474
+ export function cookieSendsToHost(cookieDomain: string, hostname: string): boolean {
475
+ const host = hostname.toLowerCase();
476
+ if (cookieDomain.startsWith(".")) return domainMatches(cookieDomain, host);
477
+ return cookieDomain.toLowerCase() === host;
478
+ }
479
+
480
+ /**
481
+ * Cookie-prefix rules (RFC 6265bis). `__Secure-` needs Secure; `__Host-` also
482
+ * needs Path=/ and no Domain attribute, which a jar records as a host-only
483
+ * domain (no leading dot). Browsers refuse a violating cookie, so the jar
484
+ * never stores one and the browser injector never hands one to Chromium.
485
+ */
486
+ export function violatesCookiePrefix(
487
+ cookie: Pick<Cookie, "name" | "domain" | "path" | "secure">,
488
+ ): boolean {
489
+ if (cookie.name.startsWith("__Secure-") && !cookie.secure) return true;
490
+ if (
491
+ cookie.name.startsWith("__Host-") &&
492
+ (!cookie.secure || cookie.domain.startsWith(".") || cookie.path !== "/")
493
+ ) {
494
+ return true;
495
+ }
496
+ return false;
497
+ }
498
+
499
+ /** True for hosts a browser treats as a secure context without TLS. */
500
+ export function isLoopbackHost(hostname: string): boolean {
501
+ const host = hostname.toLowerCase();
502
+ return (
503
+ host === "localhost" ||
504
+ host.endsWith(".localhost") ||
505
+ host === "[::1]" ||
506
+ /^127(?:\.\d{1,3}){3}$/.test(host)
507
+ );
508
+ }
509
+
462
510
  /** RFC 6265 path match. */
463
511
  export function pathMatches(cookiePath: string, urlPath: string): boolean {
464
512
  if (urlPath === cookiePath) return true;
@@ -473,13 +521,17 @@ export function isExpired(cookie: Cookie): boolean {
473
521
  return cookie.expires < Date.now() / 1000;
474
522
  }
475
523
 
476
- /** Cookies that should be sent with a request to `url`. */
524
+ /**
525
+ * Cookies that should be sent with a request to `url`: exact-host delivery for
526
+ * host-only cookies, domain delivery for dotted ones, then path and Secure.
527
+ * Secure cookies go over HTTPS and to loopback hosts, as in browsers.
528
+ */
477
529
  export function matchingCookies(store: CookieStore, url: string): Cookie[] {
478
530
  const parsed = new URL(url);
479
- const isSecure = parsed.protocol === "https:";
531
+ const isSecure = parsed.protocol === "https:" || isLoopbackHost(parsed.hostname);
480
532
  return store.cookies.filter((c) => {
481
533
  if (isExpired(c)) return false;
482
- if (!domainMatches(c.domain, parsed.hostname)) return false;
534
+ if (!cookieSendsToHost(c.domain, parsed.hostname)) return false;
483
535
  if (!pathMatches(c.path, parsed.pathname)) return false;
484
536
  if (c.secure && !isSecure) return false;
485
537
  return true;
@@ -4,12 +4,15 @@ export {
4
4
  type CookieJarOptions,
5
5
  type CookieStore,
6
6
  CookieStoreParseError,
7
+ cookieSendsToHost,
7
8
  domainMatches,
8
9
  type InfoResult,
9
10
  isExpired,
11
+ isLoopbackHost,
10
12
  matchingCookies,
11
13
  mergeCookies,
12
14
  type OriginEntry,
13
15
  pathMatches,
16
+ violatesCookiePrefix,
14
17
  } from "./client.js";
15
18
  export { applyExtraCookies, type ExtraCookies } from "./extra.js";
@@ -118,6 +118,33 @@ function addDays(value: string, days: number): string {
118
118
  return date.toISOString().replace(".000Z", "Z");
119
119
  }
120
120
 
121
+ /** Allowance for clock skew between machines before a timestamp counts as future. */
122
+ const FUTURE_SKEW_MS = 10 * 60 * 1000;
123
+
124
+ /** Lifecycle timestamps record events that already happened, so none may be later than now. */
125
+ const PAST_TIMESTAMP_FIELDS = [
126
+ "created_at",
127
+ "updated_at",
128
+ "status_changed_at",
129
+ "resolved_at",
130
+ "reviewed_at",
131
+ ] as const;
132
+
133
+ function inFuture(value: unknown, now: string): boolean {
134
+ if (typeof value !== "string") return false;
135
+ const time = Date.parse(value);
136
+ return Number.isFinite(time) && time > Date.parse(now) + FUTURE_SKEW_MS;
137
+ }
138
+
139
+ /**
140
+ * While a status is unchanged its status_changed_at is pinned to the committed
141
+ * value. A committed value later than now cannot be true, so a correction to a
142
+ * time that has already passed is accepted instead of being reverted.
143
+ */
144
+ function correctsFutureStatusTime(committed: unknown, current: unknown, now: string): boolean {
145
+ return inFuture(committed, now) && typeof current === "string" && !inFuture(current, now);
146
+ }
147
+
121
148
  export async function runDocsMetadataSync(
122
149
  opts: DocsMetadataSyncOpts,
123
150
  ): Promise<DocsMetadataSyncRow[]> {
@@ -171,7 +198,8 @@ export async function runDocsMetadataSync(
171
198
  if (
172
199
  !statusChanged &&
173
200
  beforeV2 &&
174
- current.data.status_changed_at !== beforeV2.status_changed_at
201
+ current.data.status_changed_at !== beforeV2.status_changed_at &&
202
+ !correctsFutureStatusTime(beforeV2.status_changed_at, current.data.status_changed_at, now)
175
203
  ) {
176
204
  expected.status_changed_at = beforeV2.status_changed_at;
177
205
  fields.push("status_changed_at");
@@ -203,6 +231,19 @@ export async function runDocsMetadataSync(
203
231
  continue;
204
232
  }
205
233
  if (opts.check) {
234
+ const future = PAST_TIMESTAMP_FIELDS.filter((field) => inFuture(current.data[field], now));
235
+ if (future.length > 0) {
236
+ rows.push({
237
+ repo: target.name,
238
+ path,
239
+ status: "invalid",
240
+ fields: [...future],
241
+ message: future
242
+ .map((field) => `${field}: cannot be later than the current time`)
243
+ .join("; "),
244
+ });
245
+ continue;
246
+ }
206
247
  const validation = validateDocsMetadataV2(current.data);
207
248
  if (!validation.valid) {
208
249
  rows.push({
@@ -238,7 +279,8 @@ export async function runDocsMetadataSync(
238
279
  if (
239
280
  beforeV2 &&
240
281
  !statusChanged &&
241
- current.data.status_changed_at !== beforeV2.status_changed_at
282
+ current.data.status_changed_at !== beforeV2.status_changed_at &&
283
+ !correctsFutureStatusTime(beforeV2.status_changed_at, current.data.status_changed_at, now)
242
284
  ) {
243
285
  drift.push("status_changed_at");
244
286
  }
@@ -63,6 +63,11 @@ export interface DurableJobDocument {
63
63
  /** Directory the wrapped command runs in. */
64
64
  cwd: string;
65
65
  created_at: string;
66
+ /**
67
+ * Wait while Linux memory pressure is above this before queueing, as the
68
+ * foreground run does. The client resolves it from the resource's project config at launch.
69
+ */
70
+ defer_on_pressure?: { psi_full_avg10: number; max_wait_seconds: number };
66
71
  }
67
72
 
68
73
  export interface DurableJobStatus {
@@ -208,9 +213,19 @@ export function readJobDocument(jobDir: string): DurableJobDocument | null {
208
213
  : [],
209
214
  cwd: typeof parsed.cwd === "string" ? parsed.cwd : "",
210
215
  created_at: typeof parsed.created_at === "string" ? parsed.created_at : "",
216
+ ...readDeferOnPressure(parsed.defer_on_pressure),
211
217
  };
212
218
  }
213
219
 
220
+ function readDeferOnPressure(value: unknown): Pick<DurableJobDocument, "defer_on_pressure"> {
221
+ if (!isRecord(value)) return {};
222
+ const threshold = value.psi_full_avg10;
223
+ const maxWait = value.max_wait_seconds;
224
+ return typeof threshold === "number" && typeof maxWait === "number" && maxWait > 0
225
+ ? { defer_on_pressure: { psi_full_avg10: threshold, max_wait_seconds: maxWait } }
226
+ : {};
227
+ }
228
+
214
229
  /** Read the status document, or null when it is missing or unusable. */
215
230
  export function readJobStatus(jobDir: string): DurableJobStatus | null {
216
231
  const path = join(resolve(jobDir), DURABLE_JOB_STATUS_FILENAME);
@@ -2,7 +2,9 @@ import {
2
2
  applyExtraCookies,
3
3
  type Cookie,
4
4
  type CookieJar,
5
+ domainMatches,
5
6
  type ExtraCookies,
7
+ violatesCookiePrefix,
6
8
  } from "../cookies/index.ts";
7
9
 
8
10
  /**
@@ -52,6 +54,15 @@ export interface FetchOptions<TResponseType extends FetchResponseType = "text">
52
54
  extraCookies?: ExtraCookies;
53
55
  /** Response body representation. Default `text`; use `bytes` for lossless file output. */
54
56
  responseType?: TResponseType;
57
+ /**
58
+ * With `redirect: "follow"`, follow redirects in this client instead of the
59
+ * runtime's fetch. Every hop then gets its own cookie header, `extraHeaders`
60
+ * call, and Set-Cookie persistence, so a caller that inspects those can stop
61
+ * a chain between hops. Credential headers are dropped on a cross-origin hop.
62
+ * Default: on whenever a `jar` is passed (host-only cookies must follow the
63
+ * jar's exact-host rule on every hop), off without one.
64
+ */
65
+ clientRedirects?: boolean;
55
66
  }
56
67
 
57
68
  type FetchBody<TResponseType extends FetchResponseType> = TResponseType extends "bytes"
@@ -76,9 +87,55 @@ export interface FetchResult<TBody = string> {
76
87
  * Streams aren't supported; this is a CLI helper, not a streaming HTTP
77
88
  * client. Use Bun's `fetch` directly for streaming workloads.
78
89
  */
90
+ const REDIRECT_STATUSES = new Set([301, 302, 303, 307, 308]);
91
+ const MAX_CLIENT_REDIRECTS = 20;
92
+
79
93
  export async function fetchWithJar<TResponseType extends FetchResponseType = "text">(
80
94
  url: string,
81
95
  opts: FetchOptions<TResponseType> = {},
96
+ ): Promise<FetchResult<FetchBody<TResponseType>>> {
97
+ // With a jar, redirects are followed here so every hop gets its own Cookie
98
+ // header from the jar (exact-host for host-only cookies) and its own
99
+ // Set-Cookie persistence; the runtime would otherwise carry or drop the
100
+ // first hop's header on its own rules. Pass `clientRedirects: false` to opt out.
101
+ const clientRedirects = opts.clientRedirects ?? Boolean(opts.jar);
102
+ if (!clientRedirects || (opts.redirect ?? "follow") !== "follow") return fetchOnce(url, opts);
103
+
104
+ let current = url;
105
+ let method = opts.method ?? "GET";
106
+ let body = opts.body;
107
+ let headers = opts.headers;
108
+ let cookiesSaved = 0;
109
+ for (let hop = 0; ; hop++) {
110
+ const result = await fetchOnce(current, { ...opts, method, body, headers, redirect: "manual" });
111
+ cookiesSaved += result.cookiesSaved;
112
+ const location = result.headers.location;
113
+ if (!REDIRECT_STATUSES.has(result.status) || !location) return { ...result, cookiesSaved };
114
+ if (hop >= MAX_CLIENT_REDIRECTS) {
115
+ throw new Error(`Too many redirects (more than ${MAX_CLIENT_REDIRECTS}) starting at ${url}`);
116
+ }
117
+ const next = new URL(location, current);
118
+ if (
119
+ result.status === 303 ||
120
+ ((result.status === 301 || result.status === 302) && method !== "GET" && method !== "HEAD")
121
+ ) {
122
+ method = "GET";
123
+ body = undefined;
124
+ }
125
+ if (next.origin !== new URL(current).origin && headers) {
126
+ headers = Object.fromEntries(
127
+ Object.entries(headers).filter(
128
+ ([name]) => !["authorization", "cookie"].includes(name.toLowerCase()),
129
+ ),
130
+ );
131
+ }
132
+ current = next.href;
133
+ }
134
+ }
135
+
136
+ async function fetchOnce<TResponseType extends FetchResponseType = "text">(
137
+ url: string,
138
+ opts: FetchOptions<TResponseType> = {},
82
139
  ): Promise<FetchResult<FetchBody<TResponseType>>> {
83
140
  const headers: Record<string, string> = { ...(opts.headers ?? {}) };
84
141
 
@@ -229,7 +286,9 @@ function parseSetCookie(raw: string, defaultDomain: string): Cookie | null {
229
286
  for (const attr of parts.slice(1)) {
230
287
  const lower = attr.toLowerCase();
231
288
  if (lower.startsWith("domain=")) {
232
- const d = attr.slice(7).trim();
289
+ const d = attr.slice(7).trim().toLowerCase();
290
+ // A server may only scope a cookie to its own host or a parent domain.
291
+ if (!d || !domainMatches(d, defaultDomain)) return null;
233
292
  cookie.domain = d.startsWith(".") ? d : `.${d}`;
234
293
  } else if (lower.startsWith("path=")) {
235
294
  cookie.path = attr.slice(5).trim() || "/";
@@ -255,5 +314,8 @@ function parseSetCookie(raw: string, defaultDomain: string): Cookie | null {
255
314
  }
256
315
  }
257
316
 
317
+ // A cookie that breaks its name prefix is dropped, as a browser would.
318
+ if (violatesCookiePrefix(cookie)) return null;
319
+
258
320
  return cookie;
259
321
  }
@@ -133,10 +133,10 @@ carries a reason instead of \`(no intent)\`; the [tool-intent
133
133
  guide](https://harnery.com/guides/tool-intent/) owns the details.
134
134
 
135
135
  **Messaging another agent.** \`${b} agents ping <name> "<message>"\` reaches an
136
- agent by name whether or not they are running: a live agent sees it on their next
137
- prompt, and a dormant name holds it until a session of that name next starts.
138
- Send it as your first and only step. Do not check who is live first, and do not
139
- hunt for somewhere else to leave it. Only a never-used name is refused.
136
+ agent by name, running or not, here or on another machine sharing this origin:
137
+ a live agent sees it on their next prompt (after a fetch, if remote), and a
138
+ dormant name holds it until that session next starts. Send it as your only step;
139
+ do not check who is live first or hunt elsewhere. Only an unused name is refused.
140
140
 
141
141
  **Journal.** \`${b} journal add <category> "<text>"\` (category = ${journalCats})
142
142
  leaves breadcrumbs that survive context compaction;
@@ -1,26 +0,0 @@
1
- import type { ProcessGenerationVerdict } from "./proc-start.js";
2
- export interface ProcessHandleSignalResult {
3
- sent: boolean;
4
- verdict: ProcessGenerationVerdict;
5
- }
6
- export type NativeProcessHelperRunner = (command: string, args: string[], options: {
7
- input: string;
8
- encoding: "utf8";
9
- timeout: number;
10
- maxBuffer: number;
11
- windowsHide: boolean;
12
- }) => {
13
- status: number | null;
14
- stdout: string | Buffer | null;
15
- error?: Error;
16
- };
17
- export declare const LINUX_PROCESS_HANDLE_SCRIPT = "\nimport json, os, pathlib, signal, sys\nresult = {\"sent\": False, \"verdict\": \"unknown\"}\nfd = None\ntry:\n request = json.loads(sys.stdin.readline())\n if not hasattr(os, \"pidfd_open\") or not hasattr(signal, \"pidfd_send_signal\"):\n raise RuntimeError(\"pidfd support unavailable\")\n fd = os.pidfd_open(request[\"pid\"])\n stat = pathlib.Path(\"/proc/%d/stat\" % request[\"pid\"]).read_text()\n ticks = stat[stat.rfind(\") \") + 2:].split()[19]\n boot = pathlib.Path(\"/proc/sys/kernel/random/boot_id\").read_text().strip().replace(\"-\", \"\")[:8]\n token = \"l%s.%s\" % (boot, ticks)\n result[\"token\"] = token\n recorded = request.get(\"token\")\n result[\"verdict\"] = \"same\" if token == recorded else \"different\"\n if request[\"mode\"] == \"signal\" and result[\"verdict\"] == \"same\":\n print(json.dumps({\"phase\": \"verified\"}), flush=True)\n if sys.stdin.readline().strip() == \"signal\":\n signal.pidfd_send_signal(fd, request[\"signal\"])\n result[\"sent\"] = True\nexcept Exception:\n pass\nfinally:\n if fd is not None:\n os.close(fd)\nprint(json.dumps(result), flush=True)\n";
18
- export declare const WINDOWS_PROCESS_HANDLE_SCRIPT = "\n$ErrorActionPreference = 'Stop'\n$result = @{ sent = $false; verdict = 'unknown' }\n$handle = [IntPtr]::Zero\ntry {\n Add-Type -TypeDefinition @'\nusing System;\nusing System.Runtime.InteropServices;\npublic static class HarneryProcessHandle {\n [DllImport(\"kernel32.dll\", SetLastError=true)] public static extern IntPtr OpenProcess(uint access, bool inherit, uint processId);\n [DllImport(\"kernel32.dll\", SetLastError=true)] public static extern bool GetProcessTimes(IntPtr handle, out long creation, out long exit, out long kernel, out long user);\n [DllImport(\"kernel32.dll\", SetLastError=true)] public static extern bool TerminateProcess(IntPtr handle, uint exitCode);\n [DllImport(\"kernel32.dll\")] public static extern uint WaitForSingleObject(IntPtr handle, uint milliseconds);\n [DllImport(\"kernel32.dll\")] public static extern bool CloseHandle(IntPtr handle);\n}\n'@\n $request = [Console]::In.ReadLine() | ConvertFrom-Json\n $access = [uint32]0x101000\n if ($request.mode -eq 'signal') { $access = $access -bor 1 }\n $handle = [HarneryProcessHandle]::OpenProcess($access, $false, [uint32]$request.pid)\n if ($handle -eq [IntPtr]::Zero) { throw 'process handle unavailable' }\n [long]$creation = 0; [long]$exitTime = 0; [long]$kernel = 0; [long]$userTime = 0\n if (-not [HarneryProcessHandle]::GetProcessTimes($handle, [ref]$creation, [ref]$exitTime, [ref]$kernel, [ref]$userTime)) { throw 'process time unavailable' }\n $token = 'w' + $creation.ToString([Globalization.CultureInfo]::InvariantCulture)\n $result.token = $token\n $result.verdict = if ($token -ceq $request.token) { 'same' } else { 'different' }\n if ($request.mode -eq 'signal' -and $result.verdict -eq 'same') {\n [Console]::Out.WriteLine('{\"phase\":\"verified\"}')\n [Console]::Out.Flush()\n if ([Console]::In.ReadLine() -eq 'signal' -and [HarneryProcessHandle]::WaitForSingleObject($handle, 0) -eq 258) {\n $result.sent = [HarneryProcessHandle]::TerminateProcess($handle, 1)\n }\n }\n} catch {} finally {\n if ($handle -ne [IntPtr]::Zero) { [void][HarneryProcessHandle]::CloseHandle($handle) }\n}\n$result | ConvertTo-Json -Compress\n";
19
- /** Windows creation FILETIME, read from an OpenProcess handle. */
20
- export declare function windowsProcessStartToken(pid: number, run?: NativeProcessHelperRunner): string | null;
21
- /** No numeric-PID signaling fallback is permitted when an exact handle fails. */
22
- export declare function signalProcessWithHandle(pid: number, recordedToken: string, signal: NodeJS.Signals | number, options?: {
23
- platform?: NodeJS.Platform;
24
- run?: NativeProcessHelperRunner;
25
- }): ProcessHandleSignalResult;
26
- //# sourceMappingURL=process-handle.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"process-handle.d.ts","sourceRoot":"","sources":["../../../../src/core/agents/state/process-handle.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,iBAAiB,CAAC;AAEhE,MAAM,WAAW,yBAAyB;IACxC,IAAI,EAAE,OAAO,CAAC;IACd,OAAO,EAAE,wBAAwB,CAAC;CACnC;AAMD,MAAM,MAAM,yBAAyB,GAAG,CACtC,OAAO,EAAE,MAAM,EACf,IAAI,EAAE,MAAM,EAAE,EACd,OAAO,EAAE;IACP,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,OAAO,CAAC;CACtB,KACE;IAAE,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAAC;IAAC,KAAK,CAAC,EAAE,KAAK,CAAA;CAAE,CAAC;AAI9E,eAAO,MAAM,2BAA2B,+qCA2BvC,CAAC;AAEF,eAAO,MAAM,6BAA6B,qnEAqCzC,CAAC;AAwCF,kEAAkE;AAClE,wBAAgB,wBAAwB,CACtC,GAAG,EAAE,MAAM,EACX,GAAG,GAAE,yBAAqC,GACzC,MAAM,GAAG,IAAI,CAGf;AAED,iFAAiF;AACjF,wBAAgB,uBAAuB,CACrC,GAAG,EAAE,MAAM,EACX,aAAa,EAAE,MAAM,EACrB,MAAM,EAAE,MAAM,CAAC,OAAO,GAAG,MAAM,EAC/B,OAAO,GAAE;IAAE,QAAQ,CAAC,EAAE,MAAM,CAAC,QAAQ,CAAC;IAAC,GAAG,CAAC,EAAE,yBAAyB,CAAA;CAAO,GAC5E,yBAAyB,CAgB3B"}