@hover-dev/core 0.15.0 → 0.16.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 (49) hide show
  1. package/dist/agents/aider.d.ts.map +1 -1
  2. package/dist/agents/aider.js +6 -14
  3. package/dist/agents/codex.d.ts.map +1 -1
  4. package/dist/agents/codex.js +9 -4
  5. package/dist/agents/cursor.d.ts.map +1 -1
  6. package/dist/agents/cursor.js +8 -17
  7. package/dist/agents/gemini.d.ts.map +1 -1
  8. package/dist/agents/gemini.js +3 -14
  9. package/dist/agents/qwen.d.ts.map +1 -1
  10. package/dist/agents/qwen.js +3 -14
  11. package/dist/agents/shared.d.ts +28 -0
  12. package/dist/agents/shared.d.ts.map +1 -0
  13. package/dist/agents/shared.js +35 -0
  14. package/dist/mcp/sourceFence.d.ts +23 -0
  15. package/dist/mcp/sourceFence.d.ts.map +1 -0
  16. package/dist/mcp/sourceFence.js +75 -0
  17. package/dist/mcp/sourceServer.d.ts +3 -0
  18. package/dist/mcp/sourceServer.d.ts.map +1 -0
  19. package/dist/mcp/sourceServer.js +116 -0
  20. package/dist/playwright/preflight.d.ts.map +1 -1
  21. package/dist/playwright/preflight.js +6 -1
  22. package/dist/playwright/raiseWindow.d.ts.map +1 -1
  23. package/dist/playwright/raiseWindow.js +22 -3
  24. package/dist/playwright/resolveMcpConfig.d.ts +6 -0
  25. package/dist/playwright/resolveMcpConfig.d.ts.map +1 -1
  26. package/dist/playwright/resolveMcpConfig.js +15 -2
  27. package/dist/plugin-api.d.ts +7 -0
  28. package/dist/plugin-api.d.ts.map +1 -1
  29. package/dist/runSession.d.ts.map +1 -1
  30. package/dist/runSession.js +5 -0
  31. package/dist/service/cdpHandlers.d.ts +3 -7
  32. package/dist/service/cdpHandlers.d.ts.map +1 -1
  33. package/dist/service/cdpHandlers.js +4 -16
  34. package/dist/service.d.ts +6 -0
  35. package/dist/service.d.ts.map +1 -1
  36. package/dist/service.js +128 -49
  37. package/dist/specs/optimizeSpec.d.ts.map +1 -1
  38. package/dist/specs/optimizeSpec.js +28 -6
  39. package/dist/specs/softBatch.d.ts +14 -0
  40. package/dist/specs/softBatch.d.ts.map +1 -0
  41. package/dist/specs/softBatch.js +177 -0
  42. package/dist/specs/text.d.ts +17 -0
  43. package/dist/specs/text.d.ts.map +1 -0
  44. package/dist/specs/text.js +24 -0
  45. package/dist/specs/writeCaseCsv.d.ts.map +1 -1
  46. package/dist/specs/writeCaseCsv.js +2 -8
  47. package/dist/specs/writeSpec.d.ts.map +1 -1
  48. package/dist/specs/writeSpec.js +2 -9
  49. package/package.json +5 -2
@@ -3,6 +3,14 @@ import { mkdirSync, writeFileSync } from 'node:fs';
3
3
  import { dirname, resolve } from 'node:path';
4
4
  import { tmpdir } from 'node:os';
5
5
  import process from 'node:process';
6
+ /** The `mcp__<id>` tool-name prefix Claude Code exposes a plugin MCP server's
7
+ * tools under: non-alphanumerics collapse to `_` and edges are trimmed (e.g.
8
+ * `@hover-dev/security:flows` → `mcp__hover_dev_security_flows`). Used to build
9
+ * the hard-sandbox allow-list. Single source so the service and the CLI scan
10
+ * command can't drift on how the prefix is derived. */
11
+ export function mcpToolPrefix(serverId) {
12
+ return `mcp__${serverId.replace(/[^a-zA-Z0-9]+/g, '_').replace(/^_+|_+$/g, '')}`;
13
+ }
6
14
  export function resolveMcpConfig(opts) {
7
15
  // Resolve the package's main file, then walk back to its package root.
8
16
  // Using `package.json` as the resolution target is the documented
@@ -46,8 +54,13 @@ export function resolveMcpConfig(opts) {
46
54
  const config = { mcpServers };
47
55
  const outDir = resolve(tmpdir(), 'hover');
48
56
  mkdirSync(outDir, { recursive: true });
49
- const suffix = opts.suffix ? `-${opts.suffix}` : '';
50
- const outPath = resolve(outDir, `mcp-config-${opts.port}${suffix}.json`);
57
+ // Sanitise the suffix before it lands in a filesystem path — it's derived
58
+ // from plugin/mode ids, so guard against path separators and other unsafe
59
+ // characters slipping into the filename.
60
+ const safeSuffix = opts.suffix
61
+ ? `-${opts.suffix.replace(/[^a-zA-Z0-9._-]+/g, '_')}`
62
+ : '';
63
+ const outPath = resolve(outDir, `mcp-config-${opts.port}${safeSuffix}.json`);
51
64
  writeFileSync(outPath, JSON.stringify(config, null, 2), 'utf-8');
52
65
  return outPath;
53
66
  }
@@ -40,6 +40,13 @@ export interface HoverPluginMode {
40
40
  /** Mode ids this mode cannot be active alongside. Two plugins both
41
41
  * needing an exclusive proxy would set each other here. */
42
42
  conflictsWith?: string[];
43
+ /** CSS colour the widget tints to while this mode is engaged — the mode
44
+ * bar, launcher, and panel chrome all retint to it. Any CSS colour the
45
+ * user's Chrome accepts (the widget derives the dim/hover/ink/tint shades
46
+ * from it via `color-mix`). Defaults to security orange (`#fb923c`) when
47
+ * omitted, so a plugin only sets this to stand apart — e.g. pentest's
48
+ * `#ef4444` red signalling "offensive mode". */
49
+ accent?: string;
43
50
  }
44
51
  export interface HoverPluginMcpServer {
45
52
  /** Stable, namespaced id (`@hover-dev/security:flows`). Host enforces
@@ -1 +1 @@
1
- {"version":3,"file":"plugin-api.d.ts","sourceRoot":"","sources":["../src/plugin-api.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"plugin-api.d.ts","sourceRoot":"","sources":["../src/plugin-api.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"runSession.d.ts","sourceRoot":"","sources":["../src/runSession.ts"],"names":[],"mappings":"AAoBA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AACrD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,wBAAwB,CAAC;AAGxD,MAAM,WAAW,iBAAiB;IAChC,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;IAChB;oFACgF;IAChF,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,mEAAmE;IACnE,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;6EACyE;IACzE,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,kEAAkE;IAClE,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB;;yCAEqC;IACrC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;+EAC2E;IAC3E,iBAAiB,CAAC,EAAE,MAAM,EAAE,CAAC;IAC7B;oFACgF;IAChF,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,2DAA2D;IAC3D,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,gBAAgB;IAC/B;mCAC+B;IAC/B,KAAK,EAAE,SAAS,EAAE,CAAC;IACnB,yCAAyC;IACzC,OAAO,EAAE,MAAM,CAAC;IAChB,qDAAqD;IACrD,OAAO,EAAE,OAAO,CAAC;CAClB;AAED,wBAAsB,UAAU,CAC9B,IAAI,EAAE,iBAAiB,EACvB,OAAO,EAAE,CAAC,EAAE,EAAE,WAAW,KAAK,IAAI,GACjC,OAAO,CAAC,gBAAgB,CAAC,CAuD3B"}
1
+ {"version":3,"file":"runSession.d.ts","sourceRoot":"","sources":["../src/runSession.ts"],"names":[],"mappings":"AAoBA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AACrD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,wBAAwB,CAAC;AAGxD,MAAM,WAAW,iBAAiB;IAChC,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;IAChB;oFACgF;IAChF,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,mEAAmE;IACnE,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;6EACyE;IACzE,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,kEAAkE;IAClE,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB;;yCAEqC;IACrC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;+EAC2E;IAC3E,iBAAiB,CAAC,EAAE,MAAM,EAAE,CAAC;IAC7B;oFACgF;IAChF,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,2DAA2D;IAC3D,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,gBAAgB;IAC/B;mCAC+B;IAC/B,KAAK,EAAE,SAAS,EAAE,CAAC;IACnB,yCAAyC;IACzC,OAAO,EAAE,MAAM,CAAC;IAChB,qDAAqD;IACrD,OAAO,EAAE,OAAO,CAAC;CAClB;AAED,wBAAsB,UAAU,CAC9B,IAAI,EAAE,iBAAiB,EACvB,OAAO,EAAE,CAAC,EAAE,EAAE,WAAW,KAAK,IAAI,GACjC,OAAO,CAAC,gBAAgB,CAAC,CA4D3B"}
@@ -70,6 +70,11 @@ export async function runSession(opts, onEvent) {
70
70
  isError = true;
71
71
  }
72
72
  }
73
+ // On abort (opts.signal), invokeAgent SIGTERMs the child and no session_end
74
+ // arrives, so the error flag above never gets set. Honour the doc contract
75
+ // ("True if the run ended in error or was aborted") by flipping it here.
76
+ if (opts.signal?.aborted)
77
+ isError = true;
73
78
  if (summary)
74
79
  steps.push({ kind: 'done', summary });
75
80
  return { steps, summary, isError };
@@ -14,13 +14,9 @@ import type { WebSocket } from 'ws';
14
14
  import { type LaunchOptions } from '../playwright/launchChrome.js';
15
15
  import { type ClientMessage } from './types.js';
16
16
  /** Extra launch options surfaced from the active mode (security plugin
17
- * needs proxy + spki + separate profile + non-default CDP port). When
18
- * none are set, behaviour is identical to pre-v0.7 normal-mode launch. */
19
- export type LaunchExtras = Pick<LaunchOptions, 'userDataDir' | 'proxy'> & {
20
- /** Override CDP port (mode-specific, e.g. 9333 for security). When set,
21
- * this also wins over the `port` parsed from cdpUrl. */
22
- cdpPort?: number;
23
- };
17
+ * needs a resident proxy + spki). When none are set, behaviour is identical
18
+ * to pre-v0.7 normal-mode launch. */
19
+ export type LaunchExtras = Pick<LaunchOptions, 'proxy'>;
24
20
  /**
25
21
  * "Is this widget running inside the debug Chrome?" The widget asks this on
26
22
  * connect (and after every status-changing event) so it can render itself as
@@ -1 +1 @@
1
- {"version":3,"file":"cdpHandlers.d.ts","sourceRoot":"","sources":["../../src/service/cdpHandlers.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,IAAI,CAAC;AAEpC,OAAO,EAAqB,KAAK,aAAa,EAAE,MAAM,+BAA+B,CAAC;AACtF,OAAO,EAAQ,KAAK,aAAa,EAAE,MAAM,YAAY,CAAC;AAEtD;;2EAE2E;AAC3E,MAAM,MAAM,YAAY,GAAG,IAAI,CAAC,aAAa,EAAE,aAAa,GAAG,OAAO,CAAC,GAAG;IACxE;6DACyD;IACzD,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB,CAAC;AAEF;;;;;;;GAOG;AACH,wBAAsB,cAAc,CAClC,EAAE,EAAE,SAAS,EACb,GAAG,EAAE,aAAa,EAClB,MAAM,EAAE,MAAM,EACd,MAAM,CAAC,EAAE,YAAY,GACpB,OAAO,CAAC,IAAI,CAAC,CAWf;AAED;;;;;GAKG;AACH,wBAAsB,kBAAkB,CACtC,EAAE,EAAE,SAAS,EACb,GAAG,EAAE,aAAa,EAClB,MAAM,EAAE,MAAM,EACd,MAAM,CAAC,EAAE,YAAY,GACpB,OAAO,CAAC,IAAI,CAAC,CAkCf;AAED;;;;;;GAMG;AACH,wBAAsB,gBAAgB,CACpC,EAAE,EAAE,SAAS,EACb,GAAG,EAAE,aAAa,EAClB,MAAM,EAAE,MAAM,EACd,MAAM,CAAC,EAAE,YAAY,GACpB,OAAO,CAAC,IAAI,CAAC,CAaf"}
1
+ {"version":3,"file":"cdpHandlers.d.ts","sourceRoot":"","sources":["../../src/service/cdpHandlers.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,IAAI,CAAC;AAEpC,OAAO,EAAqB,KAAK,aAAa,EAAE,MAAM,+BAA+B,CAAC;AACtF,OAAO,EAAQ,KAAK,aAAa,EAAE,MAAM,YAAY,CAAC;AAEtD;;sCAEsC;AACtC,MAAM,MAAM,YAAY,GAAG,IAAI,CAAC,aAAa,EAAE,OAAO,CAAC,CAAC;AAExD;;;;;;;GAOG;AACH,wBAAsB,cAAc,CAClC,EAAE,EAAE,SAAS,EACb,GAAG,EAAE,aAAa,EAClB,MAAM,EAAE,MAAM,EACd,MAAM,CAAC,EAAE,YAAY,GACpB,OAAO,CAAC,IAAI,CAAC,CAQf;AAED;;;;;GAKG;AACH,wBAAsB,kBAAkB,CACtC,EAAE,EAAE,SAAS,EACb,GAAG,EAAE,aAAa,EAClB,MAAM,EAAE,MAAM,EACd,MAAM,CAAC,EAAE,YAAY,GACpB,OAAO,CAAC,IAAI,CAAC,CA4Bf;AAED;;;;;;GAMG;AACH,wBAAsB,gBAAgB,CACpC,EAAE,EAAE,SAAS,EACb,GAAG,EAAE,aAAa,EAClB,MAAM,EAAE,MAAM,EACd,MAAM,CAAC,EAAE,YAAY,GACpB,OAAO,CAAC,IAAI,CAAC,CAUf"}
@@ -27,10 +27,7 @@ export async function handleCheckCdp(ws, msg, cdpUrl, extras) {
27
27
  send(ws, { type: 'error', payload: { message: 'check-cdp: pageUrl is required' } });
28
28
  return;
29
29
  }
30
- const effectiveCdpUrl = extras?.cdpPort
31
- ? `http://localhost:${extras.cdpPort}`
32
- : cdpUrl;
33
- const status = await checkCdpStatus(effectiveCdpUrl, pageUrl);
30
+ const status = await checkCdpStatus(cdpUrl, pageUrl);
34
31
  send(ws, { type: 'cdp-status', payload: status });
35
32
  }
36
33
  /**
@@ -48,7 +45,7 @@ export async function handleLaunchChrome(ws, msg, cdpUrl, extras) {
48
45
  // Tell the widget we're launching so it can render a spinner immediately —
49
46
  // findChromeBinary + spawn + ready-poll can take a few seconds.
50
47
  send(ws, { type: 'cdp-status', payload: { state: 'no-cdp', launching: true } });
51
- const port = extras?.cdpPort ?? (() => {
48
+ const port = (() => {
52
49
  try {
53
50
  return Number(new URL(cdpUrl).port) || 9222;
54
51
  }
@@ -59,19 +56,13 @@ export async function handleLaunchChrome(ws, msg, cdpUrl, extras) {
59
56
  const result = await launchDebugChrome({
60
57
  url: pageUrl,
61
58
  port,
62
- userDataDir: extras?.userDataDir,
63
59
  proxy: extras?.proxy,
64
60
  });
65
61
  if (!result.ok) {
66
62
  send(ws, { type: 'cdp-status', payload: { state: 'no-cdp', reason: result.reason } });
67
63
  return;
68
64
  }
69
- // Re-check status against the port we actually launched on, so a mode-
70
- // specific port (9333 for security) doesn't get probed at 9222.
71
- const effectiveCdpUrl = extras?.cdpPort
72
- ? `http://localhost:${extras.cdpPort}`
73
- : cdpUrl;
74
- const status = await checkCdpStatus(effectiveCdpUrl, pageUrl);
65
+ const status = await checkCdpStatus(cdpUrl, pageUrl);
75
66
  send(ws, { type: 'cdp-status', payload: status });
76
67
  }
77
68
  /**
@@ -87,10 +78,7 @@ export async function handleFocusDebug(ws, msg, cdpUrl, extras) {
87
78
  send(ws, { type: 'error', payload: { message: 'focus-debug: pageUrl is required' } });
88
79
  return;
89
80
  }
90
- const effectiveCdpUrl = extras?.cdpPort
91
- ? `http://localhost:${extras.cdpPort}`
92
- : cdpUrl;
93
- const result = await focusDebugTab(effectiveCdpUrl, pageUrl);
81
+ const result = await focusDebugTab(cdpUrl, pageUrl);
94
82
  if (!result.ok) {
95
83
  send(ws, { type: 'error', payload: { message: `focus-debug: ${result.reason}` } });
96
84
  }
package/dist/service.d.ts CHANGED
@@ -29,6 +29,12 @@ export interface ServiceOptions {
29
29
  * see the proxy; moving it here is what enables the single-Chrome model.
30
30
  * Default false (shims pass it through from their own option). */
31
31
  autoLaunchChrome?: boolean;
32
+ /** Opt-in: give the agent READ-ONLY, fenced access to the project's source
33
+ * via a `read_source` / `list_source` MCP server (in addition to Playwright
34
+ * MCP), in every mode. Lets it author against real selectors/routes and do
35
+ * white-box security/pentest. Fenced to devRoot, secrets/keys/.git/build
36
+ * excluded, no write/exec. Default false — the agent stays browser-only. */
37
+ codeContext?: boolean;
32
38
  /** The dev-server URL the auto-launched Chrome should open. Each shim knows
33
39
  * its own framework's dev URL and passes it here. Defaults to the cdp host
34
40
  * if unset, but shims should always provide it. */
@@ -1 +1 @@
1
- {"version":3,"file":"service.d.ts","sourceRoot":"","sources":["../src/service.ts"],"names":[],"mappings":"AA6EA,OAAO,EAEL,KAAK,mBAAmB,EAEzB,MAAM,iBAAiB,CAAC;AAEzB,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB;oFACgF;IAChF,YAAY,CAAC,EAAE,KAAK,GAAG,SAAS,GAAG,IAAI,CAAC;IACxC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,gFAAgF;IAChF,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;;;gFAG4E;IAC5E,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB;;;;uBAImB;IACnB,OAAO,CAAC,EAAE,mBAAmB,EAAE,CAAC;IAChC;;;;;;uEAMmE;IACnE,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B;;wDAEoD;IACpD,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,aAAa;IAC5B;4EACwE;IACxE,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACxB;AAiED,wBAAsB,YAAY,CAAC,IAAI,EAAE,cAAc,GAAG,OAAO,CAAC,aAAa,CAAC,CA64B/E"}
1
+ {"version":3,"file":"service.d.ts","sourceRoot":"","sources":["../src/service.ts"],"names":[],"mappings":"AA+EA,OAAO,EAEL,KAAK,mBAAmB,EAEzB,MAAM,iBAAiB,CAAC;AAQzB,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB;oFACgF;IAChF,YAAY,CAAC,EAAE,KAAK,GAAG,SAAS,GAAG,IAAI,CAAC;IACxC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,gFAAgF;IAChF,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;;;gFAG4E;IAC5E,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB;;;;uBAImB;IACnB,OAAO,CAAC,EAAE,mBAAmB,EAAE,CAAC;IAChC;;;;;;uEAMmE;IACnE,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B;;;;iFAI6E;IAC7E,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB;;wDAEoD;IACpD,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,aAAa;IAC5B;4EACwE;IACxE,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACxB;AAiED,wBAAsB,YAAY,CAAC,IAAI,EAAE,cAAc,GAAG,OAAO,CAAC,aAAa,CAAC,CA49B/E"}
package/dist/service.js CHANGED
@@ -46,6 +46,8 @@
46
46
  * { type: 'list-modes' }
47
47
  */
48
48
  import { WebSocketServer, WebSocket } from 'ws';
49
+ import { fileURLToPath } from 'node:url';
50
+ import { dirname, resolve } from 'node:path';
49
51
  import { runSession } from './runSession.js';
50
52
  import { readConventions } from './service/conventions.js';
51
53
  import { optimizeSpecWithAgent } from './specs/optimizeSpecWithAgent.js';
@@ -53,7 +55,7 @@ import { promoteOptimized, discardOptimized } from './specs/optimizeSpec.js';
53
55
  import { listAgentAvailability, pickPrimaryAgent, } from './agents/detect.js';
54
56
  import { getAgent } from './agents/registry.js';
55
57
  import { getPreflight, invalidatePreflight } from './playwright/preflightCache.js';
56
- import { resolveMcpConfig } from './playwright/resolveMcpConfig.js';
58
+ import { resolveMcpConfig, mcpToolPrefix } from './playwright/resolveMcpConfig.js';
57
59
  import { launchDebugChrome } from './playwright/launchChrome.js';
58
60
  import { listSpecs } from './specs/listSpecs.js';
59
61
  import { readSeeds, BUILTIN_SEEDS } from './specs/seeds.js';
@@ -62,6 +64,11 @@ import { buildCdpHint, buildCdpHintResume } from './service/cdpHint.js';
62
64
  import { handleCheckCdp, handleLaunchChrome, handleFocusDebug, } from './service/cdpHandlers.js';
63
65
  import { handleSaveArtifact, SPEC_CONFIG, CASE_CSV_CONFIG, } from './service/saveHandlers.js';
64
66
  import { CURRENT_API_VERSION, } from './plugin-api.js';
67
+ /** The source-reader MCP server (codeContext). Id → the `mcp__hover_source`
68
+ * tool prefix; script path resolved relative to this module so it works from
69
+ * dist/. Spawned only when codeContext is enabled. */
70
+ const SOURCE_MCP_ID = 'hover-source';
71
+ const SOURCE_MCP_SCRIPT = resolve(dirname(fileURLToPath(import.meta.url)), 'mcp', 'sourceServer.js');
65
72
  // ClientMessage + send moved to ./service/types.ts so the cdp + save
66
73
  // handler modules can share them. See those files for the wire shape.
67
74
  const PROTOCOL_VERSION = 1;
@@ -195,6 +202,15 @@ export async function startService(opts) {
195
202
  }
196
203
  }
197
204
  }
205
+ // codeContext (opt-in, all modes): the fenced read-only source reader.
206
+ if (opts.codeContext) {
207
+ extra.push({
208
+ id: SOURCE_MCP_ID,
209
+ command: process.execPath,
210
+ args: [SOURCE_MCP_SCRIPT],
211
+ env: { HOVER_PROJECT_ROOT: devRoot },
212
+ });
213
+ }
198
214
  // Single-Chrome model: the Playwright MCP always points at the one debug
199
215
  // Chrome on the normal cdpUrl. (Pre-single-Chrome this branched to a
200
216
  // mode-specific port like 9333; there's no second Chrome anymore.)
@@ -239,6 +255,25 @@ export async function startService(opts) {
239
255
  }
240
256
  /** id of the currently-active mode, or null for normal (unmoded) mode. */
241
257
  let currentModeId = null;
258
+ /**
259
+ * The single in-flight agent run, held at SERVICE scope (not per-connection)
260
+ * so it SURVIVES the widget's WS dropping. The widget lives in the page the
261
+ * agent drives, so any agent navigation (a pentest payload in the URL, an
262
+ * HMR reload) tears the widget down and closes its socket — but the agent is
263
+ * still happily driving the tab over CDP and recording findings server-side.
264
+ * Killing it on every navigation made pentest mode (which navigates
265
+ * constantly) unusable. Instead: detach on close, keep streaming to whichever
266
+ * ws is attached, and only abort if no widget reconnects within the grace
267
+ * window. Single active run — Hover binds 127.0.0.1 for one local user.
268
+ */
269
+ const RECONNECT_GRACE_MS = 15_000;
270
+ let activeRun = null;
271
+ /** Send a run event to whichever ws is currently attached (survives reconnect). */
272
+ const emitToRun = (msg) => {
273
+ const c = activeRun?.client;
274
+ if (c && c.readyState === WebSocket.OPEN)
275
+ send(c, msg);
276
+ };
242
277
  /** Chrome-proxy settings a plugin's `hover:service:start` hook set on us
243
278
  * (security's resident MITM). RESIDENT for the whole session — set once
244
279
  * before Chrome launches, never cleared on mode change — so the single
@@ -273,6 +308,9 @@ export async function startService(opts) {
273
308
  id: p.mode.id,
274
309
  label: p.mode.label,
275
310
  description: p.mode.description,
311
+ // Widget retints to this while the mode is engaged (falls back to
312
+ // security orange in the widget when absent).
313
+ accent: p.mode.accent,
276
314
  pluginName: p.name,
277
315
  }));
278
316
  const payload = { current: currentModeId, available };
@@ -410,20 +448,42 @@ export async function startService(opts) {
410
448
  // Send the mode catalogue too, so the widget can render the mode
411
449
  // toggle immediately. Empty list when no plugins are loaded.
412
450
  broadcastModes(ws);
413
- let busy = false;
414
- let inflight = null;
415
- let cancelled = false;
416
- // If the page reloads (e.g. AI navigated to a same-origin URL), the WS
417
- // connection drops. Abort the in-flight agent so we don't leave an
418
- // orphan claude process driving the now-vanished browser tab.
451
+ // Re-attach to a run that's still in flight (the previous widget dropped —
452
+ // most commonly the agent navigated and reloaded the page the widget lives
453
+ // in). Cancel the pending abort, point the run's event stream at this fresh
454
+ // socket, and tell the widget so it can restore its "running" UI. Without
455
+ // this the run would be killed on every agent navigation.
456
+ // Only re-attach during a genuine reconnect GAP (the prior client is gone).
457
+ // If a live client is still attached, this is a SECOND widget (e.g. the
458
+ // user's regular tab alongside the debug-Chrome tab — both inject a widget
459
+ // on the same origin and open their own socket). Seizing the stream would
460
+ // silence the first widget and let the second's close abort a healthy run,
461
+ // so leave a second concurrent widget in idle UI rather than hijacking.
462
+ if (activeRun && activeRun.client === null) {
463
+ if (activeRun.graceTimer) {
464
+ clearTimeout(activeRun.graceTimer);
465
+ activeRun.graceTimer = null;
466
+ }
467
+ activeRun.client = ws;
468
+ send(ws, { type: 'run-active', payload: { prompt: activeRun.prompt } });
469
+ }
470
+ // If the widget's socket closes while a run it owns is in flight, DON'T
471
+ // abort — the agent is still driving the tab over CDP. Detach this ws and
472
+ // start a grace window; a reconnecting widget (above) cancels the abort.
473
+ // Only if nobody comes back do we abort, so we still never leave an orphan.
419
474
  ws.on('close', () => {
420
- inflight?.abort();
475
+ if (activeRun && activeRun.client === ws) {
476
+ activeRun.client = null;
477
+ activeRun.graceTimer = setTimeout(() => {
478
+ activeRun?.abort.abort();
479
+ }, RECONNECT_GRACE_MS);
480
+ }
421
481
  });
422
482
  const cancel = () => {
423
- if (!busy)
483
+ if (!activeRun)
424
484
  return;
425
- cancelled = true;
426
- inflight?.abort();
485
+ activeRun.cancelled = true;
486
+ activeRun.abort.abort();
427
487
  // Send a synthetic session_end so the widget resets to idle immediately.
428
488
  // The for-await loop below short-circuits on `cancelled`, so no events
429
489
  // from the dying child will arrive after this.
@@ -433,7 +493,7 @@ export async function startService(opts) {
433
493
  // stays false because the agent didn't fail: the user chose to
434
494
  // end the run. The widget renders this as a neutral "Stopped"
435
495
  // state rather than a red Failed card.
436
- send(ws, {
496
+ emitToRun({
437
497
  type: 'event',
438
498
  payload: {
439
499
  kind: 'session_end',
@@ -460,7 +520,7 @@ export async function startService(opts) {
460
520
  return;
461
521
  }
462
522
  if (msg.type === 'set-mode') {
463
- if (busy) {
523
+ if (activeRun) {
464
524
  send(ws, {
465
525
  type: 'error',
466
526
  payload: { message: 'set-mode: a command is already running; stop it first' },
@@ -515,7 +575,7 @@ export async function startService(opts) {
515
575
  // Refuse to switch mid-flight; the user's running command would
516
576
  // otherwise outlive its own descriptor and the events it produces
517
577
  // would be parsed against the wrong wire format.
518
- if (busy) {
578
+ if (activeRun) {
519
579
  send(ws, {
520
580
  type: 'error',
521
581
  payload: { message: 'switch-agent: a command is already running; stop it first' },
@@ -690,16 +750,21 @@ export async function startService(opts) {
690
750
  : undefined;
691
751
  if (typeof text !== 'string' || !text.trim())
692
752
  return;
693
- if (busy) {
753
+ if (activeRun) {
694
754
  send(ws, {
695
755
  type: 'error',
696
- payload: { message: 'A command is already running on this connection.' },
756
+ payload: { message: 'A command is already running.' },
697
757
  });
698
758
  return;
699
759
  }
700
- busy = true;
701
- cancelled = false;
702
- inflight = new AbortController();
760
+ const run = {
761
+ abort: new AbortController(),
762
+ cancelled: false,
763
+ client: ws,
764
+ graceTimer: null,
765
+ prompt: text,
766
+ };
767
+ activeRun = run;
703
768
  try {
704
769
  // Build the MCP config first — it's pure local file IO and lets
705
770
  // us assert plugin-contributed servers landed in the config even
@@ -710,13 +775,7 @@ export async function startService(opts) {
710
775
  // Playwright MCP server would silently launch its own Chromium —
711
776
  // and Hover's premise is to drive the user's existing Chrome (with
712
777
  // their dev state, cookies, devtools open), never spawn a fresh one.
713
- // In an active mode, the relevant CDP endpoint may be the mode's
714
- // own port (e.g. 9333 for security), not the default cdpUrl.
715
- const preflightExtras = effectiveLaunchExtras();
716
- const preflightCdpUrl = preflightExtras?.cdpPort
717
- ? `http://localhost:${preflightExtras.cdpPort}`
718
- : cdpUrl;
719
- const cdp = await getPreflight(preflightCdpUrl);
778
+ const cdp = await getPreflight(cdpUrl);
720
779
  if (!cdp.ok) {
721
780
  send(ws, {
722
781
  type: 'event',
@@ -771,6 +830,13 @@ export async function startService(opts) {
771
830
  }
772
831
  }
773
832
  }
833
+ // codeContext: tell the agent the fenced source reader exists, so it
834
+ // proactively reads the real code (better selectors/routes when
835
+ // authoring; white-box confirmation when probing) instead of only
836
+ // guessing from the rendered DOM.
837
+ if (opts.codeContext) {
838
+ appendSystemPrompt = `${appendSystemPrompt}\n\nYou also have read-only access to this project's source via mcp__hover_source (read_source / list_source), fenced to the repo (secrets, keys, .env, .git, node_modules and build output are refused). Use it to read the actual component / route / API code — write tests against the real selectors and, when probing for security issues, confirm a finding against the server code (the query, the authz check) rather than guessing from the page alone.`;
839
+ }
774
840
  // Mirror the prompt's language in the agent's *prose* output — the
775
841
  // verification summary (Result card), the ## Findings block, and the
776
842
  // step narration — the same way Voice mode mirrors it in TTS. A
@@ -784,8 +850,8 @@ export async function startService(opts) {
784
850
  }
785
851
  // Snapshot the agent id so a switch-agent message during the run
786
852
  // can't smear two agents across one invocation. (We also gate
787
- // switch-agent on `busy`, but defense in depth.) runSession gates the
788
- // allow/deny lists on the agent's sandboxStrength internally.
853
+ // switch-agent on an active run, but defense in depth.) runSession gates
854
+ // the allow/deny lists on the agent's sandboxStrength internally.
789
855
  const invokedAgentId = currentAgentId;
790
856
  // Active mode's plugin-contributed MCP server ids — added to the
791
857
  // hard-sandbox allow list so Claude can actually call them. Claude
@@ -794,18 +860,20 @@ export async function startService(opts) {
794
860
  // and `--allowedTools mcp__foo` matches every tool under that
795
861
  // prefix. We pass the prefix `mcp__<sanitized>` so all of the
796
862
  // server's tools are reachable.
797
- const sanitize = (s) => s.replace(/[^a-zA-Z0-9]+/g, '_').replace(/^_+|_+$/g, '');
798
863
  const activePluginMcpIds = [];
799
864
  if (currentModeId) {
800
865
  for (const p of plugins) {
801
866
  for (const srv of p.mcpServers ?? []) {
802
867
  const scope = srv.activeInModes ?? (p.mode ? [p.mode.id] : []);
803
868
  if (scope.includes('*') || scope.includes(currentModeId)) {
804
- activePluginMcpIds.push(`mcp__${sanitize(srv.id)}`);
869
+ activePluginMcpIds.push(mcpToolPrefix(srv.id));
805
870
  }
806
871
  }
807
872
  }
808
873
  }
874
+ // codeContext: the fenced source reader is allowed in every mode.
875
+ if (opts.codeContext)
876
+ activePluginMcpIds.push(mcpToolPrefix(SOURCE_MCP_ID));
809
877
  const runResult = await runSession({
810
878
  agentId: invokedAgentId,
811
879
  prompt: text,
@@ -821,19 +889,21 @@ export async function startService(opts) {
821
889
  maxBudgetUsd,
822
890
  model,
823
891
  apiKey: currentApiKey,
824
- signal: inflight.signal,
892
+ signal: run.abort.signal,
825
893
  }, (ev) => {
826
- if (cancelled || ws.readyState !== WebSocket.OPEN)
894
+ // Stream to whichever ws is attached NOW — survives the widget
895
+ // reconnecting mid-run (emitToRun is a no-op during a reconnect gap).
896
+ if (run.cancelled)
827
897
  return;
828
- send(ws, { type: 'event', payload: ev });
898
+ emitToRun({ type: 'event', payload: ev });
829
899
  });
830
900
  // Re-record: write a fresh spec from the steps runSession accumulated
831
901
  // (`user` → `step`* → `done`). Only on a clean, non-cancelled finish —
832
902
  // a cancelled/aborted run throws out of runSession into the catch
833
903
  // below, and an errored agent leaves the original spec untouched.
834
- if (reRecordSlug && !cancelled) {
904
+ if (reRecordSlug && !run.cancelled) {
835
905
  if (runResult.isError) {
836
- sendIfOpen(ws, {
906
+ emitToRun({
837
907
  type: 'error',
838
908
  payload: {
839
909
  message: `Re-record failed: ${runResult.summary || 'agent reported an error'}. ` +
@@ -850,14 +920,14 @@ export async function startService(opts) {
850
920
  steps: runResult.steps,
851
921
  overwrite: true,
852
922
  });
853
- sendIfOpen(ws, {
923
+ emitToRun({
854
924
  type: 'spec-saved',
855
925
  payload: { name: reRecordSlug, path: written.path },
856
926
  });
857
927
  }
858
928
  catch (e) {
859
929
  const m = e instanceof Error ? e.message : String(e);
860
- sendIfOpen(ws, {
930
+ emitToRun({
861
931
  type: 'error',
862
932
  payload: { message: `Re-record could not write spec: ${m}` },
863
933
  });
@@ -872,30 +942,25 @@ export async function startService(opts) {
872
942
  // widget to reconcile two terminal events for one run. CDP isn't
873
943
  // suspect either — the user just stopped — so skip preflight
874
944
  // invalidation too.
875
- if (!cancelled) {
945
+ if (!run.cancelled) {
876
946
  const message = err instanceof Error ? err.message : String(err);
877
947
  const errorEvent = {
878
948
  kind: 'session_end',
879
949
  isError: true,
880
950
  summary: message,
881
951
  };
882
- sendIfOpen(ws, { type: 'event', payload: errorEvent });
952
+ emitToRun({ type: 'event', payload: errorEvent });
883
953
  // Force the next command to re-probe CDP. The error could be from
884
954
  // Chrome dying, MCP spawning a stray Chromium, the user closing
885
955
  // their debug window — anything that would make a cached "all
886
- // healthy" result lie. Invalidate the mode-effective URL (see
887
- // preflightCdpUrl above) — not the static cdpUrl — so security
888
- // mode invalidations don't no-op against the default port.
889
- const invalExtras = effectiveLaunchExtras();
890
- const invalCdpUrl = invalExtras?.cdpPort
891
- ? `http://localhost:${invalExtras.cdpPort}`
892
- : cdpUrl;
893
- invalidatePreflight(invalCdpUrl);
956
+ // healthy" result lie.
957
+ invalidatePreflight(cdpUrl);
894
958
  }
895
959
  }
896
960
  finally {
897
- busy = false;
898
- inflight = null;
961
+ if (run.graceTimer)
962
+ clearTimeout(run.graceTimer);
963
+ activeRun = null;
899
964
  }
900
965
  });
901
966
  });
@@ -954,6 +1019,20 @@ export async function startService(opts) {
954
1019
  return {
955
1020
  port,
956
1021
  async close() {
1022
+ // Kill any in-flight run FIRST. The run is held at service scope and is
1023
+ // only torn down by aborting its signal (invoke.ts SIGTERMs the agent
1024
+ // child on abort). wss.close() below stops the listener but does NOT
1025
+ // terminate established client sockets, so no ws.on('close') fires — so
1026
+ // without this the agent child would keep driving the debug Chrome as an
1027
+ // orphan after the dev server is gone, and a pending grace timer would
1028
+ // fire abort() 15s into the void.
1029
+ if (activeRun) {
1030
+ if (activeRun.graceTimer)
1031
+ clearTimeout(activeRun.graceTimer);
1032
+ activeRun.cancelled = true;
1033
+ activeRun.abort.abort();
1034
+ activeRun = null;
1035
+ }
957
1036
  // Deactivate the active mode first, then run every plugin's
958
1037
  // shutdown hook (regardless of which mode is active — a plugin may
959
1038
  // own background state even outside its mode). Best-effort: log
@@ -1 +1 @@
1
- {"version":3,"file":"optimizeSpec.d.ts","sourceRoot":"","sources":["../../src/specs/optimizeSpec.ts"],"names":[],"mappings":"AAcA,OAAO,EAAc,KAAK,WAAW,EAAE,MAAM,cAAc,CAAC;AAC5D,OAAO,EAA4B,KAAK,QAAQ,EAAE,MAAM,YAAY,CAAC;AAErE,qBAAa,aAAc,SAAQ,KAAK;gBAC1B,OAAO,EAAE,MAAM;CAI5B;AAED,wEAAwE;AACxE,MAAM,MAAM,UAAU,GAAG,CAAC,MAAM,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;AAE7D,MAAM,WAAW,cAAc;IAC7B,wEAAwE;IACxE,aAAa,EAAE,MAAM,CAAC;IACtB,sCAAsC;IACtC,IAAI,EAAE,MAAM,CAAC;IACb;2EACuE;IACvE,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,wBAAsB,YAAY,CAChC,OAAO,EAAE,MAAM,EACf,IAAI,EAAE,MAAM,EACZ,UAAU,EAAE,UAAU,GACrB,OAAO,CAAC,cAAc,CAAC,CAsCzB;AAED;;;;GAIG;AACH,wBAAgB,mBAAmB,CACjC,KAAK,EAAE,MAAM,EACb,OAAO,EAAE,WAAW,GAAG,IAAI,EAC3B,KAAK,GAAE,QAAQ,EAAO,GACrB,MAAM,CAmDR;AAED,kEAAkE;AAClE,wBAAgB,WAAW,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAI/C;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG;IAAE,EAAE,EAAE,OAAO,CAAC;IAAC,MAAM,EAAE,MAAM,EAAE,CAAA;CAAE,CAahF;AAMD;;gCAEgC;AAChC,wBAAsB,gBAAgB,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAYrF;AAED;+BAC+B;AAC/B,wBAAsB,gBAAgB,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAEnF"}
1
+ {"version":3,"file":"optimizeSpec.d.ts","sourceRoot":"","sources":["../../src/specs/optimizeSpec.ts"],"names":[],"mappings":"AAeA,OAAO,EAAc,KAAK,WAAW,EAAE,MAAM,cAAc,CAAC;AAC5D,OAAO,EAA4B,KAAK,QAAQ,EAAE,MAAM,YAAY,CAAC;AAGrE,qBAAa,aAAc,SAAQ,KAAK;gBAC1B,OAAO,EAAE,MAAM;CAI5B;AAED,wEAAwE;AACxE,MAAM,MAAM,UAAU,GAAG,CAAC,MAAM,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;AAE7D,MAAM,WAAW,cAAc;IAC7B,wEAAwE;IACxE,aAAa,EAAE,MAAM,CAAC;IACtB,sCAAsC;IACtC,IAAI,EAAE,MAAM,CAAC;IACb;2EACuE;IACvE,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,wBAAsB,YAAY,CAChC,OAAO,EAAE,MAAM,EACf,IAAI,EAAE,MAAM,EACZ,UAAU,EAAE,UAAU,GACrB,OAAO,CAAC,cAAc,CAAC,CA2CzB;AAED;;;;GAIG;AACH,wBAAgB,mBAAmB,CACjC,KAAK,EAAE,MAAM,EACb,OAAO,EAAE,WAAW,GAAG,IAAI,EAC3B,KAAK,GAAE,QAAQ,EAAO,GACrB,MAAM,CAmDR;AAED,kEAAkE;AAClE,wBAAgB,WAAW,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAI/C;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG;IAAE,EAAE,EAAE,OAAO,CAAC;IAAC,MAAM,EAAE,MAAM,EAAE,CAAA;CAAE,CAWhF;AAyBD;;gCAEgC;AAChC,wBAAsB,gBAAgB,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAYrF;AAED;+BAC+B;AAC/B,wBAAsB,gBAAgB,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAEnF"}
@@ -12,8 +12,10 @@
12
12
  */
13
13
  import { readFile, mkdir, writeFile, rm } from 'node:fs/promises';
14
14
  import { join } from 'node:path';
15
+ import { Project } from 'ts-morph';
15
16
  import { sidecarDir } from './sidecar.js';
16
17
  import { readSeeds, relevantSeeds } from './seeds.js';
18
+ import { softBatch } from './softBatch.js';
17
19
  export class OptimizeError extends Error {
18
20
  constructor(message) {
19
21
  super(message);
@@ -41,11 +43,15 @@ export async function optimizeSpec(devRoot, slug, runCodegen) {
41
43
  .map(s => s.tool));
42
44
  const seeds = relevantSeeds(await readSeeds(devRoot), specTools);
43
45
  const raw = await runCodegen(buildOptimizePrompt(draft, sidecar, seeds));
44
- const code = extractCode(raw);
45
- const check = validateSpecCode(code);
46
+ const llmCode = extractCode(raw);
47
+ const check = validateSpecCode(llmCode);
46
48
  if (!check.ok) {
47
49
  throw new OptimizeError(`optimization rejected — ${check.errors.join('; ')}`);
48
50
  }
51
+ // Deterministic finishing step: the LLM decided WHAT to assert; soft-batch
52
+ // applies the safe mechanical rewrite (trailing run of independent assertions
53
+ // → expect.soft) surgically on its output. See softBatch.ts for the guard.
54
+ const code = softBatch(llmCode).code;
49
55
  const dir = join(devRoot, '__vibe_tests__', '.hover', 'optimized');
50
56
  await mkdir(dir, { recursive: true });
51
57
  // `.spec.ts.draft`, never `*.spec.ts` — Playwright's glob must not collect a
@@ -133,12 +139,28 @@ export function validateSpecCode(code) {
133
139
  if (!/from\s+['"](@playwright\/test|\.\/fixtures)['"]/.test(code)) {
134
140
  errors.push('missing @playwright/test (or ./fixtures) import');
135
141
  }
136
- const open = (code.match(/\{/g) ?? []).length;
137
- const close = (code.match(/\}/g) ?? []).length;
138
- if (open !== close)
139
- errors.push('unbalanced braces');
142
+ if (hasSyntaxError(code))
143
+ errors.push('has a syntax error');
140
144
  return { ok: errors.length === 0, errors };
141
145
  }
146
+ /**
147
+ * Real syntax check via the TypeScript parser (the same ts-morph the soft-batch
148
+ * step uses). Replaces a naive `{`/`}` count that mis-flagged a valid spec
149
+ * asserting on a string like 'a { b' — braces inside string literals are not
150
+ * structural. We look at SYNTACTIC diagnostics only: a candidate references
151
+ * `page` / `expect` / `@playwright/test` that aren't resolvable in this throwaway
152
+ * project, so SEMANTIC ("cannot find module", "implicitly any") diagnostics are
153
+ * expected and must be ignored — only a genuine parse error (an unbalanced
154
+ * brace, a stray token) should reject the optimization.
155
+ */
156
+ function hasSyntaxError(code) {
157
+ const project = new Project({
158
+ useInMemoryFileSystem: true,
159
+ compilerOptions: { allowJs: true },
160
+ });
161
+ const sf = project.createSourceFile('__candidate.ts', code, { overwrite: true });
162
+ return project.getProgram().getSyntacticDiagnostics(sf).length > 0;
163
+ }
142
164
  function candidatePathFor(devRoot, slug) {
143
165
  return join(devRoot, '__vibe_tests__', '.hover', 'optimized', `${slug}.spec.ts.draft`);
144
166
  }
@@ -0,0 +1,14 @@
1
+ /** A trailing run with fewer than this many assertions is left alone —
2
+ * `expect.soft` only earns its keep when ≥2 failures could be collected. */
3
+ export declare const MIN_RUN = 2;
4
+ export interface SoftBatchResult {
5
+ /** The (possibly) rewritten source. */
6
+ code: string;
7
+ /** Whether anything changed. */
8
+ changed: boolean;
9
+ /** How many assertions were softened across all tests. */
10
+ softened: number;
11
+ }
12
+ /** Run the soft-batch step over a spec's source text. Pure: text in, text out. */
13
+ export declare function softBatch(source: string): SoftBatchResult;
14
+ //# sourceMappingURL=softBatch.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"softBatch.d.ts","sourceRoot":"","sources":["../../src/specs/softBatch.ts"],"names":[],"mappings":"AAgCA;6EAC6E;AAC7E,eAAO,MAAM,OAAO,IAAI,CAAC;AAEzB,MAAM,WAAW,eAAe;IAC9B,uCAAuC;IACvC,IAAI,EAAE,MAAM,CAAC;IACb,gCAAgC;IAChC,OAAO,EAAE,OAAO,CAAC;IACjB,0DAA0D;IAC1D,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,kFAAkF;AAClF,wBAAgB,SAAS,CAAC,MAAM,EAAE,MAAM,GAAG,eAAe,CAuBzD"}