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
@@ -78,6 +78,10 @@ import {
78
78
  readLiveCoordinationRows,
79
79
  } from "../core/agents/state/live-coordination-view.ts";
80
80
  import { forkRelationLabels, resolveForkAncestry } from "../core/agents/state/names.ts";
81
+ import {
82
+ type AgentParentReference,
83
+ resolveAgentParentReference,
84
+ } from "../core/agents/state/parent-reference.ts";
81
85
  import {
82
86
  type AgentActivity,
83
87
  foldSessionState,
@@ -114,6 +118,7 @@ import {
114
118
  import {
115
119
  listHookProducerStateRecordsV3,
116
120
  readHookProducerStateV3,
121
+ scanHookProducerStateRecordsV3,
117
122
  } from "../core/events/v3/producers/recorder.ts";
118
123
  import { readLedgerV3 } from "../core/events/v3/reader.ts";
119
124
  import { EVENT_V3_LEDGER_RELATIVE_ROOT } from "../core/events/v3/writer.ts";
@@ -124,7 +129,11 @@ import {
124
129
  readRuntimeContextUsage,
125
130
  } from "../core/hooks/adapter/runtime-telemetry.ts";
126
131
  import { HarneryInboxCapacityError } from "../core/inbox/contract.ts";
127
- import { peerInbox, sendPeerMessage } from "../core/inbox/peer-messages.ts";
132
+ import {
133
+ type PeerRecipientResult,
134
+ peerInbox,
135
+ sendPeerMessage,
136
+ } from "../core/inbox/peer-messages.ts";
128
137
  import { type RemoteMachine, readRemoteMachines } from "../core/presence/index.ts";
129
138
  import { formatResourceSummary, readResourceStatus } from "../core/resources/status.ts";
130
139
  import { registerContextCommand } from "./context.ts";
@@ -532,17 +541,31 @@ export function registerAgentsCommand(
532
541
  .command("ping <name> <message...>")
533
542
  .description(
534
543
  "Queue a durable message for one known task instance by name. Stale or " +
535
- "done tasks receive it when that same task resumes. Ambiguous names " +
544
+ "done tasks receive it when that same task resumes. A recipient on " +
545
+ "another machine is reached through the shared origin. Ambiguous names " +
536
546
  "require an exact instance; receipts do not confirm attention.",
537
547
  )
538
548
  .option("--instance-id <id>", "Select the exact known recipient instance")
549
+ .option("--generation-id <id>", "Require the exact recipient generation")
550
+ .option("--operation-id <id>", "Reuse a frozen delivery operation after a caller retry")
551
+ .option(
552
+ "--operation-replay-only",
553
+ "Require an existing plan after an ambiguous caller dispatch",
554
+ )
539
555
  .option("--require-live", "Refuse a recipient outside live presence")
540
556
  .option("--json", "JSON output")
541
557
  .action(
542
558
  (
543
559
  name: string,
544
560
  message: string[],
545
- opts: { json?: boolean; instanceId?: string; requireLive?: boolean },
561
+ opts: {
562
+ json?: boolean;
563
+ instanceId?: string;
564
+ generationId?: string;
565
+ operationId?: string;
566
+ operationReplayOnly?: boolean;
567
+ requireLive?: boolean;
568
+ },
546
569
  ) => {
547
570
  runPing(name, message.join(" "), opts);
548
571
  },
@@ -1754,6 +1777,7 @@ async function runShow(name: string, opts: { json?: boolean }): Promise<void> {
1754
1777
  tool_counts: report?.tool_counts ?? [],
1755
1778
  total_events: report?.total_events ?? 0,
1756
1779
  bq_error: bqError,
1780
+ parent_reference: resolveAgentParentReference(root, hb),
1757
1781
  };
1758
1782
 
1759
1783
  if (opts.json) {
@@ -1774,6 +1798,7 @@ async function runShow(name: string, opts: { json?: boolean }): Promise<void> {
1774
1798
  ` session ${formatAge(ageSecs)} old · kind=${data.kind} · session_id=${data.session_id.slice(0, 8)}…`,
1775
1799
  );
1776
1800
  lines.push(` last heartbeat: ${formatAge(heartbeatAgeSecs)} ago`);
1801
+ lines.push(` parent: ${describeParentReference(data.parent_reference)}`);
1777
1802
  lines.push(` activity: ${data.activity}`);
1778
1803
  lines.push(
1779
1804
  ` lifecycle: ${data.task_state}${data.task_state === "blocked" && data.task_state_reason ? `: ${data.task_state_reason}` : ""}`,
@@ -2542,6 +2567,7 @@ function runStatus(opts: {
2542
2567
  name: displayName,
2543
2568
  instance_id: hb.instance_id,
2544
2569
  kind: normalizeKind(hb.kind),
2570
+ parent_reference: resolveAgentParentReference(root, hb),
2545
2571
  session_age_secs: ageSecs,
2546
2572
  turn_elapsed_secs: turnElapsed ? turnElapsed.elapsed.secs : null,
2547
2573
  turn_complete: turnElapsed ? turnElapsed.elapsed.complete : null,
@@ -2596,6 +2622,7 @@ function runStatus(opts: {
2596
2622
  ["changes", turnChanges.value],
2597
2623
  ["activity", activityOf(hb)],
2598
2624
  ["lifecycle", lifecycleLabel(hb)],
2625
+ ["parent", describeParentReference(data.parent_reference)],
2599
2626
  ["context", ctxStr],
2600
2627
  ["resources", formatResourceSummary(resources)],
2601
2628
  ["files", filesStr],
@@ -3299,7 +3326,13 @@ export function collectEventLedgerHealthV3(root: string, nowMs = Date.now()): Ev
3299
3326
  [];
3300
3327
  const spanPressure: Extract<EventLedgerHealthV3, { state: "live" }>["span_pressure"] = [];
3301
3328
  try {
3302
- for (const { state } of listHookProducerStateRecordsV3(root)) {
3329
+ const scan = scanHookProducerStateRecordsV3(root);
3330
+ for (const file of scan.invalid) {
3331
+ collectionErrors.push(
3332
+ `producer state skipped (${file.adapter}/${file.session_hash}.json): ${file.reason}`,
3333
+ );
3334
+ }
3335
+ for (const { state } of scan.records) {
3303
3336
  if (state.spans.length >= SPAN_PRESSURE_SOFT_WATERMARK) {
3304
3337
  spanPressure.push({
3305
3338
  instance_id: state.instance_id,
@@ -4878,10 +4911,42 @@ function formatDuration(ms: number): string {
4878
4911
  return h > 0 ? `${d}d${h}h` : `${d}d`;
4879
4912
  }
4880
4913
 
4914
+ export function describeParentReference(reference: AgentParentReference): string {
4915
+ return reference.state === "verified"
4916
+ ? `${reference.parent_name ?? "unnamed"} (${reference.parent_instance_id}; ${reference.source}; ${reference.addressable ? "addressable" : "not addressable"})`
4917
+ : `${reference.state}: ${reference.reason}`;
4918
+ }
4919
+
4920
+ export function formatPeerRecipientReceipt(recipient: PeerRecipientResult): string {
4921
+ if (!recipient.ok) return `${recipient.code}: ${recipient.message}`;
4922
+ const address = `agent-${recipient.target.name} (${recipient.target.instance_id})`;
4923
+ if (recipient.delivery_state === "delivered")
4924
+ return `Previously acknowledged by the recipient machine for ${address}; task attention is not confirmed.`;
4925
+ if (recipient.delivery_state === "expired")
4926
+ return `The prior remote message for ${address} expired; no new delivery is established.`;
4927
+ if (recipient.target.machine)
4928
+ return (
4929
+ `Stored for ${address} on ${recipient.target.machine} as ${recipient.message_id}. ` +
4930
+ (recipient.published
4931
+ ? "Published to origin; recipient-machine import is not confirmed."
4932
+ : `Publication is not confirmed${recipient.publish_error ? ` (${recipient.publish_error})` : ""}.`)
4933
+ );
4934
+ return recipient.state === "duplicate"
4935
+ ? `Previously stored for ${address} as ${recipient.message_id}; current surface state is not established by this receipt.`
4936
+ : `Stored for ${address} as ${recipient.message_id}; task attention is not confirmed.`;
4937
+ }
4938
+
4881
4939
  function runPing(
4882
4940
  name: string,
4883
4941
  message: string,
4884
- opts: { json?: boolean; instanceId?: string; requireLive?: boolean },
4942
+ opts: {
4943
+ json?: boolean;
4944
+ instanceId?: string;
4945
+ generationId?: string;
4946
+ operationId?: string;
4947
+ operationReplayOnly?: boolean;
4948
+ requireLive?: boolean;
4949
+ },
4885
4950
  ): void {
4886
4951
  if (!message || message.trim().length === 0) {
4887
4952
  emit.error({ code: "empty_message", message: "message is required (and non-empty)" });
@@ -4910,10 +4975,17 @@ function runPing(
4910
4975
  try {
4911
4976
  sent = sendPeerMessage({
4912
4977
  coordRoot,
4913
- target: { name, instanceId: opts.instanceId, requireLive: opts.requireLive },
4978
+ target: {
4979
+ name,
4980
+ instanceId: opts.instanceId,
4981
+ generationId: opts.generationId,
4982
+ requireLive: opts.requireLive,
4983
+ },
4984
+ operationId: opts.operationId,
4985
+ operationReplayOnly: opts.operationReplayOnly,
4914
4986
  senderName: fromName,
4915
4987
  senderInstanceId: myOwner,
4916
- body: message.trim(),
4988
+ body: message,
4917
4989
  });
4918
4990
  } catch (error) {
4919
4991
  if (error instanceof HarneryInboxCapacityError) {
@@ -4922,51 +4994,84 @@ function runPing(
4922
4994
  }
4923
4995
  throw error;
4924
4996
  }
4997
+ if (opts.json) emit.config({ format: "json" });
4998
+ const routing = {
4999
+ operation_id: sent.operation_id,
5000
+ child: sent.child,
5001
+ parent_reference: sent.parent_reference,
5002
+ parent_copy: sent.parent_copy,
5003
+ complete: sent.complete,
5004
+ };
5005
+ // Keep both actual recipient writes visible, including parent-only success.
5006
+ for (const recipient of [sent.child, sent.parent_copy]) {
5007
+ if (!("ok" in recipient) || !recipient.ok) continue;
5008
+ emitEventV3({
5009
+ owner: myOwner,
5010
+ session: nativeSessionIdentity(myHb, myOwner),
5011
+ adapter: normalizeAdapter(myHb?.platform),
5012
+ observation: {
5013
+ event_type: "coord.message_observed",
5014
+ direction: "sent",
5015
+ subject: recipient.target.instance_id,
5016
+ message_id: recipient.message_id,
5017
+ body:
5018
+ recipient === sent.child
5019
+ ? message
5020
+ : `[Parent copy for agent-${sent.child.target?.name} (${sent.child.target?.instance_id})]\n${message}`,
5021
+ },
5022
+ });
5023
+ }
4925
5024
  if (!sent.ok) {
4926
- emit.error({
5025
+ emit.data({
5026
+ ...routing,
5027
+ ok: false,
4927
5028
  code: sent.code,
4928
5029
  message: sent.message,
5030
+ from: fromName,
4929
5031
  ...(sent.candidates ? { candidates: sent.candidates } : {}),
4930
5032
  });
4931
- process.exit(1);
5033
+ process.exitCode = 1;
5034
+ return;
4932
5035
  }
4933
5036
 
4934
- // Canonical delivery record: sender is the envelope owner, recipient rides
4935
- // in data, so read-only observers can render the communication without
4936
- // joining journal files.
4937
- emitEventV3({
4938
- owner: myOwner,
4939
- session: nativeSessionIdentity(myHb, myOwner),
4940
- adapter: normalizeAdapter(myHb?.platform),
4941
- observation: {
4942
- event_type: "coord.message_observed",
4943
- direction: "sent",
4944
- subject: sent.target.instance_id,
4945
- message_id: sent.message_id,
4946
- body: message.trim(),
4947
- },
4948
- });
4949
-
5037
+ const remote = "published" in sent ? sent : null;
4950
5038
  const data = {
5039
+ ...routing,
4951
5040
  peer: sent.target.name,
4952
5041
  peer_instance_id: sent.target.instance_id,
5042
+ ...(sent.target.machine ? { peer_machine: sent.target.machine } : {}),
4953
5043
  state: sent.state,
4954
5044
  message_id: sent.message_id,
4955
5045
  synced: sent.synced,
4956
5046
  body_bytes: sent.body_bytes,
4957
5047
  from: fromName,
5048
+ ...(remote
5049
+ ? {
5050
+ published: remote.published,
5051
+ ...(remote.delivery_state ? { delivery_state: remote.delivery_state } : {}),
5052
+ ...(remote.publish_error ? { publish_error: remote.publish_error } : {}),
5053
+ }
5054
+ : {}),
4958
5055
  };
4959
5056
 
4960
5057
  if (opts.json) {
4961
5058
  emit.config({ format: "json" });
4962
5059
  emit.data(data);
5060
+ if (!sent.complete) process.exitCode = 1;
4963
5061
  return;
4964
5062
  }
4965
5063
  emit.data(data);
5064
+ if (!sent.complete) process.exitCode = 1;
4966
5065
  emit.text(
4967
- `${data.state} for agent-${data.peer} (${data.peer_instance_id}); ` +
4968
- `stored as ${data.message_id}, pending until surfaced to that task.\n`,
5066
+ `parent copy: ${
5067
+ "ok" in sent.parent_copy
5068
+ ? sent.parent_copy.ok
5069
+ ? formatPeerRecipientReceipt(sent.parent_copy)
5070
+ : `${sent.parent_copy.code}: ${sent.parent_copy.message}`
5071
+ : `skipped (${sent.parent_copy.reason})`
5072
+ }\n`,
4969
5073
  );
5074
+ emit.text(`${formatPeerRecipientReceipt(sent.child)}\n`);
4970
5075
  }
4971
5076
 
4972
5077
  async function runWait(
@@ -74,6 +74,7 @@ import {
74
74
  } from "../lib/browser/page-review-budget.ts";
75
75
  import {
76
76
  PAGE_REVIEW_CAPTURE_PLAN_SCHEMA,
77
+ PAGE_REVIEW_MAX_TILE_BUDGET,
77
78
  type PageReviewCapturePlan,
78
79
  type PageReviewContextAllocation,
79
80
  type PageReviewSourceEvidence,
@@ -137,10 +138,19 @@ import {
137
138
  keepWindowsChromeSession,
138
139
  stopWindowsChromeProfile,
139
140
  windowsChromePath,
141
+ windowsChromeProfileProcesses,
140
142
  windowsLocalAppData,
143
+ windowsPathToWsl,
141
144
  windowsPlainProfile,
142
145
  wslPathToWindows,
143
146
  } from "../lib/browser/windows-chrome.ts";
147
+ import {
148
+ type LoginSyncDeps,
149
+ type LoginSyncResult,
150
+ profileCookieDbMtimeMs,
151
+ recordLoginSync,
152
+ syncLoginCookies,
153
+ } from "../lib/browser/windows-login-sync.ts";
144
154
  import { applyExtraCookies, CookieJar, mergeCookies } from "../lib/cookies/index.ts";
145
155
  import { commandPaceGate } from "../lib/pace/index.ts";
146
156
 
@@ -216,6 +226,7 @@ interface BrowseOpts {
216
226
  collectOut?: string;
217
227
  networkHar?: string;
218
228
  login?: boolean;
229
+ syncLogin?: boolean;
219
230
  attached?: boolean;
220
231
  loginCloseFile?: string;
221
232
  controlFile?: string;
@@ -249,7 +260,9 @@ interface BrowseOpts {
249
260
  checkWidthAnnotate?: boolean;
250
261
  // `--check-runts [selector]`: true = whole body, string = scope selector.
251
262
  checkRunts?: boolean | string;
263
+ checkScanNodeBudget?: string;
252
264
  checkRuntsMinChars?: string;
265
+ checkRuntsMaxFindings?: string;
253
266
  checkRuntsFail?: boolean;
254
267
  checkRuntsAnnotate?: boolean;
255
268
  // Horizontal-overflow check
@@ -350,6 +363,7 @@ const VIEWPORT_PRESETS: Record<string, { width: number; height: number }> = {
350
363
  // cookies/read: the many helper functions in this large file close over
351
364
  // `emit` so action callbacks stay concise.
352
365
  let emit: EmitContext;
366
+ let browseBinName = "harn";
353
367
 
354
368
  export function registerBrowseCommand(
355
369
  program: Command,
@@ -358,6 +372,7 @@ export function registerBrowseCommand(
358
372
  binName = resolveBinName(),
359
373
  ): void {
360
374
  emit = emitParam;
375
+ browseBinName = binName;
361
376
  program
362
377
  .command("browse <url>")
363
378
  .description(
@@ -469,9 +484,17 @@ export function registerBrowseCommand(
469
484
  .option(
470
485
  "--login",
471
486
  "Open the URL in the installed Google Chrome with nothing attached (no DevTools protocol, no " +
472
- "Playwright) so a person can sign in; under WSL this is the Windows host's Chrome. Sign in, then " +
473
- "close the window or create the --login-close-file; later browse and browse-session runs reuse " +
474
- "the cookies. Sites cannot tell this window from the person's own browser.",
487
+ "Playwright) so a person can sign in; under WSL this is the Windows host's Chrome on a dedicated " +
488
+ "sign-in profile. Sign in, then close the window or create the --login-close-file; later browse " +
489
+ "and browse-session runs reuse the cookies. Under WSL the profile can also stay in use as an " +
490
+ "ordinary browser: each headless browse copies the target site's cookies from it when it changed " +
491
+ "and is closed, and an open profile is left alone (see --sync-login). Sites cannot tell this " +
492
+ "window from the person's own browser.",
493
+ )
494
+ .option(
495
+ "--sync-login",
496
+ "WSL only: copy the URL's site cookies from the Windows sign-in profile into the cookie store " +
497
+ "now, then exit without loading the page. Refuses while Chrome is open on that profile.",
475
498
  )
476
499
  .option(
477
500
  "--attached",
@@ -621,6 +644,12 @@ export function registerBrowseCommand(
621
644
  "Minimum block text length to scan (default 40; smaller labels can't meaningfully wrap).",
622
645
  "40",
623
646
  )
647
+ .option(
648
+ "--check-runts-max-findings <n>",
649
+ "Maximum runt findings to retain (integer 1..5000, default 50). Reaching the limit " +
650
+ "still marks the sweep incomplete; the separate 5000-block scan cap is unchanged.",
651
+ "50",
652
+ )
624
653
  .option(
625
654
  "--check-runts-fail",
626
655
  "Run a document-wide runt check when needed; exit 2 on a runt, missing scope, or incomplete sweep.",
@@ -710,6 +739,10 @@ export function registerBrowseCommand(
710
739
  )
711
740
  .option("--check-crowd-fail", "Exit 2 when a crowd target fails, is unknown, or is missing.")
712
741
  .option("--no-check-crowd-annotate", "Skip crowd screenshot annotations.")
742
+ .option(
743
+ "--check-scan-node-budget <n>",
744
+ "Explicit geometry/content traversal budget per scope (1–100000 nodes). Omit to keep each rule's existing limit; incomplete scans remain unknown.",
745
+ )
713
746
  .option(
714
747
  "--check-placeholder [selector]",
715
748
  "Scan rendered text for unrendered template tokens and bound-value tells: " +
@@ -786,7 +819,7 @@ export function registerBrowseCommand(
786
819
  )
787
820
  .option(
788
821
  "--check-critique-max-tiles <n>",
789
- "Cap on tiles sent to the model, to bound cost (default 24).",
822
+ `Cap on tiles sent to the model, to bound cost (default 24, at most ${PAGE_REVIEW_MAX_TILE_BUDGET}).`,
790
823
  "24",
791
824
  )
792
825
  .option("--check-critique-rubric <text>", "Override the default critique rubric.")
@@ -1000,6 +1033,23 @@ async function runBrowse(
1000
1033
  "--check-overlap-threshold",
1001
1034
  );
1002
1035
  const crowdMin = parseNonNegativeNumber(opts.checkCrowdMin ?? "6", "--check-crowd-min");
1036
+ const scanNodeBudget =
1037
+ opts.checkScanNodeBudget === undefined
1038
+ ? undefined
1039
+ : parsePositiveInt(opts.checkScanNodeBudget, "--check-scan-node-budget");
1040
+ if (
1041
+ scanNodeBudget !== undefined &&
1042
+ (!Number.isSafeInteger(scanNodeBudget) || scanNodeBudget > 100_000)
1043
+ ) {
1044
+ throw new Error("--check-scan-node-budget must be an integer from 1 to 100000.");
1045
+ }
1046
+ const runtsMaxFindings = parsePositiveInt(
1047
+ opts.checkRuntsMaxFindings ?? "50",
1048
+ "--check-runts-max-findings",
1049
+ );
1050
+ if (runtsMaxFindings > 5000) {
1051
+ throw new Error("--check-runts-max-findings must be an integer from 1 to 5000.");
1052
+ }
1003
1053
  const hitProfile = parseTargetSizeProfile(opts.checkHitProfile ?? "wcag-aa");
1004
1054
 
1005
1055
  // Commander: `--no-cookies` turns `opts.cookies` into `false`. Default is `true`.
@@ -1048,6 +1098,13 @@ async function runBrowse(
1048
1098
  );
1049
1099
  }
1050
1100
 
1101
+ if (opts.syncLogin) {
1102
+ if (opts.login) throw new Error("--sync-login cannot run with --login.");
1103
+ if (!jar) throw new Error("--sync-login needs the cookie store; drop --no-cookies.");
1104
+ runSyncLogin(url, jar.path);
1105
+ return;
1106
+ }
1107
+
1051
1108
  const collectPlan = opts.collect ? buildCollectPlan(opts) : null;
1052
1109
 
1053
1110
  const pace = commandPaceGate(opts.pace !== false, (message) => emit.log(message, "info"));
@@ -1067,12 +1124,15 @@ async function runBrowse(
1067
1124
  return;
1068
1125
  }
1069
1126
 
1127
+ if (jar && loginMode === "none") autoSyncLoginCookies(url, jar.path);
1128
+
1070
1129
  const channel = resolveBrowserChannel(opts, Boolean(headed));
1071
1130
  const userAgent = resolveUserAgent({
1072
1131
  requested: opts.userAgent,
1073
1132
  env: process.env.HARNERY_BROWSER_UA,
1074
1133
  chromeMajor: launchedChromeMajor(channel),
1075
1134
  });
1135
+ const cookieGuard = context?.cookieGuard?.();
1076
1136
  const browser = new Browser({
1077
1137
  profileDir: opts.profile ?? DEFAULT_PROFILE,
1078
1138
  headed,
@@ -1084,6 +1144,7 @@ async function runBrowse(
1084
1144
  waitUntil: opts.waitUntil as BrowseOpts["waitUntil"] as never,
1085
1145
  recordHarPath: opts.networkHar ? resolve(opts.networkHar) : undefined,
1086
1146
  extraHeaders: context?.extraHeaders,
1147
+ ...(cookieGuard ? { cookieGuard } : {}),
1087
1148
  pace,
1088
1149
  launchArgs: resolveLaunchArgs(opts, Boolean(headed), Boolean(proxy)),
1089
1150
  ...(channel ? { channel } : {}),
@@ -1183,6 +1244,7 @@ async function runBrowse(
1183
1244
  runts = await browser.checkRunts({
1184
1245
  scope: typeof opts.checkRunts === "string" ? opts.checkRunts : null,
1185
1246
  minChars: Number.parseInt(opts.checkRuntsMinChars ?? "40", 10),
1247
+ maxFindings: runtsMaxFindings,
1186
1248
  });
1187
1249
  }
1188
1250
  const hasLayoutLint =
@@ -1194,6 +1256,7 @@ async function runBrowse(
1194
1256
  let layoutLint: LayoutLintResult | undefined;
1195
1257
  if (hasLayoutLint) {
1196
1258
  layoutLint = await browser.checkLayoutLint({
1259
+ scanNodeBudget,
1197
1260
  align: (opts.checkAlign ?? []).map((selector) => ({
1198
1261
  selector,
1199
1262
  axis: alignAxis,
@@ -1231,6 +1294,7 @@ async function runBrowse(
1231
1294
  let content: ContentChecksResult | undefined;
1232
1295
  if (hasContentChecks) {
1233
1296
  content = await browser.checkContent({
1297
+ scanNodeBudget,
1234
1298
  placeholder:
1235
1299
  opts.checkPlaceholder !== undefined
1236
1300
  ? { scope: contentScope(opts.checkPlaceholder) }
@@ -1792,6 +1856,9 @@ async function runBrowse(
1792
1856
  }
1793
1857
  if (failed.length > 0) process.exitCode = 2;
1794
1858
  }
1859
+ } catch (error) {
1860
+ // A cookie guard that stopped the session is the real failure, not the closed-page errors it caused.
1861
+ throw browser.cookieGuardFault ?? error;
1795
1862
  } finally {
1796
1863
  process.removeListener("SIGINT", handleSigint);
1797
1864
  process.removeListener("SIGTERM", handleSigterm);
@@ -1799,6 +1866,7 @@ async function runBrowse(
1799
1866
  await browser.close();
1800
1867
  if (receivedSignal) process.exitCode = receivedSignal === "SIGINT" ? 130 : 143;
1801
1868
  }
1869
+ if (browser.cookieGuardFault) throw browser.cookieGuardFault;
1802
1870
  }
1803
1871
 
1804
1872
  // ---------------------------------------------------------------------------
@@ -2800,7 +2868,7 @@ async function runQaPlanning(
2800
2868
  );
2801
2869
  const band = Number.parseInt(opts.checkCritiqueBand ?? "1400", 10);
2802
2870
  const overlap = Number.parseInt(opts.checkCritiqueOverlap ?? "120", 10);
2803
- const maxTiles = Number.parseInt(opts.checkCritiqueMaxTiles ?? "24", 10);
2871
+ const maxTiles = critiqueTilingKnobs(opts).maxTiles;
2804
2872
  const estimatedFullPageTiles = Math.min(
2805
2873
  maxTiles,
2806
2874
  Math.max(1, Math.ceil(pageHeight / Math.max(1, band - overlap))),
@@ -2833,7 +2901,12 @@ function critiqueTilingKnobs(
2833
2901
  opts: BrowseOpts,
2834
2902
  tileBudgetPx?: number,
2835
2903
  ): { maxTiles: number; band: number; overlap: number } {
2836
- const maxTiles = Math.max(1, Number.parseInt(opts.checkCritiqueMaxTiles ?? "24", 10));
2904
+ // No flag value can exceed the hard per-invocation ceiling.
2905
+ const requested = Number.parseInt(opts.checkCritiqueMaxTiles ?? "24", 10);
2906
+ const maxTiles = Math.min(
2907
+ PAGE_REVIEW_MAX_TILE_BUDGET,
2908
+ Math.max(1, Number.isFinite(requested) ? requested : 24),
2909
+ );
2837
2910
  const bandFlag = Math.max(200, Number.parseInt(opts.checkCritiqueBand ?? "1400", 10));
2838
2911
  const band = tileBudgetPx ? Math.max(200, Math.min(bandFlag, tileBudgetPx)) : bandFlag;
2839
2912
  const overlap = Math.max(0, Number.parseInt(opts.checkCritiqueOverlap ?? "120", 10));
@@ -3452,6 +3525,125 @@ async function runPlainLogin(
3452
3525
  emit.log(`[--login] Chrome closed; cookies persisted in ${profileDir}.`, "info");
3453
3526
  }
3454
3527
 
3528
+ /**
3529
+ * The WSL sign-in profile and the operations the cookie sync needs, or
3530
+ * `undefined` when this machine has none (not WSL, no Windows Chrome, or
3531
+ * `HARNERY_BROWSER_WSL_PLAIN=linux`).
3532
+ */
3533
+ function windowsLoginProfile(): { profileWindows: string; deps: LoginSyncDeps } | undefined {
3534
+ if (!useWindowsPlainLogin()) return undefined;
3535
+ const chromeLinux = windowsChromePath();
3536
+ const localAppData = windowsLocalAppData();
3537
+ if (!chromeLinux || !localAppData) return undefined;
3538
+ const chromeWindows = wslPathToWindows(chromeLinux);
3539
+ const profileWindows = windowsPlainProfile(localAppData);
3540
+ const profileLinux = windowsPathToWsl(profileWindows);
3541
+ return {
3542
+ profileWindows,
3543
+ deps: {
3544
+ cookieDbMtimeMs: () => profileCookieDbMtimeMs(profileLinux),
3545
+ profileProcesses: () => windowsChromeProfileProcesses(profileWindows),
3546
+ harvest: (domain) =>
3547
+ harvestWindowsChromeCookies({
3548
+ chromeWindows,
3549
+ profileWindows,
3550
+ domain,
3551
+ pageTimeoutSeconds: 0,
3552
+ }),
3553
+ },
3554
+ };
3555
+ }
3556
+
3557
+ /** One-line summary of a sync that read cookies. Names only, never values. */
3558
+ function loginSyncSummary(result: LoginSyncResult, storePath: string): string {
3559
+ if (result.status === "synced") {
3560
+ return `Copied ${result.merged} ${result.domain} cookies (${result.sessionCookies} browser-session) from the sign-in profile into ${storePath}: ${result.names.join(", ")}.`;
3561
+ }
3562
+ return `The sign-in profile's ${result.domain} cookies (${result.cookies}) match the last copy; the cookie store was left as it is.`;
3563
+ }
3564
+
3565
+ /**
3566
+ * Before a headless run under WSL, copy the target site's cookies from the
3567
+ * sign-in profile when the profile changed since the last copy and no
3568
+ * Chrome is open on it. Failures are warnings; the run continues.
3569
+ * `HARNERY_BROWSER_LOGIN_SYNC=off` turns it off.
3570
+ */
3571
+ function autoSyncLoginCookies(url: string, storePath: string): void {
3572
+ if ((process.env.HARNERY_BROWSER_LOGIN_SYNC ?? "").toLowerCase() === "off") return;
3573
+ let profile: ReturnType<typeof windowsLoginProfile>;
3574
+ let domain: string;
3575
+ try {
3576
+ profile = windowsLoginProfile();
3577
+ domain = harvestDomain(url);
3578
+ } catch {
3579
+ return;
3580
+ }
3581
+ if (!profile) return;
3582
+ try {
3583
+ const result = syncLoginCookies({ domain, storePath, deps: profile.deps });
3584
+ if (result.status === "open") {
3585
+ emit.log(
3586
+ `[login-sync] The shared sign-in window (Windows Chrome on ${profile.profileWindows}) is open and changed since ${domain} cookies were last copied; this run uses the cookie store as it is. Its ${domain} session reaches headless runs after you close that window, or run \`${browseBinName} browse --sync-login ${url}\` once it is closed.`,
3587
+ "info",
3588
+ );
3589
+ } else if (result.status === "busy") {
3590
+ emit.log(
3591
+ "[login-sync] Another run is copying sign-in cookies; this run did not wait.",
3592
+ "debug",
3593
+ );
3594
+ } else if (result.status === "synced" || result.status === "unchanged") {
3595
+ emit.log(`[login-sync] ${loginSyncSummary(result, storePath)}`, "info");
3596
+ }
3597
+ } catch (err) {
3598
+ const message = err instanceof Error ? err.message : String(err);
3599
+ emit.log(
3600
+ `[login-sync] Could not copy ${domain} cookies from the sign-in profile; continuing with the cookie store as it is. ${message}`,
3601
+ "warn",
3602
+ );
3603
+ }
3604
+ }
3605
+
3606
+ /** `--sync-login`: copy the URL's site cookies from the sign-in profile now. */
3607
+ function runSyncLogin(url: string, storePath: string): void {
3608
+ const profile = windowsLoginProfile();
3609
+ if (!profile) {
3610
+ throw new Error(
3611
+ "--sync-login needs WSL with the Windows Google Chrome; this machine has no Windows sign-in profile.",
3612
+ );
3613
+ }
3614
+ const domain = harvestDomain(url);
3615
+ const result = syncLoginCookies({ domain, storePath, deps: profile.deps, force: true });
3616
+ if (result.status === "no-profile") {
3617
+ throw new Error(
3618
+ `The sign-in profile ${profile.profileWindows} has no cookies yet; sign in with \`${browseBinName} browse --login ${url}\` first.`,
3619
+ );
3620
+ }
3621
+ if (result.status === "open") {
3622
+ throw new Error(
3623
+ `Chrome is open on the sign-in profile ${profile.profileWindows} (process ${result.processes.join(", ")}). Close that window, then run --sync-login again.`,
3624
+ );
3625
+ }
3626
+ if (result.status === "busy") {
3627
+ throw new Error("Another run is copying sign-in cookies right now; try again in a moment.");
3628
+ }
3629
+ if (result.cookies === 0) {
3630
+ emit.log(`[--sync-login] No ${domain} cookies were found in the sign-in profile.`, "warn");
3631
+ } else {
3632
+ emit.log(`[--sync-login] ${loginSyncSummary(result, storePath)}`, "info");
3633
+ }
3634
+ emit.data({
3635
+ loginSync: {
3636
+ domain,
3637
+ store: storePath,
3638
+ status: result.status,
3639
+ cookies: result.cookies,
3640
+ merged: result.merged,
3641
+ sessionCookies: result.sessionCookies,
3642
+ names: result.names,
3643
+ },
3644
+ });
3645
+ }
3646
+
3455
3647
  /**
3456
3648
  * Under WSL, `--login` opens the Windows host's Google Chrome when one
3457
3649
  * is installed. Some sign-in flows reject every browser running inside WSL,
@@ -3528,6 +3720,16 @@ async function runWindowsPlainLogin(
3528
3720
  await Promise.race([exited, new Promise((r) => setTimeout(r, 5000))]);
3529
3721
  const domain = harvestDomain(url);
3530
3722
  const { cookies, pages } = harvestWindowsChromeCookies({ chromeWindows, profileWindows, domain });
3723
+ try {
3724
+ recordLoginSync({
3725
+ domain,
3726
+ storePath,
3727
+ cookies,
3728
+ cookieDbMtimeMs: profileCookieDbMtimeMs(windowsPathToWsl(profileWindows)),
3729
+ });
3730
+ } catch {
3731
+ // The record only saves the next headless run a repeat read.
3732
+ }
3531
3733
  const lastPage = pages[0] ?? null;
3532
3734
  const names = [...new Set(cookies.map((cookie) => cookie.name))].sort();
3533
3735
  const sessionCookies = cookies.filter((cookie) => cookie.expires <= 0).length;
@@ -111,6 +111,7 @@ async function runFetch(
111
111
  signal: ac.signal,
112
112
  extraHeaders: context?.extraHeaders,
113
113
  extraCookies: context?.extraCookies,
114
+ clientRedirects: context?.clientRedirects?.() || undefined,
114
115
  responseType: "bytes",
115
116
  });
116
117
  } finally {