harnery 0.31.6 → 0.32.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 (218) hide show
  1. package/dist/commander.d.ts.map +1 -1
  2. package/dist/commander.js +2 -0
  3. package/dist/commands/agents.d.ts +0 -9
  4. package/dist/commands/agents.d.ts.map +1 -1
  5. package/dist/commands/agents.js +70 -95
  6. package/dist/commands/browse-session.d.ts +21 -0
  7. package/dist/commands/browse-session.d.ts.map +1 -0
  8. package/dist/commands/browse-session.js +157 -0
  9. package/dist/commands/browse.d.ts.map +1 -1
  10. package/dist/commands/browse.js +205 -37
  11. package/dist/commands/checkpoint.js +1 -1
  12. package/dist/commands/deinit.d.ts.map +1 -1
  13. package/dist/commands/deinit.js +4 -0
  14. package/dist/commands/docs.d.ts.map +1 -1
  15. package/dist/commands/docs.js +40 -0
  16. package/dist/commands/doctor.d.ts.map +1 -1
  17. package/dist/commands/doctor.js +100 -17
  18. package/dist/commands/init.d.ts +1 -2
  19. package/dist/commands/init.d.ts.map +1 -1
  20. package/dist/commands/init.js +16 -5
  21. package/dist/commands/tunnel.d.ts.map +1 -1
  22. package/dist/commands/tunnel.js +2 -3
  23. package/dist/core/agents/canonical-emit.d.ts +1 -2
  24. package/dist/core/agents/canonical-emit.d.ts.map +1 -1
  25. package/dist/core/agents/canonical-emit.js +1 -2
  26. package/dist/core/agents/cli.js +123 -88
  27. package/dist/core/agents/coord-client.d.ts +7 -3
  28. package/dist/core/agents/coord-client.d.ts.map +1 -1
  29. package/dist/core/agents/coord-client.js +5 -4
  30. package/dist/core/agents/finalization.d.ts +68 -0
  31. package/dist/core/agents/finalization.d.ts.map +1 -0
  32. package/dist/core/agents/finalization.js +443 -0
  33. package/dist/core/agents/git-hook.d.ts +51 -0
  34. package/dist/core/agents/git-hook.d.ts.map +1 -0
  35. package/dist/core/agents/git-hook.js +118 -0
  36. package/dist/core/agents/render/prompt-context.d.ts +6 -5
  37. package/dist/core/agents/render/prompt-context.d.ts.map +1 -1
  38. package/dist/core/agents/render/prompt-context.js +26 -13
  39. package/dist/core/agents/render/session-context.d.ts.map +1 -1
  40. package/dist/core/agents/render/session-context.js +15 -2
  41. package/dist/core/agents/rules/claim-conflict.js +3 -3
  42. package/dist/core/agents/rules/commit-conflict.d.ts +15 -6
  43. package/dist/core/agents/rules/commit-conflict.d.ts.map +1 -1
  44. package/dist/core/agents/rules/commit-conflict.js +21 -5
  45. package/dist/core/agents/rules/stop-hook.d.ts +3 -0
  46. package/dist/core/agents/rules/stop-hook.d.ts.map +1 -1
  47. package/dist/core/agents/rules/stop-hook.js +59 -23
  48. package/dist/core/agents/session-events.d.ts +8 -16
  49. package/dist/core/agents/session-events.d.ts.map +1 -1
  50. package/dist/core/agents/session-events.js +12 -26
  51. package/dist/core/agents/state/heartbeat-projector.d.ts +3 -0
  52. package/dist/core/agents/state/heartbeat-projector.d.ts.map +1 -1
  53. package/dist/core/agents/state/heartbeat-projector.js +44 -1
  54. package/dist/core/agents/state/heartbeat-writer.d.ts +32 -5
  55. package/dist/core/agents/state/heartbeat-writer.d.ts.map +1 -1
  56. package/dist/core/agents/state/heartbeat-writer.js +54 -17
  57. package/dist/core/agents/state/names.d.ts +22 -1
  58. package/dist/core/agents/state/names.d.ts.map +1 -1
  59. package/dist/core/agents/state/names.js +40 -2
  60. package/dist/core/config.d.ts +30 -2
  61. package/dist/core/config.d.ts.map +1 -1
  62. package/dist/core/config.js +74 -11
  63. package/dist/core/governor/planning.d.ts.map +1 -1
  64. package/dist/core/governor/planning.js +12 -13
  65. package/dist/core/hooks/adapter/detect.d.ts +2 -4
  66. package/dist/core/hooks/adapter/detect.d.ts.map +1 -1
  67. package/dist/core/hooks/adapter/detect.js +4 -14
  68. package/dist/core/hooks/adapter/output.d.ts +1 -1
  69. package/dist/core/hooks/adapter/output.d.ts.map +1 -1
  70. package/dist/core/hooks/adapter/output.js +5 -4
  71. package/dist/core/hooks/adapter/wiring.d.ts +23 -0
  72. package/dist/core/hooks/adapter/wiring.d.ts.map +1 -1
  73. package/dist/core/hooks/adapter/wiring.js +32 -0
  74. package/dist/core/hooks/cli.d.ts +1 -2
  75. package/dist/core/hooks/cli.d.ts.map +1 -1
  76. package/dist/core/hooks/cli.js +169 -27
  77. package/dist/core/hooks/codex-wsl-bridge.d.ts +46 -0
  78. package/dist/core/hooks/codex-wsl-bridge.d.ts.map +1 -0
  79. package/dist/core/hooks/codex-wsl-bridge.js +137 -0
  80. package/dist/core/hooks/effects/index.d.ts +1 -1
  81. package/dist/core/hooks/effects/index.js +3 -3
  82. package/dist/core/hooks/events/schema.d.ts +27 -0
  83. package/dist/core/hooks/events/schema.d.ts.map +1 -1
  84. package/dist/core/hooks/resolve/owner.d.ts +7 -1
  85. package/dist/core/hooks/resolve/owner.d.ts.map +1 -1
  86. package/dist/core/hooks/resolve/owner.js +15 -0
  87. package/dist/core/hooks/resolve/transcript.d.ts +44 -0
  88. package/dist/core/hooks/resolve/transcript.d.ts.map +1 -1
  89. package/dist/core/hooks/resolve/transcript.js +176 -1
  90. package/dist/core/hooks/session-name-presence.d.ts +30 -0
  91. package/dist/core/hooks/session-name-presence.d.ts.map +1 -0
  92. package/dist/core/hooks/session-name-presence.js +43 -0
  93. package/dist/core/hooks/unsafe-cross-shell.d.ts +13 -0
  94. package/dist/core/hooks/unsafe-cross-shell.d.ts.map +1 -0
  95. package/dist/core/hooks/unsafe-cross-shell.js +151 -0
  96. package/dist/core/work/state.d.ts +4 -5
  97. package/dist/core/work/state.d.ts.map +1 -1
  98. package/dist/core/work/state.js +5 -8
  99. package/dist/core/workflow/index.d.ts +1 -1
  100. package/dist/core/workflow/index.d.ts.map +1 -1
  101. package/dist/core/workflow/proof.d.ts +2 -2
  102. package/dist/core/workflow/proof.d.ts.map +1 -1
  103. package/dist/core/workflow/proof.js +2 -2
  104. package/dist/core/workflow/run-state.d.ts +2 -2
  105. package/dist/core/workflow/run-state.d.ts.map +1 -1
  106. package/dist/core/workflow/run-state.js +2 -2
  107. package/dist/core/workflow/types.d.ts +3 -3
  108. package/dist/core/workflow/types.d.ts.map +1 -1
  109. package/dist/core/workflow/workspaces/execution.d.ts +2 -2
  110. package/dist/core/workflow/workspaces/execution.d.ts.map +1 -1
  111. package/dist/core/workflow/workspaces/execution.js +5 -0
  112. package/dist/core/workflow/workspaces/index.d.ts +1 -1
  113. package/dist/core/workflow/workspaces/index.d.ts.map +1 -1
  114. package/dist/core/workflow/workspaces/local-git.d.ts.map +1 -1
  115. package/dist/core/workflow/workspaces/local-git.js +1 -2
  116. package/dist/core/workflow/workspaces/paths.d.ts +2 -2
  117. package/dist/core/workflow/workspaces/types.d.ts +0 -6
  118. package/dist/core/workflow/workspaces/types.d.ts.map +1 -1
  119. package/dist/core/workflow/workspaces/validate.d.ts +0 -2
  120. package/dist/core/workflow/workspaces/validate.d.ts.map +1 -1
  121. package/dist/core/workflow/workspaces/validate.js +0 -2
  122. package/dist/lib/browser/client.d.ts +118 -1
  123. package/dist/lib/browser/client.d.ts.map +1 -1
  124. package/dist/lib/browser/client.js +435 -6
  125. package/dist/lib/browser/geometry.d.ts.map +1 -1
  126. package/dist/lib/browser/geometry.js +180 -37
  127. package/dist/lib/browser/index.d.ts +5 -1
  128. package/dist/lib/browser/index.d.ts.map +1 -1
  129. package/dist/lib/browser/index.js +5 -1
  130. package/dist/lib/browser/netscape-cookies.d.ts +6 -0
  131. package/dist/lib/browser/netscape-cookies.d.ts.map +1 -0
  132. package/dist/lib/browser/netscape-cookies.js +31 -0
  133. package/dist/lib/browser/proxy.d.ts +24 -0
  134. package/dist/lib/browser/proxy.d.ts.map +1 -0
  135. package/dist/lib/browser/proxy.js +84 -0
  136. package/dist/lib/browser/runts.d.ts +6 -0
  137. package/dist/lib/browser/runts.d.ts.map +1 -1
  138. package/dist/lib/browser/runts.js +21 -4
  139. package/dist/lib/browser/session-control.d.ts +112 -0
  140. package/dist/lib/browser/session-control.d.ts.map +1 -0
  141. package/dist/lib/browser/session-control.js +670 -0
  142. package/dist/lib/docs-links.d.ts +108 -0
  143. package/dist/lib/docs-links.d.ts.map +1 -0
  144. package/dist/lib/docs-links.js +555 -0
  145. package/dist/lib/exec.d.ts +1 -1
  146. package/dist/lib/exec.js +1 -1
  147. package/dist/lib/identities/assume.d.ts +4 -2
  148. package/dist/lib/identities/assume.d.ts.map +1 -1
  149. package/dist/lib/identities/assume.js +15 -2
  150. package/dist/lib/instructions/git-hooks.d.ts +75 -0
  151. package/dist/lib/instructions/git-hooks.d.ts.map +1 -0
  152. package/dist/lib/instructions/git-hooks.js +238 -0
  153. package/dist/lib/instructions/splice.d.ts +10 -4
  154. package/dist/lib/instructions/splice.d.ts.map +1 -1
  155. package/dist/lib/instructions/splice.js +19 -11
  156. package/package.json +1 -1
  157. package/schemas/config.schema.json +32 -0
  158. package/src/commander.ts +2 -0
  159. package/src/commands/agents.ts +96 -115
  160. package/src/commands/browse-session.ts +245 -0
  161. package/src/commands/browse.ts +287 -44
  162. package/src/commands/checkpoint.ts +1 -1
  163. package/src/commands/deinit.ts +7 -1
  164. package/src/commands/docs.ts +56 -0
  165. package/src/commands/doctor.ts +100 -18
  166. package/src/commands/init.ts +17 -9
  167. package/src/commands/tunnel.ts +2 -5
  168. package/src/core/agents/canonical-emit.ts +1 -2
  169. package/src/core/agents/cli.ts +137 -88
  170. package/src/core/agents/coord-client.ts +10 -5
  171. package/src/core/agents/finalization.ts +595 -0
  172. package/src/core/agents/git-hook.ts +126 -0
  173. package/src/core/agents/render/prompt-context.ts +28 -13
  174. package/src/core/agents/render/session-context.ts +15 -2
  175. package/src/core/agents/rules/claim-conflict.ts +3 -3
  176. package/src/core/agents/rules/commit-conflict.ts +35 -8
  177. package/src/core/agents/rules/stop-hook.ts +71 -23
  178. package/src/core/agents/session-events.ts +14 -41
  179. package/src/core/agents/state/heartbeat-projector.ts +45 -1
  180. package/src/core/agents/state/heartbeat-writer.ts +78 -16
  181. package/src/core/agents/state/names.ts +54 -2
  182. package/src/core/config.ts +92 -12
  183. package/src/core/governor/planning.ts +17 -12
  184. package/src/core/hooks/adapter/detect.ts +4 -13
  185. package/src/core/hooks/adapter/output.ts +9 -4
  186. package/src/core/hooks/adapter/wiring.ts +43 -0
  187. package/src/core/hooks/cli.ts +201 -23
  188. package/src/core/hooks/codex-wsl-bridge.ts +188 -0
  189. package/src/core/hooks/effects/index.ts +5 -5
  190. package/src/core/hooks/events/schema.ts +27 -0
  191. package/src/core/hooks/resolve/owner.ts +22 -1
  192. package/src/core/hooks/resolve/transcript.ts +164 -1
  193. package/src/core/hooks/session-name-presence.ts +52 -0
  194. package/src/core/hooks/unsafe-cross-shell.ts +160 -0
  195. package/src/core/work/state.ts +8 -12
  196. package/src/core/workflow/engine.ts +2 -2
  197. package/src/core/workflow/index.ts +0 -1
  198. package/src/core/workflow/proof.ts +4 -4
  199. package/src/core/workflow/run-state.ts +5 -5
  200. package/src/core/workflow/types.ts +3 -3
  201. package/src/core/workflow/workspaces/execution.ts +11 -4
  202. package/src/core/workflow/workspaces/index.ts +0 -3
  203. package/src/core/workflow/workspaces/local-git.ts +1 -2
  204. package/src/core/workflow/workspaces/paths.ts +2 -2
  205. package/src/core/workflow/workspaces/types.ts +0 -9
  206. package/src/core/workflow/workspaces/validate.ts +1 -5
  207. package/src/lib/browser/client.ts +528 -6
  208. package/src/lib/browser/geometry.ts +197 -37
  209. package/src/lib/browser/index.ts +39 -0
  210. package/src/lib/browser/netscape-cookies.ts +39 -0
  211. package/src/lib/browser/proxy.ts +105 -0
  212. package/src/lib/browser/runts.ts +27 -3
  213. package/src/lib/browser/session-control.ts +892 -0
  214. package/src/lib/docs-links.ts +674 -0
  215. package/src/lib/exec.ts +1 -1
  216. package/src/lib/identities/assume.ts +22 -1
  217. package/src/lib/instructions/git-hooks.ts +259 -0
  218. package/src/lib/instructions/splice.ts +41 -11
@@ -1,4 +1,4 @@
1
- import { mkdirSync, statSync, writeFileSync } from "node:fs";
1
+ import { existsSync, mkdirSync, statSync, unlinkSync, writeFileSync } from "node:fs";
2
2
  import { homedir } from "node:os";
3
3
  import { dirname, isAbsolute, resolve } from "node:path";
4
4
  import type { Command } from "commander";
@@ -7,6 +7,8 @@ import { resolveBinName } from "../core/config.ts";
7
7
  import {
8
8
  type AssertResult,
9
9
  Browser,
10
+ browserProxyFromEnv,
11
+ browserProxyGateFromEnv,
10
12
  type ContentAnnotationBox,
11
13
  type ContentChecksResult,
12
14
  type CritiqueResult,
@@ -15,6 +17,7 @@ import {
15
17
  DEFAULT_CRITIQUE_RUBRIC,
16
18
  type DevOverlayResult,
17
19
  type Diagnostics,
20
+ extractObservedIp,
18
21
  type LayoutAxis,
19
22
  type LayoutLintResult,
20
23
  type OverflowResult,
@@ -25,9 +28,15 @@ import {
25
28
  type TargetSizeResult,
26
29
  tilesFromFullPage,
27
30
  type VisibilityResult,
31
+ WEBRTC_PROXY_ONLY_ARG,
28
32
  type WidthResult,
33
+ writeNetscapeCookieFile,
29
34
  wslHeadedLaunchArgs,
30
35
  } from "../lib/browser/index.ts";
36
+ import {
37
+ type BrowserSessionServer,
38
+ startBrowserSessionServer,
39
+ } from "../lib/browser/session-control.ts";
31
40
  import {
32
41
  type DiffResult,
33
42
  diffAgainstBaseline,
@@ -66,7 +75,6 @@ interface BrowseOpts {
66
75
  // `opts.noX`. So `--no-screenshot` toggles `screenshot`, `--no-full-page`
67
76
  // toggles `fullPage`, `--no-cookies` toggles `cookies`.
68
77
  screenshot?: boolean;
69
- domOnly?: boolean;
70
78
  fullPage?: boolean;
71
79
  snapshot?: boolean;
72
80
  html?: boolean;
@@ -80,8 +88,12 @@ interface BrowseOpts {
80
88
  batch?: string;
81
89
  networkHar?: string;
82
90
  login?: boolean;
91
+ loginCloseFile?: string;
92
+ controlFile?: string;
83
93
  headed?: boolean;
84
94
  browserArg?: string[];
95
+ proxyFromEnv?: boolean;
96
+ exportCookies?: string;
85
97
  cookies?: boolean;
86
98
  store?: string;
87
99
  profile?: string;
@@ -201,7 +213,6 @@ export function registerBrowseCommand(
201
213
  "Output prefix for the trio (writes <prefix>.png, .html, .json). Defaults to ~/.cache/harnery/browse/last.",
202
214
  )
203
215
  .option("--no-screenshot", "Skip the .png in the trio (DOM + JSON only)")
204
- .option("--dom-only", "Alias for --no-screenshot")
205
216
  .option("--no-full-page", "Capture only the viewport, not the full scrollable page")
206
217
  .option("--snapshot", "Print body innerText to stdout (skips file writes)")
207
218
  .option(
@@ -235,6 +246,14 @@ export function registerBrowseCommand(
235
246
  "desktop",
236
247
  )
237
248
  .option("--login", "Headed mode for one-time auth flow (cookies persist in profile)")
249
+ .option(
250
+ "--login-close-file <path>",
251
+ "With --login, wait for this file instead of terminal Enter, then remove it and close cleanly",
252
+ )
253
+ .option(
254
+ "--control-file <path>",
255
+ "With --login, publish an owner-only descriptor for repeated browse-session control",
256
+ )
238
257
  .option("--headed", "Headed mode for one-off (no auth-flow framing)")
239
258
  .option(
240
259
  "--browser-arg <flag>",
@@ -246,9 +265,20 @@ export function registerBrowseCommand(
246
265
  (value: string, prev: string[] = []) => [...prev, value],
247
266
  [] as string[],
248
267
  )
268
+ .option(
269
+ "--proxy-from-env",
270
+ "Pass HTTP(S)_PROXY to Playwright as an authenticated browser proxy. " +
271
+ "Credentials stay in the child environment, never command arguments. " +
272
+ "When the host also injects the HARNERY_BROWSER_PROXY_* gate variables, " +
273
+ "the expected IP is verified before the requested URL opens.",
274
+ )
249
275
  .option("--no-cookies", "Skip cookie-jar attach and persist")
250
276
  .option("--store <path>", `Cookie store path (default ${DEFAULT_STORE})`)
251
277
  .option("--profile <dir>", `Persistent Chromium profile dir (default ${DEFAULT_PROFILE})`)
278
+ .option(
279
+ "--export-cookies <path>",
280
+ "Write live profile cookies as an owner-only Netscape cookies.txt file on close",
281
+ )
252
282
  .option(
253
283
  "--wait-until <strategy>",
254
284
  "Navigation wait strategy: load | domcontentloaded | networkidle | commit",
@@ -317,7 +347,7 @@ export function registerBrowseCommand(
317
347
  )
318
348
  .option(
319
349
  "--check-overflow-fail",
320
- "Exit non-zero if --check-overflow detects horizontal overflow.",
350
+ "Run --check-overflow and exit 2 if horizontal overflow is detected.",
321
351
  )
322
352
  .option(
323
353
  "--no-check-overflow-annotate",
@@ -336,7 +366,10 @@ export function registerBrowseCommand(
336
366
  "Minimum block text length to scan (default 40; smaller labels can't meaningfully wrap).",
337
367
  "40",
338
368
  )
339
- .option("--check-runts-fail", "Exit non-zero if any runt is detected.")
369
+ .option(
370
+ "--check-runts-fail",
371
+ "Run a document-wide runt check when needed; exit 2 on a runt, missing scope, or incomplete sweep.",
372
+ )
340
373
  .option(
341
374
  "--no-check-runts-annotate",
342
375
  "Skip drawing runt boxes on the screenshot (JSON still emitted).",
@@ -353,7 +386,10 @@ export function registerBrowseCommand(
353
386
  "Maximum rendered-center drift in CSS pixels (default 2).",
354
387
  "2",
355
388
  )
356
- .option("--check-align-fail", "Exit 2 when an alignment target fails or is missing.")
389
+ .option(
390
+ "--check-align-fail",
391
+ "Exit 2 when an alignment target fails, is unknown, or is missing.",
392
+ )
357
393
  .option("--no-check-align-annotate", "Skip alignment screenshot annotations.")
358
394
  .option(
359
395
  "--check-gap <selector>",
@@ -367,11 +403,12 @@ export function registerBrowseCommand(
367
403
  "Expected gap in CSS pixels. Without it, groups of 3+ infer the median gap.",
368
404
  )
369
405
  .option("--check-gap-threshold <px>", "Maximum gap deviation in CSS pixels (default 2).", "2")
370
- .option("--check-gap-fail", "Exit 2 when a gap target fails or is missing.")
406
+ .option("--check-gap-fail", "Exit 2 when a gap target fails, is unknown, or is missing.")
371
407
  .option("--no-check-gap-annotate", "Skip gap screenshot annotations.")
372
408
  .option(
373
409
  "--check-clip <selector>",
374
- "Check descendants against this container's rectangular clipping chain (repeatable).",
410
+ "Check every matching container for descendant boxes or text paint leaving its " +
411
+ "rectangular clipping chain or nearest block parent (repeatable).",
375
412
  (value: string, previous: string[] = []) => [...previous, value],
376
413
  [] as string[],
377
414
  )
@@ -380,7 +417,7 @@ export function registerBrowseCommand(
380
417
  "Allowed rectangular overrun in CSS pixels (default 0).",
381
418
  "0",
382
419
  )
383
- .option("--check-clip-fail", "Exit 2 when a clip target fails or is missing.")
420
+ .option("--check-clip-fail", "Exit 2 when a clip target fails, is unknown, or is missing.")
384
421
  .option("--no-check-clip-annotate", "Skip clip screenshot annotations.")
385
422
  .option(
386
423
  "--check-overlap <selector>",
@@ -393,7 +430,10 @@ export function registerBrowseCommand(
393
430
  "Allowed overlap depth on either axis in CSS pixels (default 0).",
394
431
  "0",
395
432
  )
396
- .option("--check-overlap-fail", "Exit 2 when an overlap target fails or is missing.")
433
+ .option(
434
+ "--check-overlap-fail",
435
+ "Exit 2 when an overlap target fails, is unknown, or is missing.",
436
+ )
397
437
  .option("--no-check-overlap-annotate", "Skip overlap screenshot annotations.")
398
438
  .option(
399
439
  "--check-crowd <selector>",
@@ -413,7 +453,7 @@ export function registerBrowseCommand(
413
453
  "Pairs closer than this are flagged; negative separation (overlap) always flags.",
414
454
  "6",
415
455
  )
416
- .option("--check-crowd-fail", "Exit 2 when a crowd target fails or is missing.")
456
+ .option("--check-crowd-fail", "Exit 2 when a crowd target fails, is unknown, or is missing.")
417
457
  .option("--no-check-crowd-annotate", "Skip crowd screenshot annotations.")
418
458
  .option(
419
459
  "--check-placeholder [selector]",
@@ -424,7 +464,10 @@ export function registerBrowseCommand(
424
464
  "bug on funnels and reports. Do not point it at a page that documents template " +
425
465
  "syntax in prose.",
426
466
  )
427
- .option("--check-placeholder-fail", "Exit 2 if any placeholder tell is found.")
467
+ .option(
468
+ "--check-placeholder-fail",
469
+ "Run a document-wide placeholder check when needed; exit 2 if the result does not pass.",
470
+ )
428
471
  .option("--no-check-placeholder-annotate", "Skip placeholder screenshot annotations.")
429
472
  .option(
430
473
  "--check-images [selector]",
@@ -438,7 +481,10 @@ export function registerBrowseCommand(
438
481
  "Aspect-ratio deviation above which an image counts as stretched (0–1, default 0.1 = 10%).",
439
482
  "0.1",
440
483
  )
441
- .option("--check-images-fail", "Exit 2 if any image failed to load or is stretched.")
484
+ .option(
485
+ "--check-images-fail",
486
+ "Run a document-wide image check when needed; exit 2 if the result does not pass.",
487
+ )
442
488
  .option("--no-check-images-annotate", "Skip image screenshot annotations.")
443
489
  .option(
444
490
  "--check-truncation [selector]",
@@ -451,7 +497,10 @@ export function registerBrowseCommand(
451
497
  "Overflow past the clip box, in CSS px, above which truncation counts (default 2).",
452
498
  "2",
453
499
  )
454
- .option("--check-truncation-fail", "Exit 2 if any text is actively truncated.")
500
+ .option(
501
+ "--check-truncation-fail",
502
+ "Run a document-wide truncation check when needed; exit 2 if the result does not pass.",
503
+ )
455
504
  .option("--no-check-truncation-annotate", "Skip truncation screenshot annotations.")
456
505
  .option(
457
506
  "--check-contrast [selector]",
@@ -460,7 +509,10 @@ export function registerBrowseCommand(
460
509
  "with --batch to cover light and dark. Text over an image or gradient is reported as " +
461
510
  "`unknown`, not failed. Optional selector scopes the sweep (default: whole body).",
462
511
  )
463
- .option("--check-contrast-fail", "Exit 2 if any text fails the contrast ratio.")
512
+ .option(
513
+ "--check-contrast-fail",
514
+ "Run a document-wide contrast check when needed; exit 2 on fail, unknown, or a missing scope.",
515
+ )
464
516
  .option("--no-check-contrast-annotate", "Skip contrast screenshot annotations.")
465
517
  .option(
466
518
  "--check-critique [selector]",
@@ -483,7 +535,10 @@ export function registerBrowseCommand(
483
535
  "24",
484
536
  )
485
537
  .option("--check-critique-rubric <text>", "Override the default critique rubric.")
486
- .option("--check-critique-fail", "Exit 2 if the critique returns any high-severity finding.")
538
+ .option(
539
+ "--check-critique-fail",
540
+ "Run a whole-page critique when needed; exit 2 unless the critique conclusively passes.",
541
+ )
487
542
  .option(
488
543
  "--assert <expr>",
489
544
  "Assert a page value (repeatable). Grammar: '<op> <selector> => <expected>', where op is " +
@@ -493,7 +548,7 @@ export function registerBrowseCommand(
493
548
  (value: string, previous: string[] = []) => [...previous, value],
494
549
  [] as string[],
495
550
  )
496
- .option("--assert-fail", "Exit 2 if any --assert fails.")
551
+ .option("--assert-fail", "Exit 2 if any --assert fails; requires at least one --assert.")
497
552
  .option(
498
553
  "--check-hit [selector]",
499
554
  "Check pointer-target size and spacing in the document or optional scope (repeatable).",
@@ -510,7 +565,7 @@ export function registerBrowseCommand(
510
565
  )
511
566
  .option(
512
567
  "--check-hit-fail",
513
- "Exit 2 when target sizing fails, is incomplete, or a scope is missing.",
568
+ "Run a document-wide target-size check when needed; exit 2 when it fails or is incomplete.",
514
569
  )
515
570
  .option("--no-check-hit-annotate", "Skip target-size screenshot annotations.")
516
571
  .option(
@@ -531,7 +586,10 @@ export function registerBrowseCommand(
531
586
  "considered a match (0–1, default 0.01 = 1%).",
532
587
  "0.01",
533
588
  )
534
- .option("--diff-fail", "Exit non-zero if --diff mismatchRatio exceeds --diff-threshold.")
589
+ .option(
590
+ "--diff-fail",
591
+ "Exit non-zero if --diff mismatchRatio exceeds --diff-threshold; requires --diff.",
592
+ )
535
593
  .option(
536
594
  "--no-dev-overlay",
537
595
  "Skip auto-capture of Next.js dev-overlay issues. Default: capture every queued error (kind/code/message/stack) when a <nextjs-portal> shadow root is present. Necessary because Next.js 16 + React 19 route hydration errors + most React warnings through onCaughtError → next-devtools' errorQueue, NOT through console.error, so Playwright's standard listener doesn't see them. Surfaces them in the JSON envelope under `devOverlay`.",
@@ -552,6 +610,7 @@ async function runBrowse(
552
610
  opts: BrowseOpts,
553
611
  context: HarneryProgramContext | undefined,
554
612
  ): Promise<void> {
613
+ normalizeCheckFailOptions(opts);
555
614
  const alignAxis = parseLayoutAxis(opts.checkAlignAxis ?? "auto", "--check-align-axis");
556
615
  const gapAxis = parseLayoutAxis(opts.checkGapAxis ?? "auto", "--check-gap-axis");
557
616
  const alignThreshold = parseNonNegativeNumber(
@@ -584,6 +643,20 @@ async function runBrowse(
584
643
  : new CookieJar({ path: opts.store ?? DEFAULT_STORE, source: "harn-browse" });
585
644
  const headed = opts.login || opts.headed;
586
645
  const viewport = parseViewport(opts.viewport ?? "desktop");
646
+ const proxy = opts.proxyFromEnv ? browserProxyFromEnv() : undefined;
647
+ const proxyGate = opts.proxyFromEnv ? browserProxyGateFromEnv() : null;
648
+ if (opts.loginCloseFile && !opts.login) {
649
+ throw new Error("--login-close-file requires --login.");
650
+ }
651
+ if (opts.controlFile && !opts.login) {
652
+ throw new Error("--control-file requires --login.");
653
+ }
654
+ const loginCloseFile = opts.loginCloseFile ? resolve(opts.loginCloseFile) : null;
655
+ if (loginCloseFile && existsSync(loginCloseFile)) {
656
+ throw new Error(
657
+ `Login close signal already exists at ${loginCloseFile}. Remove the stale file before launching.`,
658
+ );
659
+ }
587
660
 
588
661
  const browser = new Browser({
589
662
  profileDir: opts.profile ?? DEFAULT_PROFILE,
@@ -594,14 +667,28 @@ async function runBrowse(
594
667
  waitUntil: opts.waitUntil as BrowseOpts["waitUntil"] as never,
595
668
  recordHarPath: opts.networkHar ? resolve(opts.networkHar) : undefined,
596
669
  extraHeaders: context?.extraHeaders,
597
- launchArgs: resolveLaunchArgs(opts, Boolean(headed)),
670
+ launchArgs: resolveLaunchArgs(opts, Boolean(headed), Boolean(proxy)),
671
+ proxy,
598
672
  });
599
673
 
600
674
  // Print mode: --snapshot / --html / --json all suppress file writes.
601
675
  const printMode = opts.snapshot || opts.html || opts.json;
676
+ let controlServer: BrowserSessionServer | null = null;
677
+ let receivedSignal: NodeJS.Signals | null = null;
678
+ let resolveSignal: (() => void) | null = null;
679
+ const signalPromise = new Promise<void>((resolveSignalPromise) => {
680
+ resolveSignal = resolveSignalPromise;
681
+ });
682
+ const handleSignal = (signal: NodeJS.Signals) => {
683
+ receivedSignal = signal;
684
+ resolveSignal?.();
685
+ };
686
+ const handleSigint = () => handleSignal("SIGINT");
687
+ const handleSigterm = () => handleSignal("SIGTERM");
602
688
 
603
689
  try {
604
690
  await browser.open();
691
+ if (proxyGate) await verifyBrowserProxyGate(browser, proxyGate);
605
692
  const navResult = await browser.navigate(url);
606
693
 
607
694
  if (opts.fill) {
@@ -790,14 +877,41 @@ async function runBrowse(
790
877
  await browser.annotateContent(contentBoxes);
791
878
  }
792
879
 
880
+ if (opts.controlFile) {
881
+ controlServer = await startBrowserSessionServer(opts.controlFile, browser);
882
+ process.once("SIGINT", handleSigint);
883
+ process.once("SIGTERM", handleSigterm);
884
+ emit.log(`browser session ready: ${controlServer.descriptorPath}`, "info");
885
+ }
886
+
793
887
  if (opts.login) {
794
- await new Promise<void>((res) => {
888
+ const lifecycleAbort = new AbortController();
889
+ const closeWaits: Promise<void>[] = [];
890
+ if (loginCloseFile) {
891
+ emit.log(
892
+ `[--login] Headed Chromium is open. Drive the visible window now. Create ${loginCloseFile} to close cleanly.`,
893
+ "info",
894
+ );
895
+ closeWaits.push(waitForLoginCloseFile(loginCloseFile, lifecycleAbort.signal));
896
+ } else {
795
897
  emit.log(
796
898
  "[--login] Headed Chromium is open. Walk through your auth flow now. Press Enter here to close + persist cookies into the profile.",
797
899
  "info",
798
900
  );
799
- process.stdin.once("data", () => res());
800
- });
901
+ closeWaits.push(waitForTerminalEnter(lifecycleAbort.signal));
902
+ }
903
+ if (controlServer) {
904
+ closeWaits.push(controlServer.closeRequested, signalPromise);
905
+ }
906
+ await Promise.race(closeWaits);
907
+ lifecycleAbort.abort();
908
+ if (controlServer) await controlServer.stopAccepting();
909
+ }
910
+
911
+ if (opts.exportCookies) {
912
+ const exportPath = resolve(opts.exportCookies);
913
+ writeNetscapeCookieFile(exportPath, await browser.cookies());
914
+ emit.log(`exported live profile cookies to ${exportPath} (mode 600)`, "info");
801
915
  }
802
916
 
803
917
  // Auto-capture Next.js dev-overlay issues unless --no-dev-overlay was passed.
@@ -911,12 +1025,21 @@ async function runBrowse(
911
1025
  }
912
1026
  }
913
1027
 
914
- if (opts.checkRuntsFail && runts && runts.runts.length > 0) {
915
- for (const hit of runts.runts) {
1028
+ if (opts.checkRuntsFail && runts && runts.outcome !== "pass") {
1029
+ if (!runts.found) {
1030
+ emit.log(`check-runts FAIL ${runts.scope}: scope not found`, "warn");
1031
+ } else if (runts.outcome === "unknown") {
916
1032
  emit.log(
917
- `check-runts FAIL ${hit.block}: last line is a lone "${hit.word}" ("…${hit.snippet.slice(-40)}")`,
1033
+ `check-runts FAIL ${runts.scope ?? "document"}: sweep incomplete after ${runts.scannedBlocks} blocks`,
918
1034
  "warn",
919
1035
  );
1036
+ } else {
1037
+ for (const hit of runts.runts) {
1038
+ emit.log(
1039
+ `check-runts FAIL ${hit.block}: last line is a lone "${hit.word}" ("…${hit.snippet.slice(-40)}")`,
1040
+ "warn",
1041
+ );
1042
+ }
920
1043
  }
921
1044
  process.exitCode = 2;
922
1045
  }
@@ -935,9 +1058,12 @@ async function runBrowse(
935
1058
 
936
1059
  applyLayoutLintFailGates(opts, layoutLint, hit);
937
1060
  applyContentFailGates(opts, content);
938
- if (opts.checkCritiqueFail && critique && critique.outcome === "fail") {
939
- const high = critique.findings.filter((f) => f.severity === "high").length;
940
- emit.log(`check-critique FAIL: ${high} high-severity finding(s)`, "warn");
1061
+ if (opts.checkCritiqueFail && critique && critique.outcome !== "pass") {
1062
+ const detail =
1063
+ critique.outcome === "skipped"
1064
+ ? (critique.error ?? "critique was skipped")
1065
+ : `${critique.findings.filter((f) => f.severity === "high").length} high-severity finding(s)`;
1066
+ emit.log(`check-critique FAIL: ${detail}`, "warn");
941
1067
  process.exitCode = 2;
942
1068
  }
943
1069
  if (opts.assertFail && asserts) {
@@ -951,7 +1077,11 @@ async function runBrowse(
951
1077
  if (failed.length > 0) process.exitCode = 2;
952
1078
  }
953
1079
  } finally {
1080
+ process.removeListener("SIGINT", handleSigint);
1081
+ process.removeListener("SIGTERM", handleSigterm);
1082
+ await controlServer?.cleanup();
954
1083
  await browser.close();
1084
+ if (receivedSignal) process.exitCode = receivedSignal === "SIGINT" ? 130 : 143;
955
1085
  }
956
1086
  }
957
1087
 
@@ -1155,7 +1285,7 @@ async function runTrioMode(
1155
1285
 
1156
1286
  const written: string[] = [];
1157
1287
 
1158
- const skipScreenshot = opts.screenshot === false || opts.domOnly === true;
1288
+ const skipScreenshot = opts.screenshot === false;
1159
1289
  let pngPath: string | undefined;
1160
1290
  let pngBytes: number | undefined;
1161
1291
  let captureEvalResult: unknown;
@@ -1176,9 +1306,7 @@ async function runTrioMode(
1176
1306
  }
1177
1307
  written.push(pngPath);
1178
1308
  } else if (opts.captureEvaluate) {
1179
- throw new Error(
1180
- "--capture-evaluate requires a screenshot; remove --no-screenshot / --dom-only.",
1181
- );
1309
+ throw new Error("--capture-evaluate requires a screenshot; remove --no-screenshot.");
1182
1310
  }
1183
1311
 
1184
1312
  const htmlPath = `${prefix}.html`;
@@ -1232,9 +1360,7 @@ async function runTrioMode(
1232
1360
  let savedBaseline: SaveBaselineResult | undefined;
1233
1361
  let diff: DiffResult | undefined;
1234
1362
  if ((opts.baseline || opts.diff) && !pngPath) {
1235
- throw new Error(
1236
- "--baseline / --diff require a screenshot; --no-screenshot / --dom-only disables it.",
1237
- );
1363
+ throw new Error("--baseline / --diff require a screenshot; --no-screenshot disables it.");
1238
1364
  }
1239
1365
  if (opts.baseline && pngPath) {
1240
1366
  savedBaseline = saveBaseline(pngPath, opts.baseline);
@@ -1328,6 +1454,13 @@ async function runTrioMode(
1328
1454
  emit.log(`check-overflow: [${ok ? "OK" : "FAIL"}] ${detail}`, ok ? "info" : "warn");
1329
1455
  }
1330
1456
 
1457
+ if (runts) {
1458
+ emit.log(
1459
+ `check-runts: [${runts.outcome.toUpperCase()}] ${runts.scope ?? "document"}: ${runts.runts.length} runt${runts.runts.length === 1 ? "" : "s"} in ${runts.scannedBlocks} scanned block${runts.scannedBlocks === 1 ? "" : "s"}${runts.truncated ? " (incomplete)" : ""}`,
1460
+ runts.outcome === "pass" ? "info" : "warn",
1461
+ );
1462
+ }
1463
+
1331
1464
  if (layoutLint) {
1332
1465
  logLayoutLintSummary(layoutLint);
1333
1466
  }
@@ -1397,6 +1530,59 @@ function parseNonNegativeNumber(value: string, flag: string): number {
1397
1530
  return parsed;
1398
1531
  }
1399
1532
 
1533
+ function normalizeCheckFailOptions(opts: BrowseOpts): void {
1534
+ // Whole-document checks can be enabled by their fail flag. Selector-based
1535
+ // checks cannot invent a scope, so reject the invocation before Chromium
1536
+ // opens instead of returning a misleading zero.
1537
+ if (opts.checkOverflowFail) opts.checkOverflow = true;
1538
+ if (opts.checkRuntsFail && opts.checkRunts === undefined) opts.checkRunts = true;
1539
+ if (opts.checkPlaceholderFail && opts.checkPlaceholder === undefined) {
1540
+ opts.checkPlaceholder = true;
1541
+ }
1542
+ if (opts.checkImagesFail && opts.checkImages === undefined) opts.checkImages = true;
1543
+ if (opts.checkTruncationFail && opts.checkTruncation === undefined) {
1544
+ opts.checkTruncation = true;
1545
+ }
1546
+ if (opts.checkContrastFail && opts.checkContrast === undefined) opts.checkContrast = true;
1547
+ if (opts.checkCritiqueFail && opts.checkCritique === undefined) opts.checkCritique = true;
1548
+ if (opts.checkHitFail && (!opts.checkHit || opts.checkHit.length === 0)) {
1549
+ opts.checkHit = [null];
1550
+ }
1551
+
1552
+ const requireTargets = (
1553
+ failEnabled: boolean | undefined,
1554
+ targets: unknown[] | undefined,
1555
+ failFlag: string,
1556
+ checkFlag: string,
1557
+ ): void => {
1558
+ if (failEnabled && (!targets || targets.length === 0)) {
1559
+ throw new Error(`${failFlag} requires ${checkFlag}.`);
1560
+ }
1561
+ };
1562
+
1563
+ requireTargets(
1564
+ opts.checkVisibleFail,
1565
+ opts.checkVisible,
1566
+ "--check-visible-fail",
1567
+ "--check-visible",
1568
+ );
1569
+ requireTargets(opts.checkWidthFail, opts.checkWidth, "--check-width-fail", "--check-width");
1570
+ requireTargets(opts.checkAlignFail, opts.checkAlign, "--check-align-fail", "--check-align");
1571
+ requireTargets(opts.checkGapFail, opts.checkGap, "--check-gap-fail", "--check-gap");
1572
+ requireTargets(opts.checkClipFail, opts.checkClip, "--check-clip-fail", "--check-clip");
1573
+ requireTargets(
1574
+ opts.checkOverlapFail,
1575
+ opts.checkOverlap,
1576
+ "--check-overlap-fail",
1577
+ "--check-overlap",
1578
+ );
1579
+ requireTargets(opts.checkCrowdFail, opts.checkCrowd, "--check-crowd-fail", "--check-crowd");
1580
+ requireTargets(opts.assertFail, opts.assert, "--assert-fail", "--assert");
1581
+ if (opts.diffFail && !opts.diff) {
1582
+ throw new Error("--diff-fail requires --diff.");
1583
+ }
1584
+ }
1585
+
1400
1586
  function parseLayoutAxis(value: string, flag: string): LayoutAxis {
1401
1587
  if (value === "auto" || value === "x" || value === "y") return value;
1402
1588
  throw new Error(`${flag} must be auto, x, or y (got: ${value})`);
@@ -1466,12 +1652,16 @@ function applyLayoutLintFailGates(
1466
1652
  results: T[] | undefined,
1467
1653
  ): void => {
1468
1654
  if (!enabled || !results) return;
1469
- const failed = results.filter((result) => !result.found || result.outcome === "fail");
1655
+ const failed = results.filter(
1656
+ (result) => !result.found || result.outcome === "fail" || result.outcome === "unknown",
1657
+ );
1470
1658
  for (const result of failed) {
1471
- emit.log(
1472
- `check-${rule} FAIL ${result.selector}: ${result.found ? "rendered relation failed" : "element not found"}`,
1473
- "warn",
1474
- );
1659
+ const reason = !result.found
1660
+ ? "element not found"
1661
+ : result.outcome === "unknown"
1662
+ ? "result unknown"
1663
+ : "rendered relation failed";
1664
+ emit.log(`check-${rule} FAIL ${result.selector}: ${reason}`, "warn");
1475
1665
  }
1476
1666
  if (failed.length > 0) process.exitCode = 2;
1477
1667
  };
@@ -1602,8 +1792,13 @@ function applyContentFailGates(opts: BrowseOpts, content: ContentChecksResult |
1602
1792
  result: { found: boolean; outcome: string } | undefined,
1603
1793
  ): void => {
1604
1794
  if (!enabled || !result) return;
1605
- if (!result.found || result.outcome === "fail") {
1606
- emit.log(`check-${rule} FAIL: ${result.found ? "issues found" : "scope not found"}`, "warn");
1795
+ if (!result.found || result.outcome !== "pass") {
1796
+ const reason = !result.found
1797
+ ? "scope not found"
1798
+ : result.outcome === "unknown"
1799
+ ? "result unknown"
1800
+ : "issues found";
1801
+ emit.log(`check-${rule} FAIL: ${reason}`, "warn");
1607
1802
  process.exitCode = 2;
1608
1803
  }
1609
1804
  };
@@ -1682,7 +1877,7 @@ function summarizeDiagnostics(diag: Diagnostics): Record<string, unknown> {
1682
1877
  * 2. HARNERY_BROWSER_ARGS env (whitespace-separated) — a machine-wide default.
1683
1878
  * 3. --browser-arg flags on this invocation (repeatable).
1684
1879
  */
1685
- function resolveLaunchArgs(opts: BrowseOpts, headed: boolean): string[] {
1880
+ function resolveLaunchArgs(opts: BrowseOpts, headed: boolean, proxyEnabled: boolean): string[] {
1686
1881
  const args: string[] = [];
1687
1882
  if (headed && !process.env.HARNERY_BROWSER_NO_WSL_DEFAULTS) {
1688
1883
  args.push(...wslHeadedLaunchArgs());
@@ -1690,9 +1885,57 @@ function resolveLaunchArgs(opts: BrowseOpts, headed: boolean): string[] {
1690
1885
  const envArgs = process.env.HARNERY_BROWSER_ARGS?.trim();
1691
1886
  if (envArgs) args.push(...envArgs.split(/\s+/));
1692
1887
  if (opts.browserArg && opts.browserArg.length > 0) args.push(...opts.browserArg);
1888
+ if (proxyEnabled) args.push(WEBRTC_PROXY_ONLY_ARG);
1693
1889
  return [...new Set(args)];
1694
1890
  }
1695
1891
 
1892
+ async function verifyBrowserProxyGate(
1893
+ browser: Browser,
1894
+ gate: { checkUrl: string; expectedIp: string },
1895
+ ): Promise<void> {
1896
+ const result = await browser.navigate(gate.checkUrl);
1897
+ if (result.status === null || result.status < 200 || result.status >= 300) {
1898
+ throw new Error(
1899
+ `Browser proxy gate failed before target navigation (HTTP ${result.status ?? "unknown"}).`,
1900
+ );
1901
+ }
1902
+ const body = await browser.evaluate<string>("document.body?.innerText ?? ''");
1903
+ const observedIp = extractObservedIp(body);
1904
+ if (observedIp !== gate.expectedIp) {
1905
+ throw new Error(
1906
+ `Browser proxy gate returned ${observedIp ?? "no IP"}; expected ${gate.expectedIp}. Target navigation was blocked.`,
1907
+ );
1908
+ }
1909
+ emit.log(`browser proxy gate: ${observedIp} · expected IP confirmed`, "info");
1910
+ }
1911
+
1912
+ async function waitForLoginCloseFile(path: string, signal?: AbortSignal): Promise<void> {
1913
+ while (!existsSync(path) && !signal?.aborted) {
1914
+ await new Promise((resolve) => setTimeout(resolve, 250));
1915
+ }
1916
+ if (signal?.aborted) return;
1917
+ unlinkSync(path);
1918
+ }
1919
+
1920
+ async function waitForTerminalEnter(signal: AbortSignal): Promise<void> {
1921
+ await new Promise<void>((resolveWait) => {
1922
+ const onData = () => {
1923
+ cleanup();
1924
+ resolveWait();
1925
+ };
1926
+ const onAbort = () => {
1927
+ cleanup();
1928
+ resolveWait();
1929
+ };
1930
+ const cleanup = () => {
1931
+ process.stdin.removeListener("data", onData);
1932
+ signal.removeEventListener("abort", onAbort);
1933
+ };
1934
+ process.stdin.once("data", onData);
1935
+ signal.addEventListener("abort", onAbort, { once: true });
1936
+ });
1937
+ }
1938
+
1696
1939
  function parseViewport(spec: string): { width: number; height: number } {
1697
1940
  const preset = VIEWPORT_PRESETS[spec.toLowerCase()];
1698
1941
  if (preset) return preset;
@@ -165,7 +165,7 @@ function resolveContinuityIdentity(
165
165
  }
166
166
 
167
167
  function adapterFromPlatform(platform: string | undefined): Adapter | null {
168
- if (platform === "claude_code" || platform === "claude-code") return "claude-code";
168
+ if (platform === "claude-code") return "claude-code";
169
169
  if (platform === "cursor") return "cursor";
170
170
  if (platform === "codex") return "codex";
171
171
  return null;
@@ -34,8 +34,10 @@ import type { Command } from "commander";
34
34
  import type { EmitContext } from "../commander.ts";
35
35
  import { DEFAULT_BIN_NAME } from "../core/config.ts";
36
36
  import { ADAPTER_SPECS, type AdapterId } from "../core/hooks/adapter/events.ts";
37
+ import type { SettingsFile } from "../core/hooks/adapter/wiring.ts";
37
38
  import { removeInstructions } from "../lib/instructions/apply.ts";
38
- import { type SettingsFile, unwireHooks } from "./init.ts";
39
+ import { removeGitHooks } from "../lib/instructions/git-hooks.ts";
40
+ import { unwireHooks } from "./init.ts";
39
41
 
40
42
  interface DeinitOpts {
41
43
  adapter: string;
@@ -138,6 +140,10 @@ export function registerDeinitCommand(program: Command, emit: EmitContext, binNa
138
140
  const removed = removeInstructions(projectRoot, { adapter, dryRun });
139
141
  actions.push(...removed.actions, ...removed.warnings.map((w) => `! ${w}`));
140
142
 
143
+ // ── 1c. git-hook managed regions ───────────────────────────────────────
144
+ const gitHooks = removeGitHooks(projectRoot, { dryRun });
145
+ actions.push(...gitHooks.actions, ...gitHooks.warnings.map((w) => `! ${w}`));
146
+
141
147
  // ── 2. coord root (opt-in; destructive) ────────────────────────────────
142
148
  if (purgeState) {
143
149
  if (!coordExists) {