harnery 0.45.2 → 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 (216) 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/core/agents/index.d.ts +1 -0
  21. package/dist/core/agents/index.d.ts.map +1 -1
  22. package/dist/core/agents/index.js +1 -0
  23. package/dist/core/agents/live-authority-v3.d.ts.map +1 -1
  24. package/dist/core/agents/live-authority-v3.js +11 -4
  25. package/dist/core/agents/render/session-context.d.ts +0 -6
  26. package/dist/core/agents/render/session-context.d.ts.map +1 -1
  27. package/dist/core/agents/render/session-context.js +1 -6
  28. package/dist/core/agents/session-events.d.ts.map +1 -1
  29. package/dist/core/agents/session-events.js +12 -4
  30. package/dist/core/agents/state/heartbeat-reader.d.ts +3 -0
  31. package/dist/core/agents/state/heartbeat-reader.d.ts.map +1 -1
  32. package/dist/core/agents/state/live-coordination-view.d.ts +3 -1
  33. package/dist/core/agents/state/live-coordination-view.d.ts.map +1 -1
  34. package/dist/core/agents/state/live-coordination-view.js +6 -2
  35. package/dist/core/agents/state/parent-reference-contract.d.ts +16 -0
  36. package/dist/core/agents/state/parent-reference-contract.d.ts.map +1 -0
  37. package/dist/core/agents/state/parent-reference-contract.js +1 -0
  38. package/dist/core/agents/state/parent-reference.d.ts +18 -0
  39. package/dist/core/agents/state/parent-reference.d.ts.map +1 -0
  40. package/dist/core/agents/state/parent-reference.js +119 -0
  41. package/dist/core/config.d.ts +39 -0
  42. package/dist/core/config.d.ts.map +1 -1
  43. package/dist/core/config.js +63 -0
  44. package/dist/core/diagnostics/path-identity.d.ts +1 -1
  45. package/dist/core/diagnostics/path-identity.d.ts.map +1 -1
  46. package/dist/core/diagnostics/path-identity.js +51 -6
  47. package/dist/core/events/v3/canonical.js +43 -14
  48. package/dist/core/events/v3/index.d.ts +1 -1
  49. package/dist/core/events/v3/index.d.ts.map +1 -1
  50. package/dist/core/events/v3/index.js +1 -1
  51. package/dist/core/events/v3/producers/recorder.d.ts +34 -0
  52. package/dist/core/events/v3/producers/recorder.d.ts.map +1 -1
  53. package/dist/core/events/v3/producers/recorder.js +117 -13
  54. package/dist/core/events/v3/reader.d.ts.map +1 -1
  55. package/dist/core/events/v3/reader.js +53 -13
  56. package/dist/core/events/v3/validate.d.ts.map +1 -1
  57. package/dist/core/events/v3/validate.js +36 -1
  58. package/dist/core/hooks/adapter/native-parent.d.ts +23 -0
  59. package/dist/core/hooks/adapter/native-parent.d.ts.map +1 -0
  60. package/dist/core/hooks/adapter/native-parent.js +91 -0
  61. package/dist/core/hooks/cli.js +79 -4
  62. package/dist/core/hooks/resolve/owner.d.ts +0 -11
  63. package/dist/core/hooks/resolve/owner.d.ts.map +1 -1
  64. package/dist/core/hooks/resolve/owner.js +0 -23
  65. package/dist/core/hooks/shell-admission.d.ts +84 -0
  66. package/dist/core/hooks/shell-admission.d.ts.map +1 -0
  67. package/dist/core/hooks/shell-admission.js +622 -0
  68. package/dist/core/hooks/shell-commit-tree.d.ts +2 -0
  69. package/dist/core/hooks/shell-commit-tree.d.ts.map +1 -0
  70. package/dist/core/hooks/shell-commit-tree.js +104 -0
  71. package/dist/core/inbox/contract.d.ts +12 -0
  72. package/dist/core/inbox/contract.d.ts.map +1 -1
  73. package/dist/core/inbox/contract.js +16 -0
  74. package/dist/core/inbox/peer-messages.d.ts +41 -16
  75. package/dist/core/inbox/peer-messages.d.ts.map +1 -1
  76. package/dist/core/inbox/peer-messages.js +372 -15
  77. package/dist/core/inbox/service.d.ts +8 -1
  78. package/dist/core/inbox/service.d.ts.map +1 -1
  79. package/dist/core/inbox/service.js +83 -3
  80. package/dist/core/inbox/targets.d.ts +17 -3
  81. package/dist/core/inbox/targets.d.ts.map +1 -1
  82. package/dist/core/inbox/targets.js +77 -2
  83. package/dist/core/presence/git.d.ts +18 -12
  84. package/dist/core/presence/git.d.ts.map +1 -1
  85. package/dist/core/presence/git.js +32 -38
  86. package/dist/core/presence/index.d.ts +2 -2
  87. package/dist/core/presence/index.d.ts.map +1 -1
  88. package/dist/core/presence/index.js +8 -6
  89. package/dist/core/presence/mail.d.ts +133 -0
  90. package/dist/core/presence/mail.d.ts.map +1 -0
  91. package/dist/core/presence/mail.js +464 -0
  92. package/dist/core/semantic/contract.d.ts +5 -5
  93. package/dist/core/storage/modes.d.ts +4 -0
  94. package/dist/core/storage/modes.d.ts.map +1 -1
  95. package/dist/core/storage/modes.js +4 -0
  96. package/dist/core/storage/segments.d.ts.map +1 -1
  97. package/dist/core/storage/segments.js +5 -4
  98. package/dist/lib/admission-history.d.ts +96 -0
  99. package/dist/lib/admission-history.d.ts.map +1 -0
  100. package/dist/lib/admission-history.js +196 -0
  101. package/dist/lib/admission.d.ts +56 -7
  102. package/dist/lib/admission.d.ts.map +1 -1
  103. package/dist/lib/admission.js +161 -19
  104. package/dist/lib/browser/client.d.ts +18 -0
  105. package/dist/lib/browser/client.d.ts.map +1 -1
  106. package/dist/lib/browser/client.js +100 -12
  107. package/dist/lib/browser/content-checks.d.ts +3 -1
  108. package/dist/lib/browser/content-checks.d.ts.map +1 -1
  109. package/dist/lib/browser/content-checks.js +10 -2
  110. package/dist/lib/browser/critique.d.ts +32 -0
  111. package/dist/lib/browser/critique.d.ts.map +1 -1
  112. package/dist/lib/browser/critique.js +72 -3
  113. package/dist/lib/browser/geometry.d.ts +2 -0
  114. package/dist/lib/browser/geometry.d.ts.map +1 -1
  115. package/dist/lib/browser/geometry.js +61 -15
  116. package/dist/lib/browser/index.d.ts +1 -1
  117. package/dist/lib/browser/index.d.ts.map +1 -1
  118. package/dist/lib/browser/index.js +1 -1
  119. package/dist/lib/browser/page-review-contracts.d.ts +6 -1
  120. package/dist/lib/browser/page-review-contracts.d.ts.map +1 -1
  121. package/dist/lib/browser/page-review-contracts.js +6 -1
  122. package/dist/lib/browser/page-review-judge.d.ts +3 -1
  123. package/dist/lib/browser/page-review-judge.d.ts.map +1 -1
  124. package/dist/lib/browser/page-review-judge.js +41 -11
  125. package/dist/lib/browser/page-review-pack.d.ts +3 -0
  126. package/dist/lib/browser/page-review-pack.d.ts.map +1 -1
  127. package/dist/lib/browser/page-review-pack.js +5 -2
  128. package/dist/lib/browser/qa-run-contracts.d.ts +11 -0
  129. package/dist/lib/browser/qa-run-contracts.d.ts.map +1 -1
  130. package/dist/lib/browser/qa-run-contracts.js +11 -4
  131. package/dist/lib/browser/qa-run.d.ts.map +1 -1
  132. package/dist/lib/browser/qa-run.js +73 -16
  133. package/dist/lib/browser/runts.d.ts +4 -2
  134. package/dist/lib/browser/runts.d.ts.map +1 -1
  135. package/dist/lib/browser/runts.js +84 -33
  136. package/dist/lib/browser/windows-chrome.d.ts +14 -0
  137. package/dist/lib/browser/windows-chrome.d.ts.map +1 -1
  138. package/dist/lib/browser/windows-chrome.js +42 -0
  139. package/dist/lib/browser/windows-login-sync.d.ts +110 -0
  140. package/dist/lib/browser/windows-login-sync.d.ts.map +1 -0
  141. package/dist/lib/browser/windows-login-sync.js +171 -0
  142. package/dist/lib/cookies/client.d.ts +30 -5
  143. package/dist/lib/cookies/client.d.ts.map +1 -1
  144. package/dist/lib/cookies/client.js +55 -10
  145. package/dist/lib/cookies/index.d.ts +1 -1
  146. package/dist/lib/cookies/index.d.ts.map +1 -1
  147. package/dist/lib/cookies/index.js +1 -1
  148. package/dist/lib/docs-metadata-sync.d.ts.map +1 -1
  149. package/dist/lib/docs-metadata-sync.js +41 -2
  150. package/dist/lib/durable-job.d.ts +8 -0
  151. package/dist/lib/durable-job.d.ts.map +1 -1
  152. package/dist/lib/durable-job.js +10 -0
  153. package/dist/lib/http/client.d.ts +9 -8
  154. package/dist/lib/http/client.d.ts.map +1 -1
  155. package/dist/lib/http/client.js +44 -2
  156. package/dist/lib/instructions/templates.js +4 -4
  157. package/package.json +2 -2
  158. package/src/commander.ts +13 -0
  159. package/src/commands/admission.ts +272 -29
  160. package/src/commands/agents.ts +132 -27
  161. package/src/commands/browse.ts +208 -6
  162. package/src/commands/fetch.ts +1 -0
  163. package/src/commands/ledger-v3.ts +8 -2
  164. package/src/commands/presence.ts +49 -0
  165. package/src/commands/qa-run.ts +4 -3
  166. package/src/commands/review-pack.ts +41 -5
  167. package/src/core/agents/index.ts +6 -0
  168. package/src/core/agents/live-authority-v3.ts +11 -4
  169. package/src/core/agents/render/session-context.ts +1 -7
  170. package/src/core/agents/session-events.ts +18 -3
  171. package/src/core/agents/state/heartbeat-reader.ts +3 -0
  172. package/src/core/agents/state/live-coordination-view.ts +13 -6
  173. package/src/core/agents/state/parent-reference-contract.ts +14 -0
  174. package/src/core/agents/state/parent-reference.ts +179 -0
  175. package/src/core/config.ts +85 -0
  176. package/src/core/diagnostics/path-identity.ts +52 -6
  177. package/src/core/events/v3/canonical.ts +46 -13
  178. package/src/core/events/v3/index.ts +3 -0
  179. package/src/core/events/v3/producers/recorder.ts +151 -13
  180. package/src/core/events/v3/reader.ts +55 -11
  181. package/src/core/events/v3/validate.ts +42 -1
  182. package/src/core/hooks/adapter/native-parent.ts +109 -0
  183. package/src/core/hooks/cli.ts +83 -4
  184. package/src/core/hooks/resolve/owner.ts +0 -23
  185. package/src/core/hooks/shell-admission.ts +634 -0
  186. package/src/core/hooks/shell-commit-tree.ts +102 -0
  187. package/src/core/inbox/contract.ts +18 -0
  188. package/src/core/inbox/peer-messages.ts +501 -15
  189. package/src/core/inbox/service.ts +108 -3
  190. package/src/core/inbox/targets.ts +94 -3
  191. package/src/core/presence/git.ts +45 -37
  192. package/src/core/presence/index.ts +16 -8
  193. package/src/core/presence/mail.ts +620 -0
  194. package/src/core/storage/modes.ts +6 -0
  195. package/src/core/storage/segments.ts +5 -4
  196. package/src/lib/admission-history.ts +267 -0
  197. package/src/lib/admission.ts +217 -19
  198. package/src/lib/browser/client.ts +147 -35
  199. package/src/lib/browser/content-checks.ts +14 -3
  200. package/src/lib/browser/critique.ts +113 -3
  201. package/src/lib/browser/geometry.ts +63 -14
  202. package/src/lib/browser/index.ts +5 -0
  203. package/src/lib/browser/page-review-contracts.ts +6 -1
  204. package/src/lib/browser/page-review-judge.ts +58 -13
  205. package/src/lib/browser/page-review-pack.ts +13 -2
  206. package/src/lib/browser/qa-run-contracts.ts +30 -4
  207. package/src/lib/browser/qa-run.ts +78 -17
  208. package/src/lib/browser/runts.ts +85 -37
  209. package/src/lib/browser/windows-chrome.ts +50 -0
  210. package/src/lib/browser/windows-login-sync.ts +275 -0
  211. package/src/lib/cookies/client.ts +62 -10
  212. package/src/lib/cookies/index.ts +3 -0
  213. package/src/lib/docs-metadata-sync.ts +44 -2
  214. package/src/lib/durable-job.ts +15 -0
  215. package/src/lib/http/client.ts +63 -1
  216. package/src/lib/instructions/templates.ts +4 -4
@@ -44,6 +44,15 @@ export interface FetchOptions<TResponseType extends FetchResponseType = "text">
44
44
  extraCookies?: ExtraCookies;
45
45
  /** Response body representation. Default `text`; use `bytes` for lossless file output. */
46
46
  responseType?: TResponseType;
47
+ /**
48
+ * With `redirect: "follow"`, follow redirects in this client instead of the
49
+ * runtime's fetch. Every hop then gets its own cookie header, `extraHeaders`
50
+ * call, and Set-Cookie persistence, so a caller that inspects those can stop
51
+ * a chain between hops. Credential headers are dropped on a cross-origin hop.
52
+ * Default: on whenever a `jar` is passed (host-only cookies must follow the
53
+ * jar's exact-host rule on every hop), off without one.
54
+ */
55
+ clientRedirects?: boolean;
47
56
  }
48
57
  type FetchBody<TResponseType extends FetchResponseType> = TResponseType extends "bytes" ? Uint8Array : string;
49
58
  export interface FetchResult<TBody = string> {
@@ -55,14 +64,6 @@ export interface FetchResult<TBody = string> {
55
64
  /** Number of cookies persisted back into the jar (0 if no jar passed). */
56
65
  cookiesSaved: number;
57
66
  }
58
- /**
59
- * Fetch a URL with optional cookie-jar attach + persist.
60
- *
61
- * Returns the response body as text by default (callers handle JSON parsing).
62
- * Pass `responseType: "bytes"` when the caller must preserve the body exactly.
63
- * Streams aren't supported; this is a CLI helper, not a streaming HTTP
64
- * client. Use Bun's `fetch` directly for streaming workloads.
65
- */
66
67
  export declare function fetchWithJar<TResponseType extends FetchResponseType = "text">(url: string, opts?: FetchOptions<TResponseType>): Promise<FetchResult<FetchBody<TResponseType>>>;
67
68
  export {};
68
69
  //# sourceMappingURL=client.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../../src/lib/http/client.ts"],"names":[],"mappings":"AAAA,OAAO,EAGL,KAAK,SAAS,EACd,KAAK,YAAY,EAClB,MAAM,qBAAqB,CAAC;AAE7B;;;;;;;;;GASG;AAEH,MAAM,MAAM,iBAAiB,GAAG,MAAM,GAAG,OAAO,CAAC;AAEjD,MAAM,WAAW,YAAY,CAAC,aAAa,SAAS,iBAAiB,GAAG,MAAM;IAC5E,gCAAgC;IAChC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,4EAA4E;IAC5E,IAAI,CAAC,EAAE,QAAQ,GAAG,IAAI,CAAC;IACvB,qDAAqD;IACrD,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACjC;;;;;OAKG;IACH,GAAG,CAAC,EAAE,SAAS,GAAG,IAAI,CAAC;IACvB,mEAAmE;IACnE,QAAQ,CAAC,EAAE,eAAe,CAAC;IAC3B,8CAA8C;IAC9C,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB;;;;;;OAMG;IACH,YAAY,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACvD;;;;OAIG;IACH,YAAY,CAAC,EAAE,YAAY,CAAC;IAC5B,0FAA0F;IAC1F,YAAY,CAAC,EAAE,aAAa,CAAC;CAC9B;AAED,KAAK,SAAS,CAAC,aAAa,SAAS,iBAAiB,IAAI,aAAa,SAAS,OAAO,GACnF,UAAU,GACV,MAAM,CAAC;AAEX,MAAM,WAAW,WAAW,CAAC,KAAK,GAAG,MAAM;IACzC,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,MAAM,CAAC;IACnB,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAChC,IAAI,EAAE,KAAK,CAAC;IACZ,0EAA0E;IAC1E,YAAY,EAAE,MAAM,CAAC;CACtB;AAED;;;;;;;GAOG;AACH,wBAAsB,YAAY,CAAC,aAAa,SAAS,iBAAiB,GAAG,MAAM,EACjF,GAAG,EAAE,MAAM,EACX,IAAI,GAAE,YAAY,CAAC,aAAa,CAAM,GACrC,OAAO,CAAC,WAAW,CAAC,SAAS,CAAC,aAAa,CAAC,CAAC,CAAC,CA+EhD"}
1
+ {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../../src/lib/http/client.ts"],"names":[],"mappings":"AAAA,OAAO,EAGL,KAAK,SAAS,EAEd,KAAK,YAAY,EAElB,MAAM,qBAAqB,CAAC;AAE7B;;;;;;;;;GASG;AAEH,MAAM,MAAM,iBAAiB,GAAG,MAAM,GAAG,OAAO,CAAC;AAEjD,MAAM,WAAW,YAAY,CAAC,aAAa,SAAS,iBAAiB,GAAG,MAAM;IAC5E,gCAAgC;IAChC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,4EAA4E;IAC5E,IAAI,CAAC,EAAE,QAAQ,GAAG,IAAI,CAAC;IACvB,qDAAqD;IACrD,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACjC;;;;;OAKG;IACH,GAAG,CAAC,EAAE,SAAS,GAAG,IAAI,CAAC;IACvB,mEAAmE;IACnE,QAAQ,CAAC,EAAE,eAAe,CAAC;IAC3B,8CAA8C;IAC9C,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB;;;;;;OAMG;IACH,YAAY,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACvD;;;;OAIG;IACH,YAAY,CAAC,EAAE,YAAY,CAAC;IAC5B,0FAA0F;IAC1F,YAAY,CAAC,EAAE,aAAa,CAAC;IAC7B;;;;;;;OAOG;IACH,eAAe,CAAC,EAAE,OAAO,CAAC;CAC3B;AAED,KAAK,SAAS,CAAC,aAAa,SAAS,iBAAiB,IAAI,aAAa,SAAS,OAAO,GACnF,UAAU,GACV,MAAM,CAAC;AAEX,MAAM,WAAW,WAAW,CAAC,KAAK,GAAG,MAAM;IACzC,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,MAAM,CAAC;IACnB,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAChC,IAAI,EAAE,KAAK,CAAC;IACZ,0EAA0E;IAC1E,YAAY,EAAE,MAAM,CAAC;CACtB;AAaD,wBAAsB,YAAY,CAAC,aAAa,SAAS,iBAAiB,GAAG,MAAM,EACjF,GAAG,EAAE,MAAM,EACX,IAAI,GAAE,YAAY,CAAC,aAAa,CAAM,GACrC,OAAO,CAAC,WAAW,CAAC,SAAS,CAAC,aAAa,CAAC,CAAC,CAAC,CAsChD"}
@@ -1,4 +1,4 @@
1
- import { applyExtraCookies, } from "../cookies/index.js";
1
+ import { applyExtraCookies, domainMatches, violatesCookiePrefix, } from "../cookies/index.js";
2
2
  /**
3
3
  * Fetch a URL with optional cookie-jar attach + persist.
4
4
  *
@@ -7,7 +7,43 @@ import { applyExtraCookies, } from "../cookies/index.js";
7
7
  * Streams aren't supported; this is a CLI helper, not a streaming HTTP
8
8
  * client. Use Bun's `fetch` directly for streaming workloads.
9
9
  */
10
+ const REDIRECT_STATUSES = new Set([301, 302, 303, 307, 308]);
11
+ const MAX_CLIENT_REDIRECTS = 20;
10
12
  export async function fetchWithJar(url, opts = {}) {
13
+ // With a jar, redirects are followed here so every hop gets its own Cookie
14
+ // header from the jar (exact-host for host-only cookies) and its own
15
+ // Set-Cookie persistence; the runtime would otherwise carry or drop the
16
+ // first hop's header on its own rules. Pass `clientRedirects: false` to opt out.
17
+ const clientRedirects = opts.clientRedirects ?? Boolean(opts.jar);
18
+ if (!clientRedirects || (opts.redirect ?? "follow") !== "follow")
19
+ return fetchOnce(url, opts);
20
+ let current = url;
21
+ let method = opts.method ?? "GET";
22
+ let body = opts.body;
23
+ let headers = opts.headers;
24
+ let cookiesSaved = 0;
25
+ for (let hop = 0;; hop++) {
26
+ const result = await fetchOnce(current, { ...opts, method, body, headers, redirect: "manual" });
27
+ cookiesSaved += result.cookiesSaved;
28
+ const location = result.headers.location;
29
+ if (!REDIRECT_STATUSES.has(result.status) || !location)
30
+ return { ...result, cookiesSaved };
31
+ if (hop >= MAX_CLIENT_REDIRECTS) {
32
+ throw new Error(`Too many redirects (more than ${MAX_CLIENT_REDIRECTS}) starting at ${url}`);
33
+ }
34
+ const next = new URL(location, current);
35
+ if (result.status === 303 ||
36
+ ((result.status === 301 || result.status === 302) && method !== "GET" && method !== "HEAD")) {
37
+ method = "GET";
38
+ body = undefined;
39
+ }
40
+ if (next.origin !== new URL(current).origin && headers) {
41
+ headers = Object.fromEntries(Object.entries(headers).filter(([name]) => !["authorization", "cookie"].includes(name.toLowerCase())));
42
+ }
43
+ current = next.href;
44
+ }
45
+ }
46
+ async function fetchOnce(url, opts = {}) {
11
47
  const headers = { ...(opts.headers ?? {}) };
12
48
  if (opts.jar) {
13
49
  applyExtraCookies(url, opts.jar, opts.extraCookies);
@@ -145,7 +181,10 @@ function parseSetCookie(raw, defaultDomain) {
145
181
  for (const attr of parts.slice(1)) {
146
182
  const lower = attr.toLowerCase();
147
183
  if (lower.startsWith("domain=")) {
148
- const d = attr.slice(7).trim();
184
+ const d = attr.slice(7).trim().toLowerCase();
185
+ // A server may only scope a cookie to its own host or a parent domain.
186
+ if (!d || !domainMatches(d, defaultDomain))
187
+ return null;
149
188
  cookie.domain = d.startsWith(".") ? d : `.${d}`;
150
189
  }
151
190
  else if (lower.startsWith("path=")) {
@@ -176,5 +215,8 @@ function parseSetCookie(raw, defaultDomain) {
176
215
  cookie.sameSite = v.charAt(0).toUpperCase() + v.slice(1).toLowerCase();
177
216
  }
178
217
  }
218
+ // A cookie that breaks its name prefix is dropped, as a browser would.
219
+ if (violatesCookiePrefix(cookie))
220
+ return null;
179
221
  return cookie;
180
222
  }
@@ -111,10 +111,10 @@ carries a reason instead of \`(no intent)\`; the [tool-intent
111
111
  guide](https://harnery.com/guides/tool-intent/) owns the details.
112
112
 
113
113
  **Messaging another agent.** \`${b} agents ping <name> "<message>"\` reaches an
114
- agent by name whether or not they are running: a live agent sees it on their next
115
- prompt, and a dormant name holds it until a session of that name next starts.
116
- Send it as your first and only step. Do not check who is live first, and do not
117
- hunt for somewhere else to leave it. Only a never-used name is refused.
114
+ agent by name, running or not, here or on another machine sharing this origin:
115
+ a live agent sees it on their next prompt (after a fetch, if remote), and a
116
+ dormant name holds it until that session next starts. Send it as your only step;
117
+ do not check who is live first or hunt elsewhere. Only an unused name is refused.
118
118
 
119
119
  **Journal.** \`${b} journal add <category> "<text>"\` (category = ${journalCats})
120
120
  leaves breadcrumbs that survive context compaction;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "harnery",
3
- "version": "0.45.2",
3
+ "version": "0.46.0",
4
4
  "description": "Multi-agent coordination for AI coding agents - Claude Code, Cursor, and Codex.",
5
5
  "license": "MIT",
6
6
  "author": "Ryan Kelly",
@@ -252,7 +252,7 @@
252
252
  "access": "public",
253
253
  "provenance": true
254
254
  },
255
- "packageManager": "bun@1.4.0",
255
+ "packageManager": "bun@1.4.4",
256
256
  "scripts": {
257
257
  "build": "npm run clean && tsc -p tsconfig.build.json && node scripts/rewrite-dts-extensions.mjs",
258
258
  "build:openclaw-plugin": "mkdir -p openclaw-plugin/dist && bun build openclaw-plugin/src/index.ts openclaw-plugin/src/record-worker.ts --target=node --format=esm --external openclaw --outdir openclaw-plugin/dist --entry-naming '[name].js' && cp openclaw-plugin/package.json openclaw-plugin/openclaw.plugin.json openclaw-plugin/dist/",
package/src/commander.ts CHANGED
@@ -130,6 +130,19 @@ export interface HarneryProgramContext {
130
130
  * harn standalone skips the callback entirely.
131
131
  */
132
132
  extraCookies?: import("./lib/cookies/index.ts").ExtraCookies;
133
+ /**
134
+ * When it returns true for a run, `fetch` follows redirects itself so the
135
+ * cookie header, `extraHeaders`, and Set-Cookie persistence apply to every
136
+ * hop instead of only the first request.
137
+ */
138
+ clientRedirects?: () => boolean;
139
+ /**
140
+ * Called once per `browse` run. Returns the guard that inspects that
141
+ * session's live cookies before each request, after each response, and at
142
+ * close (throwing stops the session and fails the run), or undefined for no
143
+ * guard.
144
+ */
145
+ cookieGuard?: () => import("./lib/browser/client.ts").CookieGuard | undefined;
133
146
  /**
134
147
  * Host-injected vision-model call for `browse --check-critique`. harnery
135
148
  * ships no model client or API key; a consumer wires this to its own
@@ -15,7 +15,7 @@ import { closeSync, openSync } from "node:fs";
15
15
  import { join } from "node:path";
16
16
  import { type Command, Option } from "commander";
17
17
  import type { EmitContext } from "../commander.ts";
18
- import { resolveBinName } from "../core/config.ts";
18
+ import { readAdmissionConfig, resolveBinName } from "../core/config.ts";
19
19
  import {
20
20
  type AdmissionEntry,
21
21
  type AdmissionStatus,
@@ -24,7 +24,16 @@ import {
24
24
  admissionBaseDir,
25
25
  admissionStatus,
26
26
  listAdmissionResources,
27
+ waitForMemoryPressure,
27
28
  } from "../lib/admission.ts";
29
+ import {
30
+ type AdmissionHistoryGroup,
31
+ type AdmissionHistoryRecord,
32
+ type AdmissionReleaseResult,
33
+ readAdmissionHistory,
34
+ resolveAdmissionHistoryDir,
35
+ summarizeAdmissionHistory,
36
+ } from "../lib/admission-history.ts";
28
37
  import {
29
38
  classifyJob,
30
39
  createJobDir,
@@ -53,6 +62,7 @@ interface AdmissionRunOpts {
53
62
  capacity?: string;
54
63
  timeout?: string;
55
64
  label?: string;
65
+ projectRoot?: string;
56
66
  detach?: boolean;
57
67
  json?: boolean;
58
68
  }
@@ -67,6 +77,15 @@ interface AdmissionWaitOpts {
67
77
  json?: boolean;
68
78
  }
69
79
 
80
+ interface AdmissionHistoryOpts {
81
+ resource?: string;
82
+ since?: string;
83
+ label?: string;
84
+ list?: boolean;
85
+ limit?: string;
86
+ json?: boolean;
87
+ }
88
+
70
89
  interface AdmissionJobsOpts {
71
90
  json?: boolean;
72
91
  limit?: string;
@@ -86,6 +105,95 @@ export function jobsBaseDir(): string {
86
105
  return coordEnv("JOBS_DIR") ?? `${admissionBaseDir()}-jobs`;
87
106
  }
88
107
 
108
+ /**
109
+ * Resources this process tree already holds, set on every child that
110
+ * `admission run` starts. A nested run on one of them runs directly.
111
+ */
112
+ const HELD_ENV = "HARNERY_ADMISSION_HELD";
113
+
114
+ function heldResources(): string[] {
115
+ return (coordEnv("ADMISSION_HELD") ?? "")
116
+ .split(",")
117
+ .map((name) => name.trim())
118
+ .filter(Boolean);
119
+ }
120
+
121
+ /** Run a command with inherited stdio and report how it ended. Never throws. */
122
+ async function runChild(
123
+ emit: EmitContext,
124
+ commandArgs: string[],
125
+ env: NodeJS.ProcessEnv,
126
+ ): Promise<AdmissionReleaseResult> {
127
+ const [executable, ...childArgs] = commandArgs;
128
+ try {
129
+ const outcome = await new Promise<{ code: number | null; signal: NodeJS.Signals | null }>(
130
+ (resolvePromise, rejectPromise) => {
131
+ const child = spawn(executable as string, childArgs, {
132
+ stdio: "inherit",
133
+ shell: false,
134
+ env,
135
+ });
136
+ child.on("error", rejectPromise);
137
+ child.on("exit", (code, signal) => resolvePromise({ code, signal }));
138
+ },
139
+ );
140
+ if (outcome.signal !== null) {
141
+ emit.error({
142
+ code: "admission_child_signal",
143
+ message: `${executable} was terminated by signal ${outcome.signal}`,
144
+ });
145
+ }
146
+ return { exitCode: outcome.code, signal: outcome.signal };
147
+ } catch (err: unknown) {
148
+ emit.error({
149
+ code: "admission_spawn_error",
150
+ message: `cannot run ${executable}: ${describeError(err)}`,
151
+ });
152
+ return { exitCode: 1 };
153
+ }
154
+ }
155
+
156
+ /** Parse "30m", "24h", or "7d" into milliseconds; undefined means invalid. */
157
+ function parseLookback(raw: string): number | undefined {
158
+ const match = /^(\d+)([mhd])$/.exec(raw.trim());
159
+ if (!match) return undefined;
160
+ const amount = Number(match[1]);
161
+ const unitMs = match[2] === "m" ? 60_000 : match[2] === "h" ? 3_600_000 : 86_400_000;
162
+ return amount > 0 ? amount * unitMs : undefined;
163
+ }
164
+
165
+ function describeHistoryGroup(group: AdmissionHistoryGroup): string {
166
+ const outcomes = Object.entries(group.outcomes)
167
+ .filter(([outcome]) => outcome !== "released")
168
+ .map(([outcome, count]) => `${count} ${outcome}`);
169
+ const hold =
170
+ group.hold_ms !== null
171
+ ? `held p50 ${formatJobAge(group.hold_ms.p50)}, p90 ${formatJobAge(group.hold_ms.p90)}`
172
+ : "no completed holds";
173
+ return (
174
+ ` ${group.label || "(no label)"}: ${group.count} run(s), ${group.queued} queued; ` +
175
+ `waited p50 ${formatJobAge(group.wait_ms.p50)}, p90 ${formatJobAge(group.wait_ms.p90)}, ` +
176
+ `max ${formatJobAge(group.wait_ms.max)}, total ${formatJobAge(group.wait_ms.total)}; ${hold}` +
177
+ (outcomes.length > 0 ? `; ${outcomes.join(", ")}` : "")
178
+ );
179
+ }
180
+
181
+ function describeHistoryRecord(record: AdmissionHistoryRecord): string {
182
+ const bits = [
183
+ record.outcome,
184
+ `waited ${formatJobAge(record.wait_ms)}`,
185
+ ...(record.hold_ms !== undefined ? [`held ${formatJobAge(record.hold_ms)}`] : []),
186
+ ...(record.exit_code !== undefined && record.exit_code !== null
187
+ ? [`exit ${record.exit_code}`]
188
+ : []),
189
+ ...(record.signal ? [`signal ${record.signal}`] : []),
190
+ ];
191
+ const behind = record.waited_behind?.length
192
+ ? ` (behind: ${record.waited_behind.join(", ")})`
193
+ : "";
194
+ return ` ${record.ended_at} ${record.resource} ${record.label || "(no label)"}: ${bits.join(", ")}${behind}`;
195
+ }
196
+
89
197
  /** Parse an integer flag with a bounded range; undefined means invalid. */
90
198
  function parseBoundedInt(
91
199
  raw: string | undefined,
@@ -231,6 +339,11 @@ export function registerAdmissionCommand(program: Command, emit: EmitContext): v
231
339
  "--label <text>",
232
340
  "Holder description shown in status listings (default: the command itself).",
233
341
  )
342
+ .option(
343
+ "--project-root <dir>",
344
+ "Read admission settings from this project's .harnery/config.jsonc instead of " +
345
+ "the nearest one (for callers inside a nested checkout).",
346
+ )
234
347
  .option(
235
348
  "--detach",
236
349
  "Launch the command as a durable job: a detached supervisor holds the slot, " +
@@ -248,7 +361,8 @@ export function registerAdmissionCommand(program: Command, emit: EmitContext): v
248
361
  "launch failure. The job's own outcome comes from admission wait.",
249
362
  )
250
363
  .action(async (commandArgs: string[], opts: AdmissionRunOpts) => {
251
- const capacity = parseBoundedInt(opts.capacity, 1, 1, 32);
364
+ const resourceConfig = readAdmissionConfig(opts.projectRoot)?.resources[opts.resource];
365
+ const capacity = parseBoundedInt(opts.capacity, resourceConfig?.capacity ?? 1, 1, 32);
252
366
  if (capacity === undefined) {
253
367
  emit.error({
254
368
  code: "admission_usage",
@@ -257,7 +371,12 @@ export function registerAdmissionCommand(program: Command, emit: EmitContext): v
257
371
  process.exitCode = 1;
258
372
  return;
259
373
  }
260
- const timeoutMinutes = parseBoundedInt(opts.timeout, 60, 1, 1440);
374
+ const timeoutMinutes = parseBoundedInt(
375
+ opts.timeout,
376
+ resourceConfig?.timeout_minutes ?? 60,
377
+ 1,
378
+ 1440,
379
+ );
261
380
  if (timeoutMinutes === undefined) {
262
381
  emit.error({
263
382
  code: "admission_usage",
@@ -271,6 +390,16 @@ export function registerAdmissionCommand(program: Command, emit: EmitContext): v
271
390
  opts.label ??
272
391
  (joined.length > LABEL_MAX_CHARS ? `${joined.slice(0, LABEL_MAX_CHARS - 3)}...` : joined);
273
392
 
393
+ // A nested run on a resource this process tree already holds would wait
394
+ // on its own parent forever, and HARNERY_ADMISSION_SKIP=1 opts one
395
+ // command out on purpose. Both run the command directly.
396
+ const held = heldResources();
397
+ if (coordEnv("ADMISSION_SKIP") === "1" || held.includes(opts.resource)) {
398
+ const result = await runChild(emit, commandArgs, process.env);
399
+ process.exitCode = result.exitCode ?? 1;
400
+ return;
401
+ }
402
+
274
403
  if (opts.detach) {
275
404
  launchDetachedJob(emit, {
276
405
  commandArgs,
@@ -279,10 +408,29 @@ export function registerAdmissionCommand(program: Command, emit: EmitContext): v
279
408
  label,
280
409
  timeoutMinutes,
281
410
  json: opts.json === true,
411
+ ...(resourceConfig?.defer_on_pressure
412
+ ? { deferOnPressure: resourceConfig.defer_on_pressure }
413
+ : {}),
282
414
  });
283
415
  return;
284
416
  }
285
417
 
418
+ let pressureWaitMs = 0;
419
+ const pressure = resourceConfig?.defer_on_pressure;
420
+ if (pressure) {
421
+ pressureWaitMs = await waitForMemoryPressure({
422
+ threshold: pressure.psi_full_avg10,
423
+ maxWaitMs: pressure.max_wait_seconds * 1000,
424
+ onWait: (event) =>
425
+ emit.log(
426
+ event.gaveUp
427
+ ? `memory pressure still ${event.pressure} after ${formatJobAge(event.waitedMs)}; starting anyway: ${label}`
428
+ : `memory pressure ${event.pressure} > ${pressure.psi_full_avg10}; waiting before: ${label}`,
429
+ "info",
430
+ ),
431
+ });
432
+ }
433
+
286
434
  const dir = admissionBaseDir();
287
435
  let lastWaitMessage = "";
288
436
  let handle: Awaited<ReturnType<typeof acquireAdmission>>;
@@ -292,6 +440,7 @@ export function registerAdmissionCommand(program: Command, emit: EmitContext): v
292
440
  {
293
441
  label,
294
442
  timeoutMs: timeoutMinutes * 60_000,
443
+ ...(pressureWaitMs > 0 ? { pressureWaitMs } : {}),
295
444
  onWait: (info) => {
296
445
  const holders = info.holders.map((holder) => holder.label).join(", ") || "none";
297
446
  const message =
@@ -317,36 +466,103 @@ export function registerAdmissionCommand(program: Command, emit: EmitContext): v
317
466
  throw err;
318
467
  }
319
468
 
320
- const [executable, ...childArgs] = commandArgs;
469
+ const childEnv = {
470
+ ...process.env,
471
+ [HELD_ENV]: [...held, opts.resource].join(","),
472
+ };
473
+ let result: AdmissionReleaseResult = {};
321
474
  try {
322
- const outcome = await new Promise<{
323
- code: number | null;
324
- signal: NodeJS.Signals | null;
325
- }>((resolvePromise, rejectPromise) => {
326
- const child = spawn(executable as string, childArgs, {
327
- stdio: "inherit",
328
- shell: false,
329
- });
330
- child.on("error", rejectPromise);
331
- child.on("exit", (code, signal) => resolvePromise({ code, signal }));
475
+ result = await runChild(emit, commandArgs, childEnv);
476
+ process.exitCode = result.signal ? 1 : (result.exitCode ?? 1);
477
+ } finally {
478
+ handle.release(result);
479
+ }
480
+ });
481
+
482
+ // ----------------------------------------------------------------- history
483
+ admission
484
+ .command("history")
485
+ .description(
486
+ "Report finished slot use: how long work queued for each resource, what it " +
487
+ "waited behind, how long holders kept their slots, and which tickets timed " +
488
+ "out or were abandoned by a process that died. Grouped by resource and " +
489
+ "label, most total wait first; --list shows individual records.",
490
+ )
491
+ .option("--resource <name>", "Report one resource instead of all of them.")
492
+ .option("--since <window>", "Look back this far: 30m, 24h, 7d (default 24h).")
493
+ .option("--label <text>", "Keep records whose label contains this text.")
494
+ .option("--list", "List individual records, newest first, after the summary.")
495
+ .option("--limit <n>", "With --list, how many records to show (1-500; default 20).")
496
+ .option("--json", "Emit { history_dir, since, groups: [...], records: [...] } as JSON.")
497
+ .addHelpText(
498
+ "after",
499
+ "\nHistory is written to $XDG_STATE_HOME/harnery/admission-history (default " +
500
+ "~/.local/state/harnery/admission-history); HARNERY_ADMISSION_HISTORY_DIR " +
501
+ "moves it and HARNERY_ADMISSION_HISTORY=off stops recording." +
502
+ "\nExit codes: 0 always (reporting only) · 1 usage error.",
503
+ )
504
+ .action((opts: AdmissionHistoryOpts) => {
505
+ const lookbackMs = parseLookback(opts.since ?? "24h");
506
+ if (lookbackMs === undefined) {
507
+ emit.error({
508
+ code: "admission_usage",
509
+ message: "--since must look like 30m, 24h, or 7d",
332
510
  });
333
- if (outcome.signal !== null) {
334
- emit.error({
335
- code: "admission_child_signal",
336
- message: `${executable} was terminated by signal ${outcome.signal}`,
337
- });
338
- process.exitCode = 1;
339
- return;
340
- }
341
- process.exitCode = outcome.code ?? 1;
342
- } catch (err: unknown) {
511
+ process.exitCode = 1;
512
+ return;
513
+ }
514
+ const limit = parseBoundedInt(opts.limit, 20, 1, 500);
515
+ if (limit === undefined) {
343
516
  emit.error({
344
- code: "admission_spawn_error",
345
- message: `cannot run ${executable}: ${describeError(err)}`,
517
+ code: "admission_usage",
518
+ message: "--limit must be an integer between 1 and 500",
346
519
  });
347
520
  process.exitCode = 1;
348
- } finally {
349
- handle.release();
521
+ return;
522
+ }
523
+ const historyDir = resolveAdmissionHistoryDir(admissionBaseDir());
524
+ const sinceMs = Date.now() - lookbackMs;
525
+ const records = readAdmissionHistory(historyDir, {
526
+ ...(opts.resource !== undefined ? { resource: opts.resource } : {}),
527
+ ...(opts.label !== undefined ? { labelContains: opts.label } : {}),
528
+ sinceMs,
529
+ });
530
+ const groups = summarizeAdmissionHistory(records);
531
+ const newest = [...records].reverse().slice(0, limit);
532
+ if (opts.json) {
533
+ emit.data({
534
+ history_dir: historyDir,
535
+ since: new Date(sinceMs).toISOString(),
536
+ groups,
537
+ records: opts.list ? newest : [],
538
+ });
539
+ return;
540
+ }
541
+ if (historyDir === null) {
542
+ emit.log("admission history recording is off (HARNERY_ADMISSION_HISTORY=off)", "info");
543
+ return;
544
+ }
545
+ if (records.length === 0) {
546
+ emit.log(`no admission history since ${new Date(sinceMs).toISOString()}`, "info");
547
+ return;
548
+ }
549
+ let resource = "";
550
+ for (const group of groups.sort((a, b) => a.resource.localeCompare(b.resource))) {
551
+ if (group.resource !== resource) {
552
+ resource = group.resource;
553
+ const inResource = groups.filter((candidate) => candidate.resource === resource);
554
+ const total = inResource.reduce((sum, candidate) => sum + candidate.wait_ms.total, 0);
555
+ emit.log(
556
+ `${resource}: ${inResource.reduce((sum, candidate) => sum + candidate.count, 0)} ` +
557
+ `run(s), ${formatJobAge(total)} total queue wait`,
558
+ "info",
559
+ );
560
+ }
561
+ emit.log(describeHistoryGroup(group), "info");
562
+ }
563
+ if (opts.list) {
564
+ emit.log(`latest ${newest.length} record(s):`, "info");
565
+ for (const record of newest) emit.log(describeHistoryRecord(record), "info");
350
566
  }
351
567
  });
352
568
 
@@ -480,6 +696,7 @@ interface LaunchOptions {
480
696
  label: string;
481
697
  timeoutMinutes: number;
482
698
  json: boolean;
699
+ deferOnPressure?: { psi_full_avg10: number; max_wait_seconds: number };
483
700
  }
484
701
 
485
702
  function launchDetachedJob(emit: EmitContext, options: LaunchOptions): void {
@@ -507,6 +724,7 @@ function launchDetachedJob(emit: EmitContext, options: LaunchOptions): void {
507
724
  argv: options.commandArgs,
508
725
  cwd: process.cwd(),
509
726
  created_at: nowIso(),
727
+ ...(options.deferOnPressure ? { defer_on_pressure: options.deferOnPressure } : {}),
510
728
  };
511
729
  writeJobDocument(jobDir, document);
512
730
  logFd = openSync(join(jobDir, DURABLE_JOB_LOG_FILENAME), "a");
@@ -623,6 +841,25 @@ export async function superviseJob(emit: EmitContext, opts: AdmissionSuperviseOp
623
841
  });
624
842
  };
625
843
 
844
+ // Defer before queueing, holding no slot, exactly as a foreground run does.
845
+ let pressureWaitMs = 0;
846
+ const pressure = document.defer_on_pressure;
847
+ if (pressure) {
848
+ pressureWaitMs = await waitForMemoryPressure({
849
+ threshold: pressure.psi_full_avg10,
850
+ maxWaitMs: pressure.max_wait_seconds * 1000,
851
+ onWait: (event) => {
852
+ writeJobStatus(jobDir, { ...status, updated_at: nowIso() });
853
+ emit.log(
854
+ event.gaveUp
855
+ ? `memory pressure still ${event.pressure} after ${formatJobAge(event.waitedMs)}; queueing anyway`
856
+ : `memory pressure ${event.pressure} > ${pressure.psi_full_avg10}; waiting before queueing`,
857
+ "info",
858
+ );
859
+ },
860
+ });
861
+ }
862
+
626
863
  let handle: Awaited<ReturnType<typeof acquireAdmission>>;
627
864
  try {
628
865
  handle = await acquireAdmission(
@@ -634,6 +871,7 @@ export async function superviseJob(emit: EmitContext, opts: AdmissionSuperviseOp
634
871
  {
635
872
  label: document.label,
636
873
  timeoutMs: timeoutMinutes * 60_000,
874
+ ...(pressureWaitMs > 0 ? { pressureWaitMs } : {}),
637
875
  onWait: () => {
638
876
  writeJobStatus(jobDir, { ...status, updated_at: nowIso() });
639
877
  },
@@ -668,6 +906,7 @@ export async function superviseJob(emit: EmitContext, opts: AdmissionSuperviseOp
668
906
  heartbeat.unref?.();
669
907
 
670
908
  const [executable, ...childArgs] = document.argv;
909
+ const result: AdmissionReleaseResult = {};
671
910
  let logFd: number | undefined;
672
911
  try {
673
912
  logFd = openSync(join(jobDir, DURABLE_JOB_LOG_FILENAME), "a");
@@ -676,12 +915,15 @@ export async function superviseJob(emit: EmitContext, opts: AdmissionSuperviseOp
676
915
  const child = spawn(executable as string, childArgs, {
677
916
  stdio: ["ignore", logFd as number, logFd as number],
678
917
  shell: false,
918
+ env: { ...process.env, [HELD_ENV]: [...heldResources(), document.resource].join(",") },
679
919
  ...(document.cwd ? { cwd: document.cwd } : {}),
680
920
  });
681
921
  child.on("error", rejectPromise);
682
922
  child.on("exit", (code, signal) => resolvePromise({ code, signal }));
683
923
  },
684
924
  );
925
+ result.exitCode = outcome.code;
926
+ result.signal = outcome.signal;
685
927
  finish({ exitCode: outcome.code, signal: outcome.signal });
686
928
  process.exitCode = outcome.signal !== null ? 1 : (outcome.code ?? 1);
687
929
  } catch (err: unknown) {
@@ -690,10 +932,11 @@ export async function superviseJob(emit: EmitContext, opts: AdmissionSuperviseOp
690
932
  message: `cannot run ${executable}: ${describeError(err)}`,
691
933
  });
692
934
  finish({ exitCode: 1, signal: null });
935
+ result.exitCode = 1;
693
936
  process.exitCode = 1;
694
937
  } finally {
695
938
  clearInterval(heartbeat);
696
939
  if (logFd !== undefined) closeSync(logFd);
697
- handle.release();
940
+ handle.release(result);
698
941
  }
699
942
  }