harnery 0.41.0 → 0.43.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 (269) hide show
  1. package/dist/commander.d.ts +16 -0
  2. package/dist/commander.d.ts.map +1 -1
  3. package/dist/commander.js +21 -0
  4. package/dist/commands/agents.d.ts.map +1 -1
  5. package/dist/commands/agents.js +63 -5
  6. package/dist/commands/artifacts.d.ts.map +1 -1
  7. package/dist/commands/artifacts.js +113 -12
  8. package/dist/commands/browse.d.ts +164 -0
  9. package/dist/commands/browse.d.ts.map +1 -1
  10. package/dist/commands/browse.js +263 -51
  11. package/dist/commands/claude-desktop.d.ts.map +1 -1
  12. package/dist/commands/claude-desktop.js +52 -0
  13. package/dist/commands/disk.d.ts +6 -0
  14. package/dist/commands/disk.d.ts.map +1 -0
  15. package/dist/commands/disk.js +80 -0
  16. package/dist/commands/doctor.d.ts +8 -0
  17. package/dist/commands/doctor.d.ts.map +1 -1
  18. package/dist/commands/doctor.js +48 -1
  19. package/dist/commands/files.d.ts +16 -0
  20. package/dist/commands/files.d.ts.map +1 -1
  21. package/dist/commands/files.js +54 -2
  22. package/dist/commands/init.d.ts +2 -2
  23. package/dist/commands/init.d.ts.map +1 -1
  24. package/dist/commands/init.js +43 -9
  25. package/dist/commands/rm.d.ts +4 -0
  26. package/dist/commands/rm.d.ts.map +1 -0
  27. package/dist/commands/rm.js +87 -0
  28. package/dist/commands/servers.d.ts +7 -0
  29. package/dist/commands/servers.d.ts.map +1 -0
  30. package/dist/commands/servers.js +394 -0
  31. package/dist/commands/tunnel.d.ts +65 -0
  32. package/dist/commands/tunnel.d.ts.map +1 -1
  33. package/dist/commands/tunnel.js +372 -103
  34. package/dist/commands/web-fetch.d.ts +3 -2
  35. package/dist/commands/web-fetch.d.ts.map +1 -1
  36. package/dist/commands/web-fetch.js +25 -19
  37. package/dist/commands/web.d.ts +7 -1
  38. package/dist/commands/web.d.ts.map +1 -1
  39. package/dist/commands/web.js +9 -3
  40. package/dist/core/adapters/profiles.d.ts +9 -0
  41. package/dist/core/adapters/profiles.d.ts.map +1 -1
  42. package/dist/core/adapters/profiles.js +22 -1
  43. package/dist/core/adapters/types.d.ts +9 -0
  44. package/dist/core/adapters/types.d.ts.map +1 -1
  45. package/dist/core/agents/cli.js +13 -6
  46. package/dist/core/agents/render/prompt-context.d.ts.map +1 -1
  47. package/dist/core/agents/render/prompt-context.js +11 -5
  48. package/dist/core/agents/render/session-context.d.ts.map +1 -1
  49. package/dist/core/agents/render/session-context.js +59 -13
  50. package/dist/core/agents/state/live-coordination-view.d.ts +20 -0
  51. package/dist/core/agents/state/live-coordination-view.d.ts.map +1 -1
  52. package/dist/core/agents/state/live-coordination-view.js +60 -0
  53. package/dist/core/agents/state/names.d.ts +43 -3
  54. package/dist/core/agents/state/names.d.ts.map +1 -1
  55. package/dist/core/agents/state/names.js +87 -3
  56. package/dist/core/artifacts/delivery-card.js +9 -2
  57. package/dist/core/artifacts/index.d.ts +140 -4
  58. package/dist/core/artifacts/index.d.ts.map +1 -1
  59. package/dist/core/artifacts/index.js +522 -49
  60. package/dist/core/config.d.ts +68 -11
  61. package/dist/core/config.d.ts.map +1 -1
  62. package/dist/core/config.js +128 -3
  63. package/dist/core/diagnostics/bundle.d.ts.map +1 -1
  64. package/dist/core/diagnostics/bundle.js +2 -2
  65. package/dist/core/events/v3/producers/recorder.d.ts.map +1 -1
  66. package/dist/core/events/v3/producers/recorder.js +16 -0
  67. package/dist/core/events/v3/projection.js +9 -1
  68. package/dist/core/events/v3/support-storage/pack-reader.d.ts.map +1 -1
  69. package/dist/core/events/v3/support-storage/pack-reader.js +42 -16
  70. package/dist/core/hooks/adapter/events.d.ts +17 -0
  71. package/dist/core/hooks/adapter/events.d.ts.map +1 -1
  72. package/dist/core/hooks/adapter/events.js +5 -0
  73. package/dist/core/hooks/adapter/runtime-telemetry.js +3 -1
  74. package/dist/core/hooks/adapter/wiring.d.ts +37 -7
  75. package/dist/core/hooks/adapter/wiring.d.ts.map +1 -1
  76. package/dist/core/hooks/adapter/wiring.js +62 -4
  77. package/dist/core/hooks/cli.js +131 -23
  78. package/dist/core/hooks/fork-lineage.d.ts +40 -0
  79. package/dist/core/hooks/fork-lineage.d.ts.map +1 -0
  80. package/dist/core/hooks/fork-lineage.js +62 -0
  81. package/dist/core/hooks/resolve/transcript.d.ts +16 -0
  82. package/dist/core/hooks/resolve/transcript.d.ts.map +1 -1
  83. package/dist/core/hooks/resolve/transcript.js +84 -0
  84. package/dist/core/hooks/run-markers.d.ts +37 -0
  85. package/dist/core/hooks/run-markers.d.ts.map +1 -0
  86. package/dist/core/hooks/run-markers.js +109 -0
  87. package/dist/core/hooks/shell-rm.d.ts +2 -0
  88. package/dist/core/hooks/shell-rm.d.ts.map +1 -0
  89. package/dist/core/hooks/shell-rm.js +105 -0
  90. package/dist/core/hooks/shell-tokens.d.ts +12 -0
  91. package/dist/core/hooks/shell-tokens.d.ts.map +1 -0
  92. package/dist/core/hooks/shell-tokens.js +97 -0
  93. package/dist/core/hooks/shell-waiter.d.ts.map +1 -1
  94. package/dist/core/hooks/shell-waiter.js +3 -93
  95. package/dist/core/semantic/contract.d.ts +5 -5
  96. package/dist/core/servers/index.d.ts +211 -0
  97. package/dist/core/servers/index.d.ts.map +1 -0
  98. package/dist/core/servers/index.js +560 -0
  99. package/dist/core/servers/net.d.ts +58 -0
  100. package/dist/core/servers/net.d.ts.map +1 -0
  101. package/dist/core/servers/net.js +270 -0
  102. package/dist/core/servers/tunnels.d.ts +14 -0
  103. package/dist/core/servers/tunnels.d.ts.map +1 -0
  104. package/dist/core/servers/tunnels.js +33 -0
  105. package/dist/core/storage/builtins.js +1 -0
  106. package/dist/core/storage/process-log.d.ts +12 -0
  107. package/dist/core/storage/process-log.d.ts.map +1 -1
  108. package/dist/core/storage/process-log.js +10 -2
  109. package/dist/core/workflow/engine.d.ts.map +1 -1
  110. package/dist/core/workflow/engine.js +30 -1
  111. package/dist/core/workflow/sandbox-projection.d.ts +13 -2
  112. package/dist/core/workflow/sandbox-projection.d.ts.map +1 -1
  113. package/dist/core/workflow/sandbox-projection.js +23 -0
  114. package/dist/core/workflow/spawn-claude.d.ts.map +1 -1
  115. package/dist/core/workflow/spawn-claude.js +6 -1
  116. package/dist/core/workflow/spawn-codex.d.ts +2 -0
  117. package/dist/core/workflow/spawn-codex.d.ts.map +1 -1
  118. package/dist/core/workflow/spawn-codex.js +20 -1
  119. package/dist/core/workflow/spawn-cursor.d.ts.map +1 -1
  120. package/dist/core/workflow/spawn-cursor.js +6 -1
  121. package/dist/core/workflow/spawn-opencode.d.ts.map +1 -1
  122. package/dist/core/workflow/spawn-opencode.js +4 -1
  123. package/dist/core/workflow/types.d.ts +47 -2
  124. package/dist/core/workflow/types.d.ts.map +1 -1
  125. package/dist/core/workflow/worker-access.d.ts +92 -0
  126. package/dist/core/workflow/worker-access.d.ts.map +1 -0
  127. package/dist/core/workflow/worker-access.js +450 -0
  128. package/dist/lib/browser/batch-steps.d.ts +18 -0
  129. package/dist/lib/browser/batch-steps.d.ts.map +1 -0
  130. package/dist/lib/browser/batch-steps.js +82 -0
  131. package/dist/lib/browser/collect/collector.d.ts +99 -0
  132. package/dist/lib/browser/collect/collector.d.ts.map +1 -0
  133. package/dist/lib/browser/collect/collector.js +202 -0
  134. package/dist/lib/browser/collect/fields.d.ts +31 -0
  135. package/dist/lib/browser/collect/fields.d.ts.map +1 -0
  136. package/dist/lib/browser/collect/fields.js +75 -0
  137. package/dist/lib/browser/collect/index.d.ts +6 -0
  138. package/dist/lib/browser/collect/index.d.ts.map +1 -0
  139. package/dist/lib/browser/collect/index.js +5 -0
  140. package/dist/lib/browser/collect/page-driver.d.ts +14 -0
  141. package/dist/lib/browser/collect/page-driver.d.ts.map +1 -0
  142. package/dist/lib/browser/collect/page-driver.js +306 -0
  143. package/dist/lib/browser/collect/presets.d.ts +52 -0
  144. package/dist/lib/browser/collect/presets.d.ts.map +1 -0
  145. package/dist/lib/browser/collect/presets.js +176 -0
  146. package/dist/lib/browser/collect/scroll-plan.d.ts +85 -0
  147. package/dist/lib/browser/collect/scroll-plan.d.ts.map +1 -0
  148. package/dist/lib/browser/collect/scroll-plan.js +118 -0
  149. package/dist/lib/browser/index.d.ts +1 -1
  150. package/dist/lib/browser/index.d.ts.map +1 -1
  151. package/dist/lib/browser/page-review-judge.d.ts +2 -0
  152. package/dist/lib/browser/page-review-judge.d.ts.map +1 -1
  153. package/dist/lib/browser/page-review-judge.js +13 -0
  154. package/dist/lib/browser/qa-run-contracts.d.ts +10 -0
  155. package/dist/lib/browser/qa-run-contracts.d.ts.map +1 -1
  156. package/dist/lib/browser/qa-run.d.ts.map +1 -1
  157. package/dist/lib/browser/qa-run.js +1 -0
  158. package/dist/lib/browser/windows-chrome.d.ts +59 -0
  159. package/dist/lib/browser/windows-chrome.d.ts.map +1 -0
  160. package/dist/lib/browser/windows-chrome.js +179 -0
  161. package/dist/lib/claude-desktop-share.d.ts +136 -0
  162. package/dist/lib/claude-desktop-share.d.ts.map +1 -0
  163. package/dist/lib/claude-desktop-share.js +394 -0
  164. package/dist/lib/disk-usage.d.ts +50 -0
  165. package/dist/lib/disk-usage.d.ts.map +1 -0
  166. package/dist/lib/disk-usage.js +303 -0
  167. package/dist/lib/docs-lint.d.ts +6 -0
  168. package/dist/lib/docs-lint.d.ts.map +1 -1
  169. package/dist/lib/docs-lint.js +8 -3
  170. package/dist/lib/guarded-remove.d.ts +34 -0
  171. package/dist/lib/guarded-remove.d.ts.map +1 -0
  172. package/dist/lib/guarded-remove.js +310 -0
  173. package/dist/lib/instructions/templates.js +8 -8
  174. package/dist/lib/tunnel/allowlist.d.ts +44 -0
  175. package/dist/lib/tunnel/allowlist.d.ts.map +1 -0
  176. package/dist/lib/tunnel/allowlist.js +204 -0
  177. package/dist/lib/tunnel/current-address.d.ts +41 -0
  178. package/dist/lib/tunnel/current-address.d.ts.map +1 -0
  179. package/dist/lib/tunnel/current-address.js +123 -0
  180. package/dist/lib/tunnel/error-page.d.ts +1 -1
  181. package/dist/lib/tunnel/error-page.d.ts.map +1 -1
  182. package/dist/lib/tunnel/error-page.js +19 -8
  183. package/dist/lib/tunnel/gate.js +33 -7
  184. package/dist/lib/tunnel/path-scope.d.ts +22 -0
  185. package/dist/lib/tunnel/path-scope.d.ts.map +1 -0
  186. package/dist/lib/tunnel/path-scope.js +62 -0
  187. package/dist/lib/tunnel/processes.d.ts +39 -0
  188. package/dist/lib/tunnel/processes.d.ts.map +1 -0
  189. package/dist/lib/tunnel/processes.js +121 -0
  190. package/dist/lib/tunnel/state.d.ts +33 -3
  191. package/dist/lib/tunnel/state.d.ts.map +1 -1
  192. package/dist/lib/tunnel/state.js +115 -30
  193. package/package.json +7 -1
  194. package/schemas/config.schema.json +91 -0
  195. package/src/commander.ts +51 -0
  196. package/src/commands/agents.ts +111 -11
  197. package/src/commands/artifacts.ts +171 -19
  198. package/src/commands/browse.ts +400 -57
  199. package/src/commands/claude-desktop.ts +61 -0
  200. package/src/commands/disk.ts +114 -0
  201. package/src/commands/doctor.ts +52 -1
  202. package/src/commands/files.ts +93 -2
  203. package/src/commands/init.ts +55 -5
  204. package/src/commands/rm.ts +115 -0
  205. package/src/commands/servers.ts +506 -0
  206. package/src/commands/tunnel.ts +494 -116
  207. package/src/commands/web-fetch.ts +30 -19
  208. package/src/commands/web.ts +9 -3
  209. package/src/core/adapters/profiles.ts +22 -1
  210. package/src/core/adapters/types.ts +10 -0
  211. package/src/core/agents/cli.ts +14 -6
  212. package/src/core/agents/render/prompt-context.ts +25 -8
  213. package/src/core/agents/render/session-context.ts +84 -16
  214. package/src/core/agents/state/live-coordination-view.ts +73 -0
  215. package/src/core/agents/state/names.ts +117 -6
  216. package/src/core/artifacts/delivery-card.ts +10 -2
  217. package/src/core/artifacts/index.ts +697 -44
  218. package/src/core/config.ts +183 -4
  219. package/src/core/diagnostics/bundle.ts +2 -1
  220. package/src/core/events/v3/producers/recorder.ts +16 -0
  221. package/src/core/events/v3/projection.ts +11 -1
  222. package/src/core/events/v3/support-storage/pack-reader.ts +45 -19
  223. package/src/core/hooks/adapter/events.ts +19 -0
  224. package/src/core/hooks/adapter/runtime-telemetry.ts +3 -1
  225. package/src/core/hooks/adapter/wiring.ts +116 -8
  226. package/src/core/hooks/cli.ts +152 -22
  227. package/src/core/hooks/fork-lineage.ts +82 -0
  228. package/src/core/hooks/resolve/transcript.ts +88 -0
  229. package/src/core/hooks/run-markers.ts +132 -0
  230. package/src/core/hooks/shell-rm.ts +103 -0
  231. package/src/core/hooks/shell-tokens.ts +96 -0
  232. package/src/core/hooks/shell-waiter.ts +4 -91
  233. package/src/core/servers/index.ts +712 -0
  234. package/src/core/servers/net.ts +283 -0
  235. package/src/core/servers/tunnels.ts +39 -0
  236. package/src/core/storage/builtins.ts +1 -0
  237. package/src/core/storage/process-log.ts +17 -2
  238. package/src/core/workflow/engine.ts +38 -1
  239. package/src/core/workflow/sandbox-projection.ts +38 -2
  240. package/src/core/workflow/spawn-claude.ts +5 -1
  241. package/src/core/workflow/spawn-codex.ts +18 -1
  242. package/src/core/workflow/spawn-cursor.ts +5 -1
  243. package/src/core/workflow/spawn-opencode.ts +4 -1
  244. package/src/core/workflow/types.ts +62 -2
  245. package/src/core/workflow/worker-access.ts +525 -0
  246. package/src/lib/browser/batch-steps.ts +79 -0
  247. package/src/lib/browser/collect/collector.ts +306 -0
  248. package/src/lib/browser/collect/fields.ts +87 -0
  249. package/src/lib/browser/collect/index.ts +47 -0
  250. package/src/lib/browser/collect/page-driver.ts +327 -0
  251. package/src/lib/browser/collect/presets.ts +237 -0
  252. package/src/lib/browser/collect/scroll-plan.ts +179 -0
  253. package/src/lib/browser/index.ts +1 -0
  254. package/src/lib/browser/page-review-judge.ts +16 -0
  255. package/src/lib/browser/qa-run-contracts.ts +11 -0
  256. package/src/lib/browser/qa-run.ts +1 -0
  257. package/src/lib/browser/windows-chrome.ts +227 -0
  258. package/src/lib/claude-desktop-share.ts +511 -0
  259. package/src/lib/disk-usage.ts +347 -0
  260. package/src/lib/docs-lint.ts +8 -3
  261. package/src/lib/guarded-remove.ts +376 -0
  262. package/src/lib/instructions/templates.ts +8 -8
  263. package/src/lib/tunnel/allowlist.ts +208 -0
  264. package/src/lib/tunnel/current-address.ts +158 -0
  265. package/src/lib/tunnel/error-page.ts +25 -9
  266. package/src/lib/tunnel/gate.ts +38 -9
  267. package/src/lib/tunnel/path-scope.ts +73 -0
  268. package/src/lib/tunnel/processes.ts +149 -0
  269. package/src/lib/tunnel/state.ts +143 -30
@@ -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,
@@ -1,7 +1,9 @@
1
1
  import { createHash } from "node:crypto";
2
- import { constants, createReadStream } from "node:fs";
2
+ import { constants } from "node:fs";
3
3
  import { lstat, mkdir, open, realpath, rm, writeFile } from "node:fs/promises";
4
4
  import { dirname, isAbsolute, relative, resolve } from "node:path";
5
+ import { Readable } from "node:stream";
6
+ import { finished } from "node:stream/promises";
5
7
  import { StringDecoder } from "node:string_decoder";
6
8
  import { createGunzip, createInflateRaw } from "node:zlib";
7
9
  import { stateDirMode, stateFileMode } from "../../../storage/modes.ts";
@@ -89,24 +91,17 @@ async function* streamEventV3SupportPackRecordsForManifest(
89
91
  throw new Error("event_v3_support_payload_length_mismatch");
90
92
  if (payloadStat.size > bounded.max_payload_bytes)
91
93
  throw new Error("event_v3_support_payload_too_large");
92
- if ((await hashHandle(payloadPath, payloadHandle)) !== manifest.payload.digest) {
94
+ if ((await hashHandle(payloadHandle)) !== manifest.payload.digest) {
93
95
  throw new Error("event_v3_support_payload_digest_mismatch");
94
96
  }
95
97
  limits.fault?.("after_payload_digest", payloadPath);
96
98
  await assertCanonicalSingleGzipMember(
97
- payloadPath,
98
99
  payloadHandle,
99
100
  payloadStat.size,
100
101
  bounded.max_decompressed_bytes,
101
102
  );
102
103
 
103
104
  const gunzip = createGunzip();
104
- const source = createReadStream(payloadPath, {
105
- fd: payloadHandle.fd,
106
- autoClose: false,
107
- start: 0,
108
- });
109
- source.pipe(gunzip);
110
105
  const decoder = new StringDecoder("utf8");
111
106
  let pending = "";
112
107
  let decompressedBytes = 0;
@@ -116,7 +111,7 @@ async function* streamEventV3SupportPackRecordsForManifest(
116
111
  const entries: EventV3LogicalAuthorityEntry[] = [];
117
112
  const seen = new Set<string>();
118
113
 
119
- for await (const current of gunzip) {
114
+ for await (const current of decompressHandle(payloadHandle, 0, gunzip)) {
120
115
  const chunk = current as Buffer;
121
116
  decompressedBytes += chunk.length;
122
117
  if (decompressedBytes > bounded.max_decompressed_bytes) {
@@ -247,16 +242,49 @@ export async function unpackEventV3SupportPack(
247
242
  }
248
243
  }
249
244
 
250
- async function hashHandle(
251
- path: string,
252
- handle: Awaited<ReturnType<typeof open>>,
253
- ): Promise<`sha256:${string}`> {
245
+ async function hashHandle(handle: Awaited<ReturnType<typeof open>>): Promise<`sha256:${string}`> {
254
246
  const hash = createHash("sha256");
255
- const stream = createReadStream(path, { fd: handle.fd, autoClose: false, start: 0 });
256
- for await (const current of stream) hash.update(current as Buffer);
247
+ for await (const current of handleChunks(handle, 0)) hash.update(current);
257
248
  return `sha256:${hash.digest("hex")}`;
258
249
  }
259
250
 
251
+ async function* handleChunks(handle: Awaited<ReturnType<typeof open>>, position: number) {
252
+ while (true) {
253
+ const buffer = Buffer.alloc(64 * 1024);
254
+ const { bytesRead } = await handle.read(buffer, 0, buffer.length, position);
255
+ if (bytesRead === 0) return;
256
+ position += bytesRead;
257
+ yield buffer.subarray(0, bytesRead);
258
+ }
259
+ }
260
+
261
+ async function* decompressHandle(
262
+ handle: Awaited<ReturnType<typeof open>>,
263
+ start: number,
264
+ decoder: ReturnType<typeof createGunzip>,
265
+ ): AsyncGenerator<Buffer> {
266
+ const source = Readable.from(handleChunks(handle, start));
267
+ // Observe failures immediately, then settle source reads before the caller
268
+ // can close and reuse the verified handle, including on early rejection.
269
+ const completed = finished(source, { cleanup: true }).then(
270
+ () => undefined,
271
+ (error: unknown) => error,
272
+ );
273
+ source.once("error", (error) => decoder.destroy(error));
274
+ source.pipe(decoder);
275
+ try {
276
+ for await (const chunk of decoder) yield chunk as Buffer;
277
+ source.unpipe(decoder);
278
+ source.resume();
279
+ const error = await completed;
280
+ if (error) throw error;
281
+ } finally {
282
+ source.destroy();
283
+ decoder.destroy();
284
+ await completed;
285
+ }
286
+ }
287
+
260
288
  async function realDirectory(path: string): Promise<string> {
261
289
  const stat = await lstat(path);
262
290
  if (!stat.isDirectory() || stat.isSymbolicLink())
@@ -265,7 +293,6 @@ async function realDirectory(path: string): Promise<string> {
265
293
  }
266
294
 
267
295
  async function assertCanonicalSingleGzipMember(
268
- path: string,
269
296
  handle: Awaited<ReturnType<typeof open>>,
270
297
  payloadBytes: number,
271
298
  maximumDecompressedBytes: number,
@@ -284,9 +311,8 @@ async function assertCanonicalSingleGzipMember(
284
311
  throw new Error("event_v3_support_gzip_header_noncanonical");
285
312
  }
286
313
  const inflate = createInflateRaw();
287
- createReadStream(path, { fd: handle.fd, autoClose: false, start: 10 }).pipe(inflate);
288
314
  let decompressed = 0;
289
- for await (const current of inflate) {
315
+ for await (const current of decompressHandle(handle, 10, inflate)) {
290
316
  decompressed += (current as Buffer).length;
291
317
  if (decompressed > maximumDecompressedBytes) {
292
318
  inflate.destroy();
@@ -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
  );