harnery 0.41.0 → 0.42.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 (212) hide show
  1. package/dist/commander.js +3 -0
  2. package/dist/commands/agents.d.ts.map +1 -1
  3. package/dist/commands/agents.js +9 -1
  4. package/dist/commands/artifacts.d.ts.map +1 -1
  5. package/dist/commands/artifacts.js +113 -12
  6. package/dist/commands/browse.d.ts +164 -0
  7. package/dist/commands/browse.d.ts.map +1 -1
  8. package/dist/commands/browse.js +263 -51
  9. package/dist/commands/claude-desktop.d.ts.map +1 -1
  10. package/dist/commands/claude-desktop.js +52 -0
  11. package/dist/commands/disk.d.ts +6 -0
  12. package/dist/commands/disk.d.ts.map +1 -0
  13. package/dist/commands/disk.js +80 -0
  14. package/dist/commands/doctor.d.ts +8 -0
  15. package/dist/commands/doctor.d.ts.map +1 -1
  16. package/dist/commands/doctor.js +48 -1
  17. package/dist/commands/files.d.ts +16 -0
  18. package/dist/commands/files.d.ts.map +1 -1
  19. package/dist/commands/files.js +54 -2
  20. package/dist/commands/init.d.ts +2 -2
  21. package/dist/commands/init.d.ts.map +1 -1
  22. package/dist/commands/init.js +43 -9
  23. package/dist/commands/rm.d.ts +4 -0
  24. package/dist/commands/rm.d.ts.map +1 -0
  25. package/dist/commands/rm.js +87 -0
  26. package/dist/commands/servers.d.ts +7 -0
  27. package/dist/commands/servers.d.ts.map +1 -0
  28. package/dist/commands/servers.js +394 -0
  29. package/dist/commands/tunnel.d.ts.map +1 -1
  30. package/dist/commands/tunnel.js +82 -2
  31. package/dist/core/adapters/profiles.d.ts +9 -0
  32. package/dist/core/adapters/profiles.d.ts.map +1 -1
  33. package/dist/core/adapters/profiles.js +22 -1
  34. package/dist/core/adapters/types.d.ts +9 -0
  35. package/dist/core/adapters/types.d.ts.map +1 -1
  36. package/dist/core/agents/cli.js +13 -6
  37. package/dist/core/agents/render/session-context.d.ts.map +1 -1
  38. package/dist/core/agents/render/session-context.js +2 -0
  39. package/dist/core/agents/state/live-coordination-view.d.ts +20 -0
  40. package/dist/core/agents/state/live-coordination-view.d.ts.map +1 -1
  41. package/dist/core/agents/state/live-coordination-view.js +60 -0
  42. package/dist/core/artifacts/delivery-card.js +9 -2
  43. package/dist/core/artifacts/index.d.ts +140 -4
  44. package/dist/core/artifacts/index.d.ts.map +1 -1
  45. package/dist/core/artifacts/index.js +522 -49
  46. package/dist/core/config.d.ts +68 -11
  47. package/dist/core/config.d.ts.map +1 -1
  48. package/dist/core/config.js +128 -3
  49. package/dist/core/diagnostics/bundle.d.ts.map +1 -1
  50. package/dist/core/diagnostics/bundle.js +2 -2
  51. package/dist/core/events/v3/producers/recorder.d.ts.map +1 -1
  52. package/dist/core/events/v3/producers/recorder.js +16 -0
  53. package/dist/core/events/v3/projection.js +9 -1
  54. package/dist/core/hooks/adapter/events.d.ts +17 -0
  55. package/dist/core/hooks/adapter/events.d.ts.map +1 -1
  56. package/dist/core/hooks/adapter/events.js +5 -0
  57. package/dist/core/hooks/adapter/runtime-telemetry.js +3 -1
  58. package/dist/core/hooks/adapter/wiring.d.ts +37 -7
  59. package/dist/core/hooks/adapter/wiring.d.ts.map +1 -1
  60. package/dist/core/hooks/adapter/wiring.js +62 -4
  61. package/dist/core/hooks/cli.js +45 -6
  62. package/dist/core/hooks/run-markers.d.ts +37 -0
  63. package/dist/core/hooks/run-markers.d.ts.map +1 -0
  64. package/dist/core/hooks/run-markers.js +109 -0
  65. package/dist/core/servers/index.d.ts +211 -0
  66. package/dist/core/servers/index.d.ts.map +1 -0
  67. package/dist/core/servers/index.js +560 -0
  68. package/dist/core/servers/net.d.ts +58 -0
  69. package/dist/core/servers/net.d.ts.map +1 -0
  70. package/dist/core/servers/net.js +270 -0
  71. package/dist/core/servers/tunnels.d.ts +14 -0
  72. package/dist/core/servers/tunnels.d.ts.map +1 -0
  73. package/dist/core/servers/tunnels.js +33 -0
  74. package/dist/core/storage/builtins.js +1 -0
  75. package/dist/core/workflow/engine.d.ts.map +1 -1
  76. package/dist/core/workflow/engine.js +30 -1
  77. package/dist/core/workflow/sandbox-projection.d.ts +13 -2
  78. package/dist/core/workflow/sandbox-projection.d.ts.map +1 -1
  79. package/dist/core/workflow/sandbox-projection.js +23 -0
  80. package/dist/core/workflow/spawn-claude.d.ts.map +1 -1
  81. package/dist/core/workflow/spawn-claude.js +6 -1
  82. package/dist/core/workflow/spawn-codex.d.ts +2 -0
  83. package/dist/core/workflow/spawn-codex.d.ts.map +1 -1
  84. package/dist/core/workflow/spawn-codex.js +20 -1
  85. package/dist/core/workflow/spawn-cursor.d.ts.map +1 -1
  86. package/dist/core/workflow/spawn-cursor.js +6 -1
  87. package/dist/core/workflow/spawn-opencode.d.ts.map +1 -1
  88. package/dist/core/workflow/spawn-opencode.js +4 -1
  89. package/dist/core/workflow/types.d.ts +47 -2
  90. package/dist/core/workflow/types.d.ts.map +1 -1
  91. package/dist/core/workflow/worker-access.d.ts +92 -0
  92. package/dist/core/workflow/worker-access.d.ts.map +1 -0
  93. package/dist/core/workflow/worker-access.js +450 -0
  94. package/dist/lib/browser/batch-steps.d.ts +18 -0
  95. package/dist/lib/browser/batch-steps.d.ts.map +1 -0
  96. package/dist/lib/browser/batch-steps.js +82 -0
  97. package/dist/lib/browser/collect/collector.d.ts +99 -0
  98. package/dist/lib/browser/collect/collector.d.ts.map +1 -0
  99. package/dist/lib/browser/collect/collector.js +202 -0
  100. package/dist/lib/browser/collect/fields.d.ts +31 -0
  101. package/dist/lib/browser/collect/fields.d.ts.map +1 -0
  102. package/dist/lib/browser/collect/fields.js +75 -0
  103. package/dist/lib/browser/collect/index.d.ts +6 -0
  104. package/dist/lib/browser/collect/index.d.ts.map +1 -0
  105. package/dist/lib/browser/collect/index.js +5 -0
  106. package/dist/lib/browser/collect/page-driver.d.ts +14 -0
  107. package/dist/lib/browser/collect/page-driver.d.ts.map +1 -0
  108. package/dist/lib/browser/collect/page-driver.js +306 -0
  109. package/dist/lib/browser/collect/presets.d.ts +52 -0
  110. package/dist/lib/browser/collect/presets.d.ts.map +1 -0
  111. package/dist/lib/browser/collect/presets.js +176 -0
  112. package/dist/lib/browser/collect/scroll-plan.d.ts +85 -0
  113. package/dist/lib/browser/collect/scroll-plan.d.ts.map +1 -0
  114. package/dist/lib/browser/collect/scroll-plan.js +118 -0
  115. package/dist/lib/browser/index.d.ts +1 -1
  116. package/dist/lib/browser/index.d.ts.map +1 -1
  117. package/dist/lib/browser/page-review-judge.d.ts +2 -0
  118. package/dist/lib/browser/page-review-judge.d.ts.map +1 -1
  119. package/dist/lib/browser/page-review-judge.js +13 -0
  120. package/dist/lib/browser/qa-run-contracts.d.ts +10 -0
  121. package/dist/lib/browser/qa-run-contracts.d.ts.map +1 -1
  122. package/dist/lib/browser/qa-run.d.ts.map +1 -1
  123. package/dist/lib/browser/qa-run.js +1 -0
  124. package/dist/lib/browser/windows-chrome.d.ts +59 -0
  125. package/dist/lib/browser/windows-chrome.d.ts.map +1 -0
  126. package/dist/lib/browser/windows-chrome.js +179 -0
  127. package/dist/lib/claude-desktop-share.d.ts +136 -0
  128. package/dist/lib/claude-desktop-share.d.ts.map +1 -0
  129. package/dist/lib/claude-desktop-share.js +394 -0
  130. package/dist/lib/disk-usage.d.ts +50 -0
  131. package/dist/lib/disk-usage.d.ts.map +1 -0
  132. package/dist/lib/disk-usage.js +303 -0
  133. package/dist/lib/docs-lint.d.ts +6 -0
  134. package/dist/lib/docs-lint.d.ts.map +1 -1
  135. package/dist/lib/docs-lint.js +8 -3
  136. package/dist/lib/guarded-remove.d.ts +34 -0
  137. package/dist/lib/guarded-remove.d.ts.map +1 -0
  138. package/dist/lib/guarded-remove.js +310 -0
  139. package/dist/lib/instructions/templates.js +8 -8
  140. package/dist/lib/tunnel/error-page.d.ts +1 -1
  141. package/dist/lib/tunnel/error-page.d.ts.map +1 -1
  142. package/dist/lib/tunnel/error-page.js +19 -8
  143. package/dist/lib/tunnel/gate.js +20 -1
  144. package/dist/lib/tunnel/path-scope.d.ts +22 -0
  145. package/dist/lib/tunnel/path-scope.d.ts.map +1 -0
  146. package/dist/lib/tunnel/path-scope.js +62 -0
  147. package/dist/lib/tunnel/state.d.ts +5 -0
  148. package/dist/lib/tunnel/state.d.ts.map +1 -1
  149. package/dist/lib/tunnel/state.js +17 -3
  150. package/package.json +6 -1
  151. package/schemas/config.schema.json +91 -0
  152. package/src/commander.ts +19 -0
  153. package/src/commands/agents.ts +14 -0
  154. package/src/commands/artifacts.ts +171 -19
  155. package/src/commands/browse.ts +400 -57
  156. package/src/commands/claude-desktop.ts +61 -0
  157. package/src/commands/disk.ts +114 -0
  158. package/src/commands/doctor.ts +52 -1
  159. package/src/commands/files.ts +93 -2
  160. package/src/commands/init.ts +55 -5
  161. package/src/commands/rm.ts +115 -0
  162. package/src/commands/servers.ts +506 -0
  163. package/src/commands/tunnel.ts +102 -2
  164. package/src/core/adapters/profiles.ts +22 -1
  165. package/src/core/adapters/types.ts +10 -0
  166. package/src/core/agents/cli.ts +14 -6
  167. package/src/core/agents/render/session-context.ts +1 -0
  168. package/src/core/agents/state/live-coordination-view.ts +73 -0
  169. package/src/core/artifacts/delivery-card.ts +10 -2
  170. package/src/core/artifacts/index.ts +697 -44
  171. package/src/core/config.ts +183 -4
  172. package/src/core/diagnostics/bundle.ts +2 -1
  173. package/src/core/events/v3/producers/recorder.ts +16 -0
  174. package/src/core/events/v3/projection.ts +11 -1
  175. package/src/core/hooks/adapter/events.ts +19 -0
  176. package/src/core/hooks/adapter/runtime-telemetry.ts +3 -1
  177. package/src/core/hooks/adapter/wiring.ts +116 -8
  178. package/src/core/hooks/cli.ts +47 -6
  179. package/src/core/hooks/run-markers.ts +132 -0
  180. package/src/core/servers/index.ts +712 -0
  181. package/src/core/servers/net.ts +283 -0
  182. package/src/core/servers/tunnels.ts +39 -0
  183. package/src/core/storage/builtins.ts +1 -0
  184. package/src/core/workflow/engine.ts +38 -1
  185. package/src/core/workflow/sandbox-projection.ts +38 -2
  186. package/src/core/workflow/spawn-claude.ts +5 -1
  187. package/src/core/workflow/spawn-codex.ts +18 -1
  188. package/src/core/workflow/spawn-cursor.ts +5 -1
  189. package/src/core/workflow/spawn-opencode.ts +4 -1
  190. package/src/core/workflow/types.ts +62 -2
  191. package/src/core/workflow/worker-access.ts +525 -0
  192. package/src/lib/browser/batch-steps.ts +79 -0
  193. package/src/lib/browser/collect/collector.ts +306 -0
  194. package/src/lib/browser/collect/fields.ts +87 -0
  195. package/src/lib/browser/collect/index.ts +47 -0
  196. package/src/lib/browser/collect/page-driver.ts +327 -0
  197. package/src/lib/browser/collect/presets.ts +237 -0
  198. package/src/lib/browser/collect/scroll-plan.ts +179 -0
  199. package/src/lib/browser/index.ts +1 -0
  200. package/src/lib/browser/page-review-judge.ts +16 -0
  201. package/src/lib/browser/qa-run-contracts.ts +11 -0
  202. package/src/lib/browser/qa-run.ts +1 -0
  203. package/src/lib/browser/windows-chrome.ts +227 -0
  204. package/src/lib/claude-desktop-share.ts +511 -0
  205. package/src/lib/disk-usage.ts +347 -0
  206. package/src/lib/docs-lint.ts +8 -3
  207. package/src/lib/guarded-remove.ts +376 -0
  208. package/src/lib/instructions/templates.ts +8 -8
  209. package/src/lib/tunnel/error-page.ts +25 -9
  210. package/src/lib/tunnel/gate.ts +22 -1
  211. package/src/lib/tunnel/path-scope.ts +73 -0
  212. package/src/lib/tunnel/state.ts +20 -3
@@ -80,6 +80,8 @@ interface HarneryConfig {
80
80
  /** Optional host prompt-context extension. Project config only. */
81
81
  hooks?: {
82
82
  promptContext?: unknown;
83
+ /** Project-only opt-in; read via `codexWindowsBridgeConfig()`. */
84
+ codexWindowsBridge?: unknown;
83
85
  };
84
86
  /**
85
87
  * Agent-ritual policy owned by the host project. Git finalization is opt-in:
@@ -107,7 +109,7 @@ interface HarneryConfig {
107
109
  * `workflow run` in this repo to subscription billing (API-key vars are
108
110
  * scrubbed from child envs) without anyone having to remember the flag.
109
111
  */
110
- workflow?: { subscriptionOnly?: boolean };
112
+ workflow?: { subscriptionOnly?: boolean; workerFullAccess?: unknown };
111
113
  /**
112
114
  * Cross-machine presence (ADR 0016). `{ enabled: false }` opts a repo out of
113
115
  * the git-refs transport (publishing `refs/harnery/presence/<machine>` to
@@ -166,11 +168,22 @@ interface HarneryConfig {
166
168
  * Managed working-artifact defaults. `default_retention_days` is the
167
169
  * create-time TTL when the caller does not pass `artifacts create --days`.
168
170
  */
171
+ /** Local server registry (`servers` command, `harnery/core/servers`). */
172
+ servers?: {
173
+ /** Inclusive port range per server type, e.g. `"preview": [3100, 3199]`. */
174
+ port_ranges?: Record<string, unknown>;
175
+ idle_hours?: number;
176
+ owner_stale_hours?: number;
177
+ };
169
178
  artifacts?: {
170
179
  default_retention_days?: number;
171
180
  auto_clean?: boolean;
172
181
  max_bytes?: number;
173
182
  max_unit_bytes?: number;
183
+ idle_grace_hours?: number;
184
+ hold_days?: number;
185
+ max_held_bytes?: number;
186
+ min_free_bytes?: number;
174
187
  };
175
188
  /**
176
189
  * Page review packs. `auto_clean` lets `qa-run` and `review-pack create`
@@ -494,6 +507,42 @@ export function resolveHooksSetupHint(coordRoot?: string | null): string | null
494
507
  * values fail closed to no reminder so a malformed optional setting cannot
495
508
  * break the prompt hook.
496
509
  */
510
+ /** A plain Windows command name, so it is safe to splice into a PowerShell hook command. */
511
+ const CODEX_WINDOWS_BRIDGE_ENTRY_POINT = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/;
512
+
513
+ export interface CodexWindowsBridgeConfig {
514
+ /** Command name Windows-native Codex hooks hand off to, or null when not opted in. */
515
+ entryPoint: string | null;
516
+ /** Why the configured value was ignored, when it was. */
517
+ error: string | null;
518
+ }
519
+
520
+ /**
521
+ * Per-project opt-in that routes Windows-native Codex hooks through a resident
522
+ * WSL bridge (ADR 0197). Read from the project config only: the value is
523
+ * written into the committed `.codex/hooks.json`, so a user-level setting
524
+ * would make `init` output differ between machines. An invalid value is
525
+ * ignored and reported rather than spliced into a shell command.
526
+ */
527
+ export function codexWindowsBridgeConfig(projectRoot: string): CodexWindowsBridgeConfig {
528
+ const raw = readProjectConfig(projectRoot).hooks?.codexWindowsBridge;
529
+ if (raw === undefined) return { entryPoint: null, error: null };
530
+ const entryPoint = isPlainObject(raw) ? raw.entryPoint : undefined;
531
+ if (typeof entryPoint !== "string") {
532
+ return {
533
+ entryPoint: null,
534
+ error: "hooks.codexWindowsBridge.entryPoint must be a command name",
535
+ };
536
+ }
537
+ if (!CODEX_WINDOWS_BRIDGE_ENTRY_POINT.test(entryPoint)) {
538
+ return {
539
+ entryPoint: null,
540
+ error: `hooks.codexWindowsBridge.entryPoint ${JSON.stringify(entryPoint)} is not a plain command name`,
541
+ };
542
+ }
543
+ return { entryPoint, error: null };
544
+ }
545
+
497
546
  export function hostPromptReminder(coordRoot?: string | null): string | null {
498
547
  const root = coordRoot ?? findCoordRoot();
499
548
  if (!root) return null;
@@ -601,6 +650,27 @@ export function workflowSubscriptionOnly(coordRoot?: string | null): boolean {
601
650
  return readConfig(root).workflow?.subscriptionOnly === true;
602
651
  }
603
652
 
653
+ /**
654
+ * The host's raw worker full-access policy (ADR 0192), validated by
655
+ * `parseWorkerFullAccessPolicy` in the workflow layer, which knows each
656
+ * adapter's effort scale.
657
+ *
658
+ * `.harnery/config.jsonc`:
659
+ * `{ "workflow": { "workerFullAccess": { "enabled": true, "floors": { ... } } } }`
660
+ *
661
+ * This is a trust boundary, so it comes only from the project config, like
662
+ * `agents.finalizationRoots`: a user-global file cannot remove the sandbox from
663
+ * one project's workers. `HARNERY_WORKFLOW_WORKER_FULL_ACCESS=0` turns it off
664
+ * for one process. There is deliberately no `=1`: an environment variable can
665
+ * withdraw the grant but never supply it.
666
+ */
667
+ export function workflowWorkerFullAccessConfig(coordRoot?: string | null): unknown {
668
+ if (coordEnv("WORKFLOW_WORKER_FULL_ACCESS") === "0") return { enabled: false };
669
+ const root = coordRoot ?? findCoordRoot();
670
+ if (!root) return undefined;
671
+ return readProjectConfig(root).workflow?.workerFullAccess;
672
+ }
673
+
604
674
  /**
605
675
  * How this project's `.harnery/` state is shared: `private` or `group`. `HARNERY_STORAGE_SHARING`
606
676
  * overrides per process; an unrecognized value falls back to `private`, the safer reading.
@@ -765,6 +835,46 @@ export function resolveWebPort(explicitPort?: string, coordRoot?: string | null)
765
835
  return DEFAULT_WEB_PORT;
766
836
  }
767
837
 
838
+ /** The configured inclusive port range for one server type, or null. */
839
+ export function serverPortRange(type: string, coordRoot?: string | null): [number, number] | null {
840
+ const root = coordRoot ?? findCoordRoot();
841
+ const value = root ? readConfig(root).servers?.port_ranges?.[type] : undefined;
842
+ if (!Array.isArray(value) || value.length !== 2) return null;
843
+ const [low, high] = value as unknown[];
844
+ const valid = (n: unknown): n is number =>
845
+ Number.isInteger(n) && (n as number) > 0 && (n as number) < 65536;
846
+ return valid(low) && valid(high) && low <= high ? [low, high] : null;
847
+ }
848
+
849
+ /** Every configured server port range, keyed by type (invalid entries dropped). */
850
+ export function serverPortRanges(coordRoot?: string | null): Record<string, [number, number]> {
851
+ const root = coordRoot ?? findCoordRoot();
852
+ const ranges = root ? readConfig(root).servers?.port_ranges : undefined;
853
+ const out: Record<string, [number, number]> = {};
854
+ for (const type of Object.keys(ranges ?? {})) {
855
+ const range = serverPortRange(type, root);
856
+ if (range) out[type] = range;
857
+ }
858
+ return out;
859
+ }
860
+
861
+ /** Configured `servers gc` windows; undefined fields use the built-in defaults. */
862
+ export function serverGcConfig(coordRoot?: string | null): {
863
+ idle_hours?: number;
864
+ owner_stale_hours?: number;
865
+ } {
866
+ const root = coordRoot ?? findCoordRoot();
867
+ const configured = root ? readConfig(root).servers : undefined;
868
+ const positive = (n: unknown): n is number =>
869
+ typeof n === "number" && Number.isFinite(n) && n > 0;
870
+ return {
871
+ ...(positive(configured?.idle_hours) ? { idle_hours: configured.idle_hours } : {}),
872
+ ...(positive(configured?.owner_stale_hours)
873
+ ? { owner_stale_hours: configured.owner_stale_hours }
874
+ : {}),
875
+ };
876
+ }
877
+
768
878
  /** Policy for converging independent termination signals on one finalizer. */
769
879
  export function sessionFinalizationConfig(coordRoot?: string | null): SessionFinalizationConfig {
770
880
  const root = coordRoot ?? findCoordRoot();
@@ -843,12 +953,81 @@ export function artifactMaxUnitBytes(coordRoot?: string | null): number {
843
953
  );
844
954
  }
845
955
 
956
+ /**
957
+ * How long an unexpired workspace must sit idle before a size rule (the
958
+ * per-bundle ceiling or the repository budget) may delete it. Idle means no
959
+ * change inside the workspace and no heartbeat from its owner. An agent waiting
960
+ * on a human reply looks idle, so a short window deletes work someone is still
961
+ * reviewing. Precedence: `HARNERY_ARTIFACT_IDLE_GRACE_HOURS` ->
962
+ * `artifacts.idle_grace_hours` -> 24. Expiry never waits for this grace.
963
+ */
964
+ export function artifactIdleGraceHours(coordRoot?: string | null): number {
965
+ const root = coordRoot ?? findCoordRoot();
966
+ return integerSetting(
967
+ coordEnv("ARTIFACT_IDLE_GRACE_HOURS"),
968
+ root ? readConfig(root).artifacts?.idle_grace_hours : undefined,
969
+ 1,
970
+ 24 * 365,
971
+ 24,
972
+ );
973
+ }
974
+
975
+ /**
976
+ * Default lifetime of an artifact hold. A hold that is not renewed lapses after
977
+ * this many days and the workspace returns to ordinary retention. Precedence:
978
+ * `HARNERY_ARTIFACT_HOLD_DAYS` -> `artifacts.hold_days` -> 14.
979
+ */
980
+ export function artifactHoldDays(coordRoot?: string | null): number {
981
+ const root = coordRoot ?? findCoordRoot();
982
+ return integerSetting(
983
+ coordEnv("ARTIFACT_HOLD_DAYS"),
984
+ root ? readConfig(root).artifacts?.hold_days : undefined,
985
+ 1,
986
+ 365,
987
+ 14,
988
+ );
989
+ }
990
+
991
+ /**
992
+ * Advisory budget for held artifact bytes. Exceeding it produces warnings
993
+ * only: holds record owner intent, so cleanup never deletes held work.
994
+ * Precedence: `HARNERY_ARTIFACT_MAX_HELD_BYTES` -> `artifacts.max_held_bytes`
995
+ * -> 20 GiB.
996
+ */
997
+ export function artifactMaxHeldBytes(coordRoot?: string | null): number {
998
+ const root = coordRoot ?? findCoordRoot();
999
+ return integerSetting(
1000
+ coordEnv("ARTIFACT_MAX_HELD_BYTES"),
1001
+ root ? readConfig(root).artifacts?.max_held_bytes : undefined,
1002
+ 64 * 1024 * 1024,
1003
+ 1024 * 1024 * 1024 * 1024,
1004
+ DEFAULT_ARTIFACT_MAX_BYTES,
1005
+ );
1006
+ }
1007
+
1008
+ /**
1009
+ * Free-disk floor for the artifact store. Below it, artifact commands warn and
1010
+ * `artifacts create --big` refuses unless `--allow-low-disk` is passed.
1011
+ * Precedence: `HARNERY_ARTIFACT_MIN_FREE_BYTES` -> `artifacts.min_free_bytes`
1012
+ * -> 20 GiB. Zero disables the floor.
1013
+ */
1014
+ export function artifactMinFreeBytes(coordRoot?: string | null): number {
1015
+ const root = coordRoot ?? findCoordRoot();
1016
+ return integerSetting(
1017
+ coordEnv("ARTIFACT_MIN_FREE_BYTES"),
1018
+ root ? readConfig(root).artifacts?.min_free_bytes : undefined,
1019
+ 0,
1020
+ 1024 * 1024 * 1024 * 1024 * 1024,
1021
+ 20 * 1024 * 1024 * 1024,
1022
+ );
1023
+ }
1024
+
846
1025
  /**
847
1026
  * Whether opportunistic cleanup of expired artifact workspaces runs.
848
1027
  * Precedence: `HARNERY_ARTIFACT_AUTO_CLEAN` (0/false disables) ->
849
- * `artifacts.auto_clean` -> enabled. The sweep only ever deletes
850
- * `managed-expired` entries via the same guarded classifier as
851
- * `artifacts clean --yes`.
1028
+ * `artifacts.auto_clean` -> enabled. The sweep deletes the same
1029
+ * `managed-expired`, `managed-oversize`, and `managed-over-budget` entries as
1030
+ * `artifacts clean --yes`, through the same guarded classifier.
852
1031
  */
853
1032
  export function artifactAutoCleanEnabled(coordRoot?: string | null): boolean {
854
1033
  const env = coordEnv("ARTIFACT_AUTO_CLEAN");
@@ -15,6 +15,7 @@ import type {
15
15
  } from "../artifacts/index.ts";
16
16
  import {
17
17
  ARTIFACT_MANIFEST,
18
+ artifactHoldState,
18
19
  artifactsRoot,
19
20
  configuredArtifactRetentionDays,
20
21
  createArtifact,
@@ -412,7 +413,7 @@ function candidateArtifactId(artifactPath: string): string | undefined {
412
413
  function candidateClassification(
413
414
  manifest: ArtifactManifestV2,
414
415
  ): ArtifactInventoryEntry["classification"] {
415
- if (manifest.holds.length > 0) return "managed-held";
416
+ if (artifactHoldState(manifest).active.length > 0) return "managed-held";
416
417
  if (!manifest.released_at) return "managed-active";
417
418
  return Date.parse(manifest.retention.expires_at) <= Date.now()
418
419
  ? "managed-expired"
@@ -1130,6 +1130,22 @@ function processHookSignalLocked(
1130
1130
  if (input.signal === "sub-agent-start") {
1131
1131
  if (!sourceId) return { state: "ignored" };
1132
1132
  delegation = state.delegations.find((candidate) => candidate.source_id === sourceId);
1133
+ if (delegation?.open_event_id) {
1134
+ // SendMessage to a running background agent re-fires SubagentStart.
1135
+ // The delegation is already open; a second agent.started is noise.
1136
+ writeProducerDiagnosticV3(input.coordRoot, "duplicate_subagent_start_suppressed", {
1137
+ adapter: input.adapter,
1138
+ signal: input.signal,
1139
+ reason: "delegation_already_open",
1140
+ instance_id: input.instance_id,
1141
+ session_hash: sessionHash,
1142
+ generation_id: state.generation_id,
1143
+ delegation_id: delegation.delegation_id,
1144
+ payload: input.payload,
1145
+ });
1146
+ publishProducerState(input.coordRoot, path, state);
1147
+ return { state: "ignored" };
1148
+ }
1133
1149
  if (!delegation) {
1134
1150
  const opened = openSpanStateV3({
1135
1151
  span_id: spanIdV3(),
@@ -539,7 +539,17 @@ function applyAgentStarted(
539
539
  projection: SafetyProjectionV3,
540
540
  event: Extract<EventV3, { event_type: "agent.started" }>,
541
541
  ): void {
542
- if (projection.delegations[event.payload.delegation_id]) {
542
+ const existing = projection.delegations[event.payload.delegation_id];
543
+ if (existing) {
544
+ // Claude Code re-fires SubagentStart when SendMessage resumes a background
545
+ // agent. A restart naming the same parent and child is that resume, not a
546
+ // conflicting delegation, and must not strip the parent's authority.
547
+ if (
548
+ existing.parent_generation_id === generationIdOf(event) &&
549
+ existing.child_generation_id === event.payload.child_generation_id
550
+ ) {
551
+ return;
552
+ }
543
553
  addDiagnostic(projection, {
544
554
  code: "delegation_duplicate_start",
545
555
  event_id: event.event_id,
@@ -69,6 +69,20 @@ export interface AdapterSpec {
69
69
  * wandered (the session shell `cd`ing into a subdirectory or off-repo).
70
70
  */
71
71
  projectDirEnv?: string;
72
+ /**
73
+ * Per-handler `timeout` (seconds) written on every Harnery hook entry. Without
74
+ * one, an adapter's own default applies (Codex: 600 seconds), so a stalled
75
+ * hook can hold a tool call for ten minutes. `bySubcommand` overrides
76
+ * `default` for one event.
77
+ */
78
+ hookTimeouts?: { default: number; bySubcommand?: Record<string, number> };
79
+ /**
80
+ * The adapter runs a handler's `commandWindows` instead of `command` on
81
+ * Windows. A project that opts in (`hooks.codexWindowsBridge`) gets one that
82
+ * hands the hook to a resident WSL bridge (ADR 0197); otherwise init leaves
83
+ * the field off.
84
+ */
85
+ windowsBridgeHooks?: boolean;
72
86
  }
73
87
 
74
88
  /** Claude Code: `.claude/settings.json`. */
@@ -191,6 +205,11 @@ export const ADAPTER_SPECS: Record<AdapterId, AdapterSpec> = {
191
205
  legacyEvents: LEGACY_CODEX_EVENTS,
192
206
  allowedTopLevelKeys: ["description", "hooks"],
193
207
  allowedEventKeys: CODEX_ALLOWED_EVENT_KEYS,
208
+ // A Codex hook that times out fails open: the run is marked failed and
209
+ // the tool proceeds. Codex clamps SessionEnd to 3 seconds and defaults it
210
+ // to 1, which is too short for a cold hook start.
211
+ hookTimeouts: { default: 20, bySubcommand: { "session-end": 3 } },
212
+ windowsBridgeHooks: true,
194
213
  },
195
214
  opencode: {
196
215
  settingsFile: ".opencode/plugins/harnery/index.ts",
@@ -671,7 +671,9 @@ function claudeModelContextCapability(model: string): ClaudeModelContextCapabili
671
671
  const normalized = model.toLowerCase();
672
672
  const oneMillionTokenModels = new Set([
673
673
  "claude-fable-5",
674
+ "claude-opus-5-5",
674
675
  "claude-opus-5",
676
+ "claude-sonnet-5-5",
675
677
  "claude-sonnet-5",
676
678
  "claude-opus-4-8",
677
679
  "claude-opus-4-7",
@@ -679,7 +681,7 @@ function claudeModelContextCapability(model: string): ClaudeModelContextCapabili
679
681
  "claude-sonnet-4-6",
680
682
  ]);
681
683
  if (oneMillionTokenModels.has(normalized)) {
682
- return { limit_tokens: 1_000_000, authority: "anthropic_model_capabilities_2026_08" };
684
+ return { limit_tokens: 1_000_000, authority: "anthropic_model_capabilities_2026_09" };
683
685
  }
684
686
  if (normalized === "claude-haiku-4-5" || normalized === "claude-haiku-4-5-20251001") {
685
687
  return { limit_tokens: 200_000, authority: "anthropic_model_capabilities_2026_08" };
@@ -14,7 +14,7 @@
14
14
  import { existsSync, readFileSync } from "node:fs";
15
15
  import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
16
16
  import { fileURLToPath } from "node:url";
17
- import { resolveBinName } from "../../config.ts";
17
+ import { codexWindowsBridgeConfig, resolveBinName } from "../../config.ts";
18
18
  import {
19
19
  ADAPTER_SPECS,
20
20
  type AdapterId,
@@ -24,10 +24,19 @@ import {
24
24
  } from "./events.ts";
25
25
  import { checkOpenCodePlugin, isOwnedOpenCodePlugin } from "./opencode-plugin.ts";
26
26
 
27
- /** Claude Code + Codex entry: `{ hooks: [{ type, command }] }`. */
27
+ /** One Claude Code or Codex command handler. */
28
+ export interface ClaudeHookHandler {
29
+ type: string;
30
+ command: string;
31
+ /** Codex: run instead of `command` on Windows. */
32
+ commandWindows?: string;
33
+ timeout?: number;
34
+ }
35
+
36
+ /** Claude Code + Codex entry: `{ hooks: [{ type, command, commandWindows?, timeout? }] }`. */
28
37
  export interface ClaudeHookGroup {
29
38
  matcher?: string;
30
- hooks: { type: string; command: string }[];
39
+ hooks: ClaudeHookHandler[];
31
40
  }
32
41
  /** Cursor entry: a flat `{ command }`. */
33
42
  export interface CursorHookGroup {
@@ -53,14 +62,92 @@ export interface SettingsFile {
53
62
  export function makeEntry(
54
63
  shape: HookEntryShape,
55
64
  command: string,
56
- extras?: { loop_limit?: number },
65
+ extras?: { loop_limit?: number; timeout?: number; commandWindows?: string },
57
66
  ): HookGroup {
58
- if (shape !== "cursor") return { hooks: [{ type: "command", command }] };
67
+ if (shape !== "cursor") {
68
+ return {
69
+ hooks: [
70
+ orderHandlerKeys({
71
+ type: "command",
72
+ command,
73
+ commandWindows: extras?.commandWindows,
74
+ timeout: extras?.timeout,
75
+ }),
76
+ ],
77
+ };
78
+ }
59
79
  return extras?.loop_limit !== undefined
60
80
  ? { command, loop_limit: extras.loop_limit }
61
81
  : { command };
62
82
  }
63
83
 
84
+ /**
85
+ * Rebuild a handler with Harnery's fields first in a fixed order, dropping
86
+ * undefined ones and keeping any other fields after them.
87
+ */
88
+ export function orderHandlerKeys(handler: ClaudeHookHandler): ClaudeHookHandler {
89
+ const { type, command, commandWindows, timeout, ...rest } = handler;
90
+ return {
91
+ type,
92
+ command,
93
+ ...(commandWindows !== undefined ? { commandWindows } : {}),
94
+ ...(timeout !== undefined ? { timeout } : {}),
95
+ ...rest,
96
+ };
97
+ }
98
+
99
+ /**
100
+ * The Windows command for an opted-in project (ADR 0197). It hands the hook to
101
+ * the bridge entry point when that is on PATH and the working directory is a
102
+ * WSL UNC path, and otherwise runs the usual command. `ProviderPath` drops the
103
+ * provider prefix that `Path` carries. The trailing `exit $LASTEXITCODE` is
104
+ * required: without it PowerShell reports any non-zero exit from inside the
105
+ * `if` block as 1, which would turn a hook's blocking exit 2 into a failure.
106
+ */
107
+ export function hookCommandWindows(
108
+ spec: AdapterSpec,
109
+ agentHookPath: string,
110
+ subcommand: string,
111
+ adapter: AdapterId,
112
+ entryPoint: string,
113
+ ): string {
114
+ const direct = hookCommand(spec, agentHookPath, subcommand, adapter);
115
+ return (
116
+ `if ((Get-Command ${entryPoint} -ErrorAction Ignore) -and $PWD.ProviderPath -like '\\\\wsl*') ` +
117
+ `{ ${entryPoint} ${subcommand} --adapter ${adapter} } else { ${direct} }; exit $LASTEXITCODE`
118
+ );
119
+ }
120
+
121
+ /** Project-level choices that change what init writes. */
122
+ export interface WiringOptions {
123
+ /** Bridge entry point from `hooks.codexWindowsBridge`, or null when not opted in. */
124
+ windowsBridgeEntryPoint?: string | null;
125
+ }
126
+
127
+ /** The `commandWindows` init writes for one event, or undefined for none. */
128
+ export function expectedCommandWindows(
129
+ spec: AdapterSpec,
130
+ agentHookPath: string,
131
+ subcommand: string,
132
+ adapter: AdapterId,
133
+ options?: WiringOptions,
134
+ ): string | undefined {
135
+ if (!spec.windowsBridgeHooks || !options?.windowsBridgeEntryPoint) return undefined;
136
+ return hookCommandWindows(
137
+ spec,
138
+ agentHookPath,
139
+ subcommand,
140
+ adapter,
141
+ options.windowsBridgeEntryPoint,
142
+ );
143
+ }
144
+
145
+ /** The `timeout` init writes on one event's Harnery hook, or undefined for none. */
146
+ export function hookTimeoutFor(spec: AdapterSpec, subcommand: string): number | undefined {
147
+ if (!spec.hookTimeouts) return undefined;
148
+ return spec.hookTimeouts.bySubcommand?.[subcommand] ?? spec.hookTimeouts.default;
149
+ }
150
+
64
151
  /** Pin Cursor Stop followups to Harnery's loop cap. Returns true when the entry changed. */
65
152
  export function applyCursorStopLoopLimit(group: HookGroup, subcommand: string): boolean {
66
153
  if (subcommand !== "stop") return false;
@@ -144,7 +231,7 @@ export interface WiringDiff {
144
231
  duplicates: HookEvent[];
145
232
  /** Spec events wired under an event key other than the canonical one. */
146
233
  misplaced: HookEvent[];
147
- /** Spec events whose correct-key command differs from init's canonical command. */
234
+ /** Spec events whose correct-key command, `commandWindows`, or `timeout` differs from what init writes. */
148
235
  stale: HookEvent[];
149
236
  /** Settings fields rejected by a adapter with a strict top-level schema. */
150
237
  invalidTopLevelKeys: string[];
@@ -159,7 +246,7 @@ export interface WiringDiff {
159
246
  export function diffWiring(
160
247
  settings: SettingsFile,
161
248
  spec: AdapterSpec,
162
- expected?: { agentHookPath: string; adapter: AdapterId },
249
+ expected?: { agentHookPath: string; adapter: AdapterId } & WiringOptions,
163
250
  ): WiringDiff {
164
251
  const missing: HookEvent[] = [];
165
252
  const present: HookEvent[] = [];
@@ -182,7 +269,27 @@ export function diffWiring(
182
269
  event.subcommand,
183
270
  expected.adapter,
184
271
  );
185
- if (commands.some((command) => command !== canonical)) stale.push(event);
272
+ const timeout = hookTimeoutFor(spec, event.subcommand);
273
+ const commandWindows = expectedCommandWindows(
274
+ spec,
275
+ expected.agentHookPath,
276
+ event.subcommand,
277
+ expected.adapter,
278
+ expected,
279
+ );
280
+ const handlerDrift = groups.some(
281
+ (group) =>
282
+ "hooks" in group &&
283
+ Array.isArray(group.hooks) &&
284
+ group.hooks.some(
285
+ (hook) =>
286
+ typeof hook.command === "string" &&
287
+ commandWiresSubcommand(hook.command, event.subcommand) &&
288
+ ((timeout !== undefined && hook.timeout !== timeout) ||
289
+ (spec.windowsBridgeHooks === true && hook.commandWindows !== commandWindows)),
290
+ ),
291
+ );
292
+ if (handlerDrift || commands.some((command) => command !== canonical)) stale.push(event);
186
293
  }
187
294
  const wrongKey = Object.entries(hooks).some(
188
295
  ([key, otherGroups]) =>
@@ -325,6 +432,7 @@ export function loadAdapterWiring(projectRoot: string): AdapterWiringStatus[] {
325
432
  ? {
326
433
  agentHookPath: agentHookPathForProject(projectRoot, packageRoot),
327
434
  adapter: id,
435
+ windowsBridgeEntryPoint: codexWindowsBridgeConfig(projectRoot).entryPoint,
328
436
  }
329
437
  : undefined,
330
438
  );
@@ -90,6 +90,7 @@ import { closeProcessLoggers, legacyLogFields, processLogger } from "../storage/
90
90
  import { stableScopeId } from "../workflow/scope-id.ts";
91
91
  import { type AdapterBehavior, adapterBehavior } from "./adapter/behaviors/index.ts";
92
92
  import { detectAdapter, shouldSkipHookAdapter } from "./adapter/detect.ts";
93
+ import { ADAPTER_SPECS } from "./adapter/events.ts";
93
94
  import {
94
95
  extractBashCommand,
95
96
  extractToolDescription,
@@ -98,6 +99,7 @@ import {
98
99
  type ParsedPayload,
99
100
  parsePayload,
100
101
  } from "./adapter/parse.ts";
102
+ import { hookTimeoutFor } from "./adapter/wiring.ts";
101
103
  import { scheduleBackupSnapshot } from "./backup-schedule.ts";
102
104
  import {
103
105
  captureImages,
@@ -131,6 +133,7 @@ import {
131
133
  scanStatusBoxPresent,
132
134
  scanTranscriptRuntime,
133
135
  } from "./resolve/transcript.ts";
136
+ import { clearHookRunMarker, writeHookRunMarker } from "./run-markers.ts";
134
137
  import { sessionNamePresence } from "./session-name-presence.ts";
135
138
  import { shellWaiterReason } from "./shell-waiter.ts";
136
139
 
@@ -140,6 +143,7 @@ interface Argv {
140
143
  }
141
144
 
142
145
  let hookHealthState: HookHealthState | undefined;
146
+ let hookRunMarkerPath: string | null = null;
143
147
 
144
148
  function parseArgv(argv: string[]): Argv {
145
149
  const out: Argv = { eventName: null, extra: [] };
@@ -351,7 +355,12 @@ function buildEventData(
351
355
  const forkedFrom: string | undefined = undefined;
352
356
  // Assign (or recover) name + kind in-process. Idempotent: resume
353
357
  // returns the original name; new owner consumes a counter slot.
354
- const assigned = assignNameInProcess(ctx.coordRoot, ctx.instanceId, "session", forkedFrom);
358
+ // Workflow children stay unnamed, as on the tool path: a pool name
359
+ // would make them the addressee of mail held for that name.
360
+ const assigned =
361
+ coordEnv("WORKFLOW_CHILD") === "1"
362
+ ? undefined
363
+ : assignNameInProcess(ctx.coordRoot, ctx.instanceId, "session", forkedFrom);
355
364
  // Write the adapter pid-map row so `harn agents whoami` ppid-walks find
356
365
  // this owner. Prefer the payload pid (the actual claude binary), then the
357
366
  // anchor walk (the `node` ancestor for Cursor, which has no payload pid),
@@ -416,7 +425,10 @@ function buildEventData(
416
425
  // SessionStart. Mint durable display identity here so a later
417
426
  // SessionStart remains idempotent and Codec is not stuck on the
418
427
  // 8-character native id.
419
- if (!readLiveCoordinationRow(ctx.coordRoot, ctx.instanceId)?.name) {
428
+ if (
429
+ coordEnv("WORKFLOW_CHILD") !== "1" &&
430
+ !readLiveCoordinationRow(ctx.coordRoot, ctx.instanceId)?.name
431
+ ) {
420
432
  assignNameInProcess(ctx.coordRoot, ctx.instanceId, "session");
421
433
  }
422
434
  const prompt = p?.prompt ?? "";
@@ -663,6 +675,24 @@ async function main(): Promise<number> {
663
675
  const { eventName, extra } = parseArgv(process.argv.slice(2));
664
676
  const adapter = detectAdapter(process.argv.slice(2));
665
677
  const behavior = adapter ? adapterBehavior(adapter) : null;
678
+ // Adapters that kill a hook at its timeout get a start marker, so a killed
679
+ // run leaves evidence for doctor (ADR 0197). Written before stdin is read.
680
+ const markerSpec = adapter ? ADAPTER_SPECS[adapter] : undefined;
681
+ const markerTimeout =
682
+ markerSpec && eventName && markerSpec.events.some((e) => e.subcommand === eventName)
683
+ ? hookTimeoutFor(markerSpec, eventName)
684
+ : undefined;
685
+ if (markerTimeout !== undefined && adapter && eventName && coordEnv("AGENT_COORD_OFF") !== "1") {
686
+ const markerRoot = resolveCoordRoot(process.cwd());
687
+ if (markerRoot) {
688
+ hookRunMarkerPath = writeHookRunMarker(markerRoot, {
689
+ event: eventName,
690
+ adapter,
691
+ pid: process.pid,
692
+ timeout_sec: markerTimeout,
693
+ });
694
+ }
695
+ }
666
696
  const raw = await readStdin();
667
697
  hookHealthState = beginHookHealth({
668
698
  started_at_ms: hookStartedAt,
@@ -1189,9 +1219,12 @@ async function handleSessionStarted(run: HookRun): Promise<void> {
1189
1219
  const { createAutomaticMaintenanceComposition, runAutomaticMaintenancePass } = await import(
1190
1220
  "../storage/maintenance-providers.ts"
1191
1221
  );
1192
- // Effect: throttled daily sweep of expired artifact workspaces (guarded,
1193
- // managed-expired only; see autoCleanArtifacts). Best-effort like every
1194
- // janitor here: a failure logs and never blocks session start.
1222
+ // Effect: throttled hourly artifact sweep (see autoCleanArtifacts). It
1223
+ // deletes expired workspaces, plus unheld workspaces over the per-bundle
1224
+ // ceiling or repository budget once they have been idle past
1225
+ // artifacts.idle_grace_hours. Each deletion is rechecked first and recorded
1226
+ // in .harnery/artifact-deletions.jsonl. Best-effort like every janitor
1227
+ // here: a failure logs and never blocks session start.
1195
1228
  try {
1196
1229
  autoCleanArtifacts(coordRoot);
1197
1230
  } catch (err) {
@@ -1564,7 +1597,8 @@ async function handlePostToolUse(run: HookRun): Promise<void> {
1564
1597
  // status is the wrong place to ask.
1565
1598
  try {
1566
1599
  const row = readLiveCoordinationRow(coordRoot, owner.instance_id);
1567
- const name = sessionNameDisplayPending(row);
1600
+ // A workflow child has no user-visible title to set.
1601
+ const name = coordEnv("WORKFLOW_CHILD") === "1" ? undefined : sessionNameDisplayPending(row);
1568
1602
  if (name && toolResponseMintedSessionName(payload?.tool_response, name)) {
1569
1603
  // Record the title before asking for it. The suggestion can still
1570
1604
  // change afterwards (an assigned-name rewrite, a lifecycle re-mint, a
@@ -1765,6 +1799,7 @@ async function enforcePendingSessionNameDisplay(
1765
1799
  payload: ParsedPayload | null,
1766
1800
  coordination: Heartbeat | null,
1767
1801
  ): Promise<boolean> {
1802
+ if (coordEnv("WORKFLOW_CHILD") === "1") return true;
1768
1803
  const name = sessionNameDisplayPending(coordination);
1769
1804
  if (!name) return true;
1770
1805
 
@@ -2168,6 +2203,10 @@ async function emitUserPromptSubmitSystemMessage(
2168
2203
  recoveryBriefing = "",
2169
2204
  hostPromptContext = "",
2170
2205
  ): Promise<boolean> {
2206
+ // Workflow children get no prompt-time coordination context, matching
2207
+ // SessionStart. The turn ritual is not enforced for them, the naming nudge
2208
+ // would register them as named peers, and rendering drains peer mail.
2209
+ if (coordEnv("WORKFLOW_CHILD") === "1") return false;
2171
2210
  const behavior = adapterBehavior(adapter);
2172
2211
  const { renderPromptContext } = await import("../agents/render/prompt-context.ts");
2173
2212
  let additionalContext = renderPromptContext({
@@ -2307,6 +2346,7 @@ function completeRecoveryInjection(
2307
2346
 
2308
2347
  main()
2309
2348
  .then(async (code) => {
2349
+ clearHookRunMarker(hookRunMarkerPath);
2310
2350
  writeHookHealthCompletion(hookHealthState, {
2311
2351
  finished_at_ms: performance.now(),
2312
2352
  finished_rss_bytes: process.memoryUsage().rss,
@@ -2321,6 +2361,7 @@ main()
2321
2361
  pid: process.pid,
2322
2362
  phase: "top-level",
2323
2363
  });
2364
+ clearHookRunMarker(hookRunMarkerPath);
2324
2365
  writeHookHealthCompletion(hookHealthState, {
2325
2366
  finished_at_ms: performance.now(),
2326
2367
  finished_rss_bytes: process.memoryUsage().rss,