@aayambansal/squint 0.4.7 → 0.5.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 (29) hide show
  1. package/README.md +6 -0
  2. package/dist/{App-HWMVSRMF.js → App-MC2XEM7K.js} +9 -9
  3. package/dist/{cdp-PBQKXC6R.js → cdp-WSWOSDEX.js} +3 -1
  4. package/dist/{chrome-SBV3H77F.js → chrome-EEXJCG6W.js} +2 -2
  5. package/dist/{chunk-QCHBDP46.js → chunk-2I3YHT6P.js} +3 -3
  6. package/dist/{flows-6LK7NGWS.js → chunk-4XSHHGA6.js} +2 -1
  7. package/dist/{chunk-VH7OOFQP.js → chunk-6NPUVPQD.js} +2 -2
  8. package/dist/{contextDoctor-A26C2ZDN.js → chunk-BKOGVFCO.js} +1 -0
  9. package/dist/{chunk-ZSDCMYZR.js → chunk-CRDRWTON.js} +24 -7
  10. package/dist/{chunk-HC4E42SV.js → chunk-DCFNX2CQ.js} +228 -3
  11. package/dist/{chunk-M25IQ7C7.js → chunk-EB4OSUB7.js} +1 -1
  12. package/dist/{chunk-KVYGPLWW.js → chunk-GV2HAALP.js} +4 -0
  13. package/dist/{chunk-YGSF2TSO.js → chunk-J7ITMK6X.js} +1 -1
  14. package/dist/{chunk-IMDRXXFU.js → chunk-PYC53J7D.js} +1 -1
  15. package/dist/{chunk-7MOKOZOR.js → chunk-WASLLXEZ.js} +31 -18
  16. package/dist/{chunk-NX7XQY2X.js → chunk-YK5DEB7T.js} +1 -1
  17. package/dist/cli.js +99 -22
  18. package/dist/contextDoctor-3Y36YURE.js +11 -0
  19. package/dist/flows-MXJL45HW.js +11 -0
  20. package/dist/{preview-WT34NVCH.js → preview-CDEMPHWV.js} +6 -4
  21. package/dist/{registry-MIJ6LSAY.js → registry-JBP7J3JB.js} +1 -1
  22. package/dist/{remote-6O6FPTUF.js → remote-C4BACCKX.js} +9 -9
  23. package/dist/{run-NDSNTVYP.js → run-GDDIYEBP.js} +2 -2
  24. package/dist/sentinel-42NZRTTS.js +0 -0
  25. package/dist/server-ENT2OFNL.js +155 -0
  26. package/dist/shots-6DRTXFDI.js +13 -0
  27. package/dist/{variants-3IEP7DFY.js → variants-VURXVIQO.js} +3 -3
  28. package/package.json +1 -1
  29. package/dist/shots-OKWOYF7F.js +0 -13
package/README.md CHANGED
@@ -174,6 +174,12 @@ squint doctor --probe # run every engine end to end, verify auth act
174
174
  flagged from the live page, and on Next 16+ the framework's own `/_next/mcp`
175
175
  channel feeds structured errors straight into the fix loop. `/context` itemizes
176
176
  the injected-context bill per source, with staleness warnings.
177
+ - **Two more doors in**: `squint mcp` serves the gates as MCP tools (any
178
+ MCP-speaking agent calls squint's verification directly); `squint ci` runs the
179
+ whole loop headlessly in a pipeline — JSON report, non-zero exit on hard findings.
180
+ - **The sentinel**: gate evasion (deleted tests, added skips, suppressed
181
+ diagnostics, weakened checks, locked-path touches) detected deterministically per
182
+ turn and reported to you — never sent back to the engine that did it.
177
183
  - **Detachable sessions**: `squint serve` owns the session on a unix socket;
178
184
  `squint attach` joins from any terminal (observer seats for teammates, driver
179
185
  seat inherited on detach). Crash-proof, ssh-friendly, no cloud.
@@ -1,23 +1,23 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  App
4
- } from "./chunk-M25IQ7C7.js";
4
+ } from "./chunk-EB4OSUB7.js";
5
5
  import "./chunk-43NQNIJY.js";
6
- import "./chunk-7MOKOZOR.js";
6
+ import "./chunk-WASLLXEZ.js";
7
7
  import "./chunk-ARDV4XH6.js";
8
8
  import "./chunk-AUJJGMZG.js";
9
9
  import "./chunk-YHRAOBI2.js";
10
- import "./chunk-QCHBDP46.js";
11
- import "./chunk-YGSF2TSO.js";
12
- import "./chunk-VH7OOFQP.js";
10
+ import "./chunk-2I3YHT6P.js";
11
+ import "./chunk-J7ITMK6X.js";
12
+ import "./chunk-6NPUVPQD.js";
13
13
  import "./chunk-K5QJMSJH.js";
14
14
  import "./chunk-WAJXATCO.js";
15
15
  import "./chunk-62JNF5M2.js";
16
- import "./chunk-ZSDCMYZR.js";
16
+ import "./chunk-CRDRWTON.js";
17
17
  import "./chunk-7CAGWFAQ.js";
18
- import "./chunk-HC4E42SV.js";
19
- import "./chunk-IMDRXXFU.js";
20
- import "./chunk-KVYGPLWW.js";
18
+ import "./chunk-DCFNX2CQ.js";
19
+ import "./chunk-PYC53J7D.js";
20
+ import "./chunk-GV2HAALP.js";
21
21
  export {
22
22
  App
23
23
  };
@@ -2,12 +2,14 @@
2
2
  import {
3
3
  cdpCapture,
4
4
  hasWebSocket,
5
+ pixelDiffAttributed,
5
6
  pixelDiffPct,
6
7
  runFlow
7
- } from "./chunk-HC4E42SV.js";
8
+ } from "./chunk-DCFNX2CQ.js";
8
9
  export {
9
10
  cdpCapture,
10
11
  hasWebSocket,
12
+ pixelDiffAttributed,
11
13
  pixelDiffPct,
12
14
  runFlow
13
15
  };
@@ -2,8 +2,8 @@
2
2
  import {
3
3
  findChrome,
4
4
  screenshot
5
- } from "./chunk-IMDRXXFU.js";
6
- import "./chunk-KVYGPLWW.js";
5
+ } from "./chunk-PYC53J7D.js";
6
+ import "./chunk-GV2HAALP.js";
7
7
  export {
8
8
  findChrome,
9
9
  screenshot
@@ -1,14 +1,14 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  variantsRoot
4
- } from "./chunk-YGSF2TSO.js";
4
+ } from "./chunk-J7ITMK6X.js";
5
5
  import {
6
6
  findChrome,
7
7
  screenshot
8
- } from "./chunk-IMDRXXFU.js";
8
+ } from "./chunk-PYC53J7D.js";
9
9
  import {
10
10
  lineSplitter
11
- } from "./chunk-KVYGPLWW.js";
11
+ } from "./chunk-GV2HAALP.js";
12
12
 
13
13
  // src/variants/shots.ts
14
14
  import path2 from "path";
@@ -140,8 +140,9 @@ function stepExpression(step) {
140
140
  return null;
141
141
  }
142
142
  }
143
+
143
144
  export {
144
- loadFlows,
145
145
  parseFlow,
146
+ loadFlows,
146
147
  stepExpression
147
148
  };
@@ -3,7 +3,7 @@ import {
3
3
  findEngineBinary,
4
4
  lineSplitter,
5
5
  truncate
6
- } from "./chunk-KVYGPLWW.js";
6
+ } from "./chunk-GV2HAALP.js";
7
7
 
8
8
  // src/runner/run.ts
9
9
  import { spawn } from "child_process";
@@ -21,7 +21,7 @@ function runAgent(engine, opts, onEvent, signal) {
21
21
  let stderrTail = "";
22
22
  const child = spawn(binaryPath, engine.buildArgs(opts), {
23
23
  cwd: opts.cwd,
24
- env: process.env,
24
+ env: { ...process.env, ...engine.env },
25
25
  stdio: ["ignore", "pipe", "pipe"]
26
26
  });
27
27
  const emit = (event) => {
@@ -68,6 +68,7 @@ function formatContextReport(report) {
68
68
  }
69
69
  return out.join("\n");
70
70
  }
71
+
71
72
  export {
72
73
  contextReport,
73
74
  formatContextReport
@@ -5,11 +5,11 @@ import {
5
5
  import {
6
6
  cdpCapture,
7
7
  hasWebSocket
8
- } from "./chunk-HC4E42SV.js";
8
+ } from "./chunk-DCFNX2CQ.js";
9
9
  import {
10
10
  findChrome,
11
11
  screenshot
12
- } from "./chunk-IMDRXXFU.js";
12
+ } from "./chunk-PYC53J7D.js";
13
13
 
14
14
  // src/preview/preview.ts
15
15
  import fs2 from "fs";
@@ -75,7 +75,7 @@ async function captureViewports(cwd, url) {
75
75
  if (hasWebSocket()) {
76
76
  try {
77
77
  const checks = loadChecks(cwd);
78
- const { report, shots: shots2, a11y, slop, narration, phantoms, viewTransitions, components, checkFailures, webmcp } = await cdpCapture(chrome, url, dir, VIEWPORTS, 2500, true, checks);
78
+ const { report, shots: shots2, a11y, slop, narration, phantoms, viewTransitions, components, checkFailures, webmcp, jank } = await cdpCapture(chrome, url, dir, VIEWPORTS, 2500, true, checks);
79
79
  const errors2 = [];
80
80
  for (const route of routes.slice(1)) {
81
81
  try {
@@ -90,7 +90,7 @@ async function captureViewports(cwd, url) {
90
90
  errors2.push(`${route}: capture failed`);
91
91
  }
92
92
  }
93
- return { shots: shots2, errors: errors2, runtime: report, a11y, slop, narration, phantoms, viewTransitions, components, checkFailures, webmcp };
93
+ return { shots: shots2, errors: errors2, runtime: report, a11y, slop, narration, phantoms, viewTransitions, components, checkFailures, webmcp, jank };
94
94
  } catch {
95
95
  }
96
96
  }
@@ -163,9 +163,15 @@ async function probeRuntime(url, cwd) {
163
163
  async function comparePulse(previous, current) {
164
164
  const chrome = findChrome();
165
165
  if (!chrome || !hasWebSocket()) return null;
166
- const { pixelDiffPct } = await import("./cdp-PBQKXC6R.js");
166
+ const { pixelDiffPct } = await import("./cdp-WSWOSDEX.js");
167
167
  return pixelDiffPct(chrome, previous, current);
168
168
  }
169
+ async function comparePulseAttributed(previous, current, url) {
170
+ const chrome = findChrome();
171
+ if (!chrome || !hasWebSocket()) return null;
172
+ const { pixelDiffAttributed } = await import("./cdp-WSWOSDEX.js");
173
+ return pixelDiffAttributed(chrome, previous, current, url);
174
+ }
169
175
  function buildRuntimeFixPrompt(report) {
170
176
  return `The running app has runtime problems.${runtimeSection(report)}
171
177
 
@@ -191,6 +197,16 @@ ${narration.join("\n")}
191
197
 
192
198
  Judge this narration as an experience: does the reading order make sense? do names actually describe their targets? is anything announced as "(no accessible name)"? Fix real incoherence \u2014 this is how non-visual users meet the page.`;
193
199
  }
200
+ function jankSection(jank) {
201
+ if (!jank || jank.length === 0) return "";
202
+ return `
203
+
204
+ ## Jank attribution (main-thread frames \u226550ms)
205
+
206
+ ${jank.join("\n")}
207
+
208
+ Each line names the function behind a long animation frame observed during load and a scripted scroll. Fix the work (memoize, virtualize, move off the main thread) \u2014 do not remove the animation.`;
209
+ }
194
210
  function webmcpSection(webmcp) {
195
211
  if (!webmcp || webmcp.length === 0) return "";
196
212
  return `
@@ -241,13 +257,13 @@ ${findings.join("\n")}
241
257
 
242
258
  These patterns make the page read as template output. Rework them within the committed design direction \u2014 this is style debt, not a defect list.`;
243
259
  }
244
- function buildReviewPrompt(shots, extra, runtime, a11y, slop, narration, phantoms, viewTransitions, components, webmcp) {
260
+ function buildReviewPrompt(shots, extra, runtime, a11y, slop, narration, phantoms, viewTransitions, components, webmcp, jank) {
245
261
  const list = shots.map((s) => `- ${s.name}: ${s.path}`).join("\n");
246
262
  return `Screenshots of the running app were just captured:
247
263
 
248
264
  ${list}
249
265
 
250
- Read each screenshot and review the rendered UI against the design standards you were given. Check: visual hierarchy and spacing rhythm, typography, color and contrast, alignment, empty-looking or broken regions, and whether the mobile capture shows horizontal overflow or cramped layout. List the concrete issues you can SEE (not hypothetical ones), ranked by visual impact${extra ? `, with special attention to: ${extra}` : ""}. Then fix them and verify the app still builds.${runtimeSection(runtime)}${a11ySection(a11y)}${slopSection(slop)}${narrationSection(narration)}${phantomSection(phantoms)}${vtSection(viewTransitions)}${componentSection(components)}${webmcpSection(webmcp)}`;
266
+ Read each screenshot and review the rendered UI against the design standards you were given. Check: visual hierarchy and spacing rhythm, typography, color and contrast, alignment, empty-looking or broken regions, and whether the mobile capture shows horizontal overflow or cramped layout. List the concrete issues you can SEE (not hypothetical ones), ranked by visual impact${extra ? `, with special attention to: ${extra}` : ""}. Then fix them and verify the app still builds.${runtimeSection(runtime)}${a11ySection(a11y)}${slopSection(slop)}${narrationSection(narration)}${phantomSection(phantoms)}${vtSection(viewTransitions)}${componentSection(components)}${webmcpSection(webmcp)}${jankSection(jank)}`;
251
267
  }
252
268
 
253
269
  export {
@@ -259,6 +275,7 @@ export {
259
275
  runtimeSummary,
260
276
  probeRuntime,
261
277
  comparePulse,
278
+ comparePulseAttributed,
262
279
  buildRuntimeFixPrompt,
263
280
  buildReviewPrompt
264
281
  };
@@ -179,7 +179,7 @@ var describe = (value) => {
179
179
  return String(value);
180
180
  };
181
181
  async function runFlow(chromePath, baseUrl, flow, outDir) {
182
- const { stepExpression } = await import("./flows-6LK7NGWS.js");
182
+ const { stepExpression } = await import("./flows-MXJL45HW.js");
183
183
  const { child, wsUrl, profileDir } = await launchChrome(chromePath);
184
184
  const shots = [];
185
185
  let connection = null;
@@ -189,6 +189,7 @@ async function runFlow(chromePath, baseUrl, flow, outDir) {
189
189
  const { sessionId } = await connection.send("Target.attachToTarget", { targetId, flatten: true });
190
190
  await connection.send("Page.enable", {}, sessionId);
191
191
  await connection.send("Page.addScriptToEvaluateOnNewDocument", { source: WEBMCP_SHIM }, sessionId).catch(() => null);
192
+ await connection.send("Page.addScriptToEvaluateOnNewDocument", { source: LOAF_SHIM }, sessionId).catch(() => null);
192
193
  await connection.send(
193
194
  "Emulation.setDeviceMetricsOverride",
194
195
  { width: 1280, height: 800, deviceScaleFactor: 1, mobile: false },
@@ -237,6 +238,145 @@ async function runFlow(chromePath, baseUrl, flow, outDir) {
237
238
  setTimeout(() => fs.rmSync(profileDir, { recursive: true, force: true }), 500).unref?.();
238
239
  }
239
240
  }
241
+ async function pixelDiffAttributed(chromePath, pngA, pngB, url) {
242
+ const { child, wsUrl, profileDir } = await launchChrome(chromePath);
243
+ let connection = null;
244
+ try {
245
+ connection = await CdpConnection.connect(wsUrl, 1e4);
246
+ const { targetId } = await connection.send("Target.createTarget", { url: "about:blank" });
247
+ const { sessionId } = await connection.send("Target.attachToTarget", { targetId, flatten: true });
248
+ await connection.send("Runtime.enable", {}, sessionId);
249
+ const expression = `(async () => {
250
+ const load = (src) => new Promise((resolve, reject) => {
251
+ const img = new Image();
252
+ img.onload = () => resolve(img);
253
+ img.onerror = () => reject(new Error('decode'));
254
+ img.src = src;
255
+ });
256
+ const a = await load('data:image/png;base64,${pngA.toString("base64")}');
257
+ const b = await load('data:image/png;base64,${pngB.toString("base64")}');
258
+ const w = Math.min(a.width, b.width), h = Math.min(a.height, b.height);
259
+ if (w === 0 || h === 0) return null;
260
+ const draw = (img) => {
261
+ const c = document.createElement('canvas');
262
+ c.width = w; c.height = h;
263
+ const ctx = c.getContext('2d');
264
+ ctx.drawImage(img, 0, 0);
265
+ return ctx.getImageData(0, 0, w, h).data;
266
+ };
267
+ const da = draw(a), db = draw(b);
268
+ const CELL = 32;
269
+ const gw = Math.ceil(w / CELL), gh = Math.ceil(h / CELL);
270
+ const marked = new Uint8Array(gw * gh);
271
+ let differ = 0, total = 0;
272
+ for (let y = 0; y < h; y += 2) {
273
+ for (let x = 0; x < w; x += 2) {
274
+ total++;
275
+ const i = (y * w + x) * 4;
276
+ if (Math.abs(da[i] - db[i]) > 8 || Math.abs(da[i + 1] - db[i + 1]) > 8 || Math.abs(da[i + 2] - db[i + 2]) > 8) {
277
+ differ++;
278
+ marked[Math.floor(y / CELL) * gw + Math.floor(x / CELL)] = 1;
279
+ }
280
+ }
281
+ }
282
+ // Flood-fill marked cells into region bounding boxes.
283
+ const seen = new Uint8Array(gw * gh);
284
+ const regions = [];
285
+ for (let cy = 0; cy < gh; cy++) {
286
+ for (let cx = 0; cx < gw; cx++) {
287
+ const start = cy * gw + cx;
288
+ if (!marked[start] || seen[start]) continue;
289
+ let minX = cx, maxX = cx, minY = cy, maxY = cy, cells = 0;
290
+ const stack = [start];
291
+ seen[start] = 1;
292
+ while (stack.length) {
293
+ const cell = stack.pop();
294
+ const x = cell % gw, y = Math.floor(cell / gw);
295
+ cells++;
296
+ if (x < minX) minX = x; if (x > maxX) maxX = x;
297
+ if (y < minY) minY = y; if (y > maxY) maxY = y;
298
+ for (const [dx, dy] of [[1,0],[-1,0],[0,1],[0,-1]]) {
299
+ const nx = x + dx, ny = y + dy;
300
+ if (nx < 0 || ny < 0 || nx >= gw || ny >= gh) continue;
301
+ const n = ny * gw + nx;
302
+ if (marked[n] && !seen[n]) { seen[n] = 1; stack.push(n); }
303
+ }
304
+ }
305
+ regions.push({
306
+ x: minX * CELL, y: minY * CELL,
307
+ w: Math.min(w, (maxX + 1) * CELL) - minX * CELL,
308
+ h: Math.min(h, (maxY + 1) * CELL) - minY * CELL,
309
+ cells,
310
+ });
311
+ }
312
+ }
313
+ regions.sort((p, q) => q.cells - p.cells);
314
+ const sizePenalty = (a.width !== b.width || a.height !== b.height) ? 1 : 0;
315
+ return { pct: Math.min(100, (differ / total) * 100 + sizePenalty), regions: regions.slice(0, 5), w, h };
316
+ })()`;
317
+ const { result } = await connection.send(
318
+ "Runtime.evaluate",
319
+ { expression, awaitPromise: true, returnByValue: true },
320
+ sessionId
321
+ );
322
+ const value = result?.value;
323
+ if (!value || typeof value.pct !== "number") return null;
324
+ if (!url || value.regions.length === 0 || value.pct < 0.5) return { pct: value.pct, sentences: [] };
325
+ let sentences = [];
326
+ try {
327
+ const page = await connection.send("Target.createTarget", { url: "about:blank" });
328
+ const attach = await connection.send("Target.attachToTarget", { targetId: page.targetId, flatten: true });
329
+ const pageSession = attach.sessionId;
330
+ await connection.send("Runtime.enable", {}, pageSession);
331
+ await connection.send("Page.enable", {}, pageSession);
332
+ await connection.send(
333
+ "Emulation.setDeviceMetricsOverride",
334
+ { width: value.w, height: value.h, deviceScaleFactor: 1, mobile: false },
335
+ pageSession
336
+ );
337
+ await connection.send("Page.navigate", { url }, pageSession);
338
+ await new Promise((resolve) => setTimeout(resolve, 1200));
339
+ const hitTest = `((regions) => regions.map((r) => {
340
+ const el = document.elementsFromPoint(r.x + r.w / 2, r.y + r.h / 2)
341
+ .find((e) => e !== document.documentElement && e !== document.body);
342
+ const where = r.w + '\xD7' + r.h + ' region @ (' + r.x + ',' + r.y + ')';
343
+ if (!el) return where + ' changed';
344
+ let label = el.tagName.toLowerCase();
345
+ if (el.id) label += '#' + el.id;
346
+ else if (el.classList[0]) label += '.' + el.classList[0];
347
+ const key = Object.keys(el).find((k) => k.startsWith('__reactFiber$'));
348
+ let chain = '';
349
+ if (key) {
350
+ let fiber = el[key];
351
+ const names = [];
352
+ let hops = 0;
353
+ while (fiber && hops < 50 && names.length < 2) {
354
+ const t = fiber.type;
355
+ const n = typeof t === 'function' ? (t.displayName || t.name || '') : '';
356
+ if (n && !names.includes(n)) names.push(n);
357
+ fiber = fiber.return; hops++;
358
+ }
359
+ chain = names.join(' < ');
360
+ }
361
+ return '<' + label + '>' + (chain ? ' (' + chain + ')' : '') + ': ' + where + ' changed';
362
+ }))(${JSON.stringify(value.regions)})`;
363
+ const hit = await connection.send(
364
+ "Runtime.evaluate",
365
+ { expression: hitTest, returnByValue: true },
366
+ pageSession
367
+ );
368
+ if (Array.isArray(hit.result?.value)) sentences = hit.result.value.map(String);
369
+ } catch {
370
+ }
371
+ return { pct: value.pct, sentences };
372
+ } catch {
373
+ return null;
374
+ } finally {
375
+ connection?.close();
376
+ child.kill("SIGKILL");
377
+ setTimeout(() => fs.rmSync(profileDir, { recursive: true, force: true }), 500).unref?.();
378
+ }
379
+ }
240
380
  async function pixelDiffPct(chromePath, pngA, pngB) {
241
381
  const { child, wsUrl, profileDir } = await launchChrome(chromePath);
242
382
  let connection = null;
@@ -297,6 +437,42 @@ var SLOP_AUDIT = `(() => {
297
437
  break;
298
438
  }
299
439
  }
440
+ // APCA (the contrast model that graduated in DevTools 149): body text
441
+ // under |Lc| 60 reads as fog even when it squeaks past WCAG AA.
442
+ const chan = (v) => Math.pow(v / 255, 2.4);
443
+ const lum = (rgb) => {
444
+ const m = rgb.match(/rgba?\\(([\\d.]+)[,\\s]+([\\d.]+)[,\\s]+([\\d.]+)/);
445
+ if (!m) return null;
446
+ return 0.2126729 * chan(+m[1]) + 0.7151522 * chan(+m[2]) + 0.0721750 * chan(+m[3]);
447
+ };
448
+ const apca = (txt, bg) => {
449
+ let yt = lum(txt), yb = lum(bg);
450
+ if (yt === null || yb === null) return null;
451
+ const clamp = (y) => (y < 0.022 ? y + Math.pow(0.022 - y, 1.414) : y);
452
+ yt = clamp(yt); yb = clamp(yb);
453
+ const c = yb > yt ? (Math.pow(yb, 0.56) - Math.pow(yt, 0.57)) * 1.14 : (Math.pow(yb, 0.65) - Math.pow(yt, 0.62)) * 1.14;
454
+ return Math.abs(c) < 0.1 ? 0 : Math.round((Math.abs(c) - 0.027) * 100);
455
+ };
456
+ let apcaFlagged = 0;
457
+ for (const p of document.querySelectorAll('p, li, td, span')) {
458
+ if (apcaFlagged >= 2) break;
459
+ const text = (p.textContent || '').trim();
460
+ if (text.length < 40) continue;
461
+ const cs = getComputedStyle(p);
462
+ if (parseFloat(cs.fontSize) >= 24) continue;
463
+ let bgEl = p, bg = 'rgba(0, 0, 0, 0)';
464
+ while (bgEl) {
465
+ const b = getComputedStyle(bgEl).backgroundColor;
466
+ if (b && !b.includes('0, 0, 0, 0')) { bg = b; break; }
467
+ bgEl = bgEl.parentElement;
468
+ }
469
+ if (bg.includes('0, 0, 0, 0')) bg = 'rgb(255, 255, 255)';
470
+ const lc = apca(cs.color, bg);
471
+ if (lc !== null && lc > 0 && lc < 60) {
472
+ out.push('low APCA contrast: Lc ' + lc + ' on <' + p.tagName.toLowerCase() + '> body text ("' + text.slice(0, 40) + '\u2026") \u2014 fog, not elegance');
473
+ apcaFlagged++;
474
+ }
475
+ }
300
476
  const vw = innerWidth, vh = innerHeight;
301
477
  for (const el of document.querySelectorAll('*')) {
302
478
  const r = el.getBoundingClientRect();
@@ -457,6 +633,40 @@ var WEBMCP_SHIM = `(() => {
457
633
  wrap(document.modelContext || (document.modelContext = {}));
458
634
  wrap(navigator.modelContext || (navigator.modelContext = {}));
459
635
  })()`;
636
+ var LOAF_SHIM = `(() => {
637
+ window.__squintLoaf = [];
638
+ try {
639
+ new PerformanceObserver((list) => {
640
+ for (const e of list.getEntries()) {
641
+ if (e.duration < 50) continue;
642
+ const s = (e.scripts && e.scripts[0]) || {};
643
+ window.__squintLoaf.push({
644
+ duration: Math.round(e.duration),
645
+ fn: s.sourceFunctionName || '',
646
+ url: (s.sourceURL || s.name || '').split('/').pop() || '',
647
+ invoker: s.invoker || '',
648
+ });
649
+ }
650
+ }).observe({ type: 'long-animation-frame', buffered: true });
651
+ } catch {}
652
+ })()`;
653
+ var SCRIPTED_SCROLL = `(async () => {
654
+ const half = Math.max(0, (document.body.scrollHeight - innerHeight) / 2);
655
+ window.scrollTo({ top: half, behavior: 'smooth' });
656
+ await new Promise((r) => setTimeout(r, 400));
657
+ window.scrollTo({ top: 0, behavior: 'smooth' });
658
+ await new Promise((r) => setTimeout(r, 300));
659
+ const seen = new Map();
660
+ for (const e of window.__squintLoaf || []) {
661
+ const key = e.fn + '@' + e.url + '@' + e.invoker;
662
+ const prev = seen.get(key);
663
+ if (!prev || e.duration > prev.duration) seen.set(key, e);
664
+ }
665
+ return [...seen.values()]
666
+ .sort((a, b) => b.duration - a.duration)
667
+ .slice(0, 6)
668
+ .map((e) => e.duration + 'ms frame \u2014 ' + (e.fn || e.invoker || 'script') + (e.url ? ' @ ' + e.url : ''));
669
+ })()`;
460
670
  async function cdpCapture(chromePath, url, outDir, viewports, settleMs = 2500, audit = false, checks = []) {
461
671
  const { child, wsUrl, profileDir } = await launchChrome(chromePath);
462
672
  const report = { consoleErrors: [], pageErrors: [], failedRequests: [] };
@@ -470,6 +680,7 @@ async function cdpCapture(chromePath, url, outDir, viewports, settleMs = 2500, a
470
680
  let components = [];
471
681
  const checkFailures = [];
472
682
  let webmcp = [];
683
+ let jank = [];
473
684
  const requests = /* @__PURE__ */ new Map();
474
685
  let connection = null;
475
686
  try {
@@ -509,6 +720,7 @@ async function cdpCapture(chromePath, url, outDir, viewports, settleMs = 2500, a
509
720
  await connection.send("Network.enable", {}, sessionId);
510
721
  await connection.send("Page.enable", {}, sessionId);
511
722
  await connection.send("Page.addScriptToEvaluateOnNewDocument", { source: WEBMCP_SHIM }, sessionId).catch(() => null);
723
+ await connection.send("Page.addScriptToEvaluateOnNewDocument", { source: LOAF_SHIM }, sessionId).catch(() => null);
512
724
  await connection.send("Page.navigate", { url }, sessionId);
513
725
  const deadline = Date.now() + 12e3;
514
726
  while (!loaded && Date.now() < deadline) {
@@ -524,6 +736,15 @@ async function cdpCapture(chromePath, url, outDir, viewports, settleMs = 2500, a
524
736
  if (result?.value && typeof result.value === "object") perf = result.value;
525
737
  } catch {
526
738
  }
739
+ try {
740
+ const { result } = await connection.send(
741
+ "Runtime.evaluate",
742
+ { expression: SCRIPTED_SCROLL, returnByValue: true, awaitPromise: true },
743
+ sessionId
744
+ );
745
+ if (Array.isArray(result?.value)) jank = result.value.map(String);
746
+ } catch {
747
+ }
527
748
  try {
528
749
  const { result } = await connection.send(
529
750
  "Runtime.evaluate",
@@ -643,7 +864,10 @@ async function cdpCapture(chromePath, url, outDir, viewports, settleMs = 2500, a
643
864
  {
644
865
  width: viewport.width,
645
866
  height: viewport.height,
646
- deviceScaleFactor: 1,
867
+ // 2x for review shots: sub-pixel border/kerning slop is
868
+ // invisible at 1x. The pulse stays 1x — its diff math and
869
+ // hit-testing assume CSS pixels.
870
+ deviceScaleFactor: viewport.name === "pulse" ? 1 : 2,
647
871
  mobile: viewport.width < 500
648
872
  },
649
873
  sessionId
@@ -659,12 +883,13 @@ async function cdpCapture(chromePath, url, outDir, viewports, settleMs = 2500, a
659
883
  child.kill("SIGKILL");
660
884
  setTimeout(() => fs.rmSync(profileDir, { recursive: true, force: true }), 500).unref?.();
661
885
  }
662
- return { report, shots, a11y, slop, perf, narration, phantoms, viewTransitions, components, checkFailures, webmcp };
886
+ return { report, shots, a11y, slop, perf, narration, phantoms, viewTransitions, components, checkFailures, webmcp, jank };
663
887
  }
664
888
 
665
889
  export {
666
890
  hasWebSocket,
667
891
  runFlow,
892
+ pixelDiffAttributed,
668
893
  pixelDiffPct,
669
894
  cdpCapture
670
895
  };
@@ -4,7 +4,7 @@ import {
4
4
  } from "./chunk-43NQNIJY.js";
5
5
  import {
6
6
  Session
7
- } from "./chunk-7MOKOZOR.js";
7
+ } from "./chunk-WASLLXEZ.js";
8
8
 
9
9
  // src/tui/App.tsx
10
10
  import { Box as Box3, Static, Text as Text3, useApp, useInput } from "ink";
@@ -148,6 +148,10 @@ var claude = {
148
148
  binary: "claude",
149
149
  install: "npm install -g @anthropic-ai/claude-code",
150
150
  supportsResume: true,
151
+ // 2.1.211+: subagent output joins stream-json. The env-var form is a
152
+ // no-op on older CLIs, unlike the flag, so the loops see spawned work
153
+ // wherever it's supported.
154
+ env: { CLAUDE_CODE_FORWARD_SUBAGENT_TEXT: "1" },
151
155
  buildArgs(opts) {
152
156
  const permissionMode = opts.mode === "plan" ? "plan" : opts.mode === "yolo" ? "bypassPermissions" : "acceptEdits";
153
157
  const args = [
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  runAgent
4
- } from "./chunk-VH7OOFQP.js";
4
+ } from "./chunk-6NPUVPQD.js";
5
5
  import {
6
6
  FAMILIES,
7
7
  renderFamilyBrief
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  findBinary
4
- } from "./chunk-KVYGPLWW.js";
4
+ } from "./chunk-GV2HAALP.js";
5
5
 
6
6
  // src/preview/chrome.ts
7
7
  import { spawn } from "child_process";
@@ -23,16 +23,16 @@ import {
23
23
  buildFixPrompt,
24
24
  detectDevCommand,
25
25
  screenshotVariants
26
- } from "./chunk-QCHBDP46.js";
26
+ } from "./chunk-2I3YHT6P.js";
27
27
  import {
28
28
  applyVariant,
29
29
  cleanVariants,
30
30
  listVariants,
31
31
  runVariants
32
- } from "./chunk-YGSF2TSO.js";
32
+ } from "./chunk-J7ITMK6X.js";
33
33
  import {
34
34
  runAgent
35
- } from "./chunk-VH7OOFQP.js";
35
+ } from "./chunk-6NPUVPQD.js";
36
36
  import {
37
37
  composePrompt
38
38
  } from "./chunk-WAJXATCO.js";
@@ -46,10 +46,10 @@ import {
46
46
  buildReviewPrompt,
47
47
  buildRuntimeFixPrompt,
48
48
  captureViewports,
49
- comparePulse,
49
+ comparePulseAttributed,
50
50
  probeRuntime,
51
51
  runtimeSummary
52
- } from "./chunk-ZSDCMYZR.js";
52
+ } from "./chunk-CRDRWTON.js";
53
53
  import {
54
54
  clearState,
55
55
  loadState,
@@ -57,10 +57,10 @@ import {
57
57
  } from "./chunk-7CAGWFAQ.js";
58
58
  import {
59
59
  findChrome
60
- } from "./chunk-IMDRXXFU.js";
60
+ } from "./chunk-PYC53J7D.js";
61
61
  import {
62
62
  getEngine
63
- } from "./chunk-KVYGPLWW.js";
63
+ } from "./chunk-GV2HAALP.js";
64
64
 
65
65
  // src/session/engine.ts
66
66
  import fs2 from "fs";
@@ -267,7 +267,7 @@ ${question}`,
267
267
  return;
268
268
  }
269
269
  await this.runTurn(
270
- buildReviewPrompt(result.shots, void 0, result.runtime, result.a11y, result.slop, result.narration, result.phantoms, result.viewTransitions, result.components, result.webmcp),
270
+ buildReviewPrompt(result.shots, void 0, result.runtime, result.a11y, result.slop, result.narration, result.phantoms, result.viewTransitions, result.components, result.webmcp, result.jank),
271
271
  `\u{1F441} polish round ${round}/${rounds}`
272
272
  );
273
273
  }
@@ -509,6 +509,17 @@ ${driftSummary(drift)}`);
509
509
  }
510
510
  } catch {
511
511
  }
512
+ try {
513
+ const { scanEvasion, sentinelSummary } = await import("./sentinel-42NZRTTS.js");
514
+ const evasions = scanEvasion(this.execCwd(), checkpoint.snapshot.stashHash ?? "HEAD");
515
+ if (evasions.length > 0) {
516
+ this.push("error", `\u26A0 sentinel: ${evasions.length} gate-evasion pattern(s) this turn
517
+ ${sentinelSummary(evasions)}
518
+ Review before trusting green \u2014 /undo rolls the turn back.`);
519
+ runHook(this.opts.cwd, "on-sentinel", { count: String(evasions.length), summary: sentinelSummary(evasions) });
520
+ }
521
+ } catch {
522
+ }
512
523
  try {
513
524
  const { rulePackSummary, scanRulePacks } = await import("./rulepacks-JOA675OJ.js");
514
525
  const findings = scanRulePacks(this.execCwd(), checkpoint.snapshot.stashHash ?? "HEAD");
@@ -647,7 +658,7 @@ ${probe.checkFailures.slice(0, 5).join("\n")}`);
647
658
  const captureResult = await this.capture();
648
659
  if (captureResult) {
649
660
  await this.runTurn(
650
- buildReviewPrompt(captureResult.shots, void 0, captureResult.runtime, captureResult.a11y, captureResult.slop, captureResult.narration, captureResult.phantoms, captureResult.viewTransitions, captureResult.components, captureResult.webmcp),
661
+ buildReviewPrompt(captureResult.shots, void 0, captureResult.runtime, captureResult.a11y, captureResult.slop, captureResult.narration, captureResult.phantoms, captureResult.viewTransitions, captureResult.components, captureResult.webmcp, captureResult.jank),
651
662
  "\u{1F441} auto-review rendered UI"
652
663
  );
653
664
  }
@@ -744,11 +755,13 @@ ${probe.checkFailures.slice(0, 5).join("\n")}`);
744
755
  this.push("image", pulsePath);
745
756
  return null;
746
757
  }
747
- const pct = await comparePulse(previous, current);
748
- if (pct === null) return null;
758
+ const diff = await comparePulseAttributed(previous, current, this.state.devUrl ?? void 0);
759
+ if (diff === null) return null;
760
+ const pct = diff.pct;
749
761
  this.push(
750
762
  "status",
751
- pct < 0.5 ? "visual pulse: stable vs last turn" : `visual pulse: ${pct.toFixed(1)}% of the page changed vs last turn`
763
+ pct < 0.5 ? "visual pulse: stable vs last turn" : `visual pulse: ${pct.toFixed(1)}% of the page changed vs last turn${diff.sentences.length > 0 ? `
764
+ ${diff.sentences.map((s) => ` ${s}`).join("\n")}` : ""}`
752
765
  );
753
766
  runHook(this.opts.cwd, "on-pulse-diff", { pct: pct.toFixed(1) });
754
767
  if (pct >= 0.5) this.push("image", pulsePath);
@@ -846,7 +859,7 @@ They are objective defects, not style preferences.`
846
859
  const arg = rest.join(" ").trim();
847
860
  switch (name) {
848
861
  case "engines": {
849
- void import("./registry-MIJ6LSAY.js").then(({ detectEngines }) => {
862
+ void import("./registry-JBP7J3JB.js").then(({ detectEngines }) => {
850
863
  const lines = detectEngines().map(({ engine, path: binaryPath }) => {
851
864
  const mark = binaryPath ? "\u2713" : "\u2717";
852
865
  const traits = [engine.createParser ? "stream" : "text", engine.supportsResume ? "resume" : null].filter(Boolean).join(" \xB7 ");
@@ -935,7 +948,7 @@ They are objective defects, not style preferences.`
935
948
  break;
936
949
  }
937
950
  case "context": {
938
- import("./contextDoctor-A26C2ZDN.js").then(({ contextReport, formatContextReport }) => {
951
+ import("./contextDoctor-3Y36YURE.js").then(({ contextReport, formatContextReport }) => {
939
952
  this.push("status", formatContextReport(contextReport(this.execCwd())));
940
953
  }).catch((error) => {
941
954
  this.push("status", `context report failed: ${error instanceof Error ? error.message : String(error)}`);
@@ -1059,7 +1072,7 @@ They are objective defects, not style preferences.`
1059
1072
  break;
1060
1073
  }
1061
1074
  void (async () => {
1062
- const { loadFlows } = await import("./flows-6LK7NGWS.js");
1075
+ const { loadFlows } = await import("./flows-MXJL45HW.js");
1063
1076
  const flows = loadFlows(this.opts.cwd);
1064
1077
  if (flows.length === 0) {
1065
1078
  this.push("status", "no flows \u2014 add .squint/flows/<name>.flow (goto/click/fill/press/expect/shot lines), or ask the engine to write one");
@@ -1070,8 +1083,8 @@ They are objective defects, not style preferences.`
1070
1083
  this.push("error", "no Chrome/Chromium found for flows");
1071
1084
  return;
1072
1085
  }
1073
- const { runFlow } = await import("./cdp-PBQKXC6R.js");
1074
- const { previewDir } = await import("./preview-WT34NVCH.js");
1086
+ const { runFlow } = await import("./cdp-WSWOSDEX.js");
1087
+ const { previewDir } = await import("./preview-CDEMPHWV.js");
1075
1088
  this.push("status", `replaying ${flows.length} flow(s)\u2026`);
1076
1089
  this.notify({ running: true, runStartedAt: Date.now() });
1077
1090
  const failures = [];
@@ -1209,7 +1222,7 @@ They are objective defects, not style preferences.`
1209
1222
  const result = await this.capture();
1210
1223
  if (result) {
1211
1224
  await this.runTurn(
1212
- buildReviewPrompt(result.shots, arg || void 0, result.runtime, result.a11y, result.slop, result.narration, result.phantoms, result.viewTransitions, result.components, result.webmcp),
1225
+ buildReviewPrompt(result.shots, arg || void 0, result.runtime, result.a11y, result.slop, result.narration, result.phantoms, result.viewTransitions, result.components, result.webmcp, result.jank),
1213
1226
  `\u{1F441} review rendered UI${arg ? ` \xB7 ${arg}` : ""}`
1214
1227
  );
1215
1228
  }
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  Session
4
- } from "./chunk-7MOKOZOR.js";
4
+ } from "./chunk-WASLLXEZ.js";
5
5
 
6
6
  // src/daemon/client.ts
7
7
  import net from "net";
package/dist/cli.js CHANGED
@@ -3,33 +3,33 @@ import {
3
3
  connectDaemon,
4
4
  socketPath,
5
5
  startDaemon
6
- } from "./chunk-NX7XQY2X.js";
6
+ } from "./chunk-YK5DEB7T.js";
7
7
  import {
8
8
  App
9
- } from "./chunk-M25IQ7C7.js";
9
+ } from "./chunk-EB4OSUB7.js";
10
10
  import "./chunk-43NQNIJY.js";
11
- import "./chunk-7MOKOZOR.js";
11
+ import "./chunk-WASLLXEZ.js";
12
12
  import "./chunk-ARDV4XH6.js";
13
13
  import "./chunk-AUJJGMZG.js";
14
14
  import "./chunk-YHRAOBI2.js";
15
- import "./chunk-QCHBDP46.js";
16
- import "./chunk-YGSF2TSO.js";
15
+ import "./chunk-2I3YHT6P.js";
16
+ import "./chunk-J7ITMK6X.js";
17
17
  import {
18
18
  runAgent
19
- } from "./chunk-VH7OOFQP.js";
19
+ } from "./chunk-6NPUVPQD.js";
20
20
  import "./chunk-K5QJMSJH.js";
21
21
  import {
22
22
  composePrompt
23
23
  } from "./chunk-WAJXATCO.js";
24
24
  import "./chunk-62JNF5M2.js";
25
- import "./chunk-ZSDCMYZR.js";
25
+ import "./chunk-CRDRWTON.js";
26
26
  import "./chunk-7CAGWFAQ.js";
27
- import "./chunk-HC4E42SV.js";
28
- import "./chunk-IMDRXXFU.js";
27
+ import "./chunk-DCFNX2CQ.js";
28
+ import "./chunk-PYC53J7D.js";
29
29
  import {
30
30
  detectEngines,
31
31
  getEngine
32
- } from "./chunk-KVYGPLWW.js";
32
+ } from "./chunk-GV2HAALP.js";
33
33
 
34
34
  // src/cli.tsx
35
35
  import { Command } from "commander";
@@ -185,15 +185,19 @@ function registerDaemon(program2) {
185
185
  process.on("SIGINT", stop);
186
186
  process.on("SIGTERM", stop);
187
187
  });
188
+ program2.command("mcp").description("serve the gates as MCP tools over stdio (check, shot, flows, context)").action(async () => {
189
+ const { runMcpServer } = await import("./server-ENT2OFNL.js");
190
+ runMcpServer(process.cwd());
191
+ });
188
192
  program2.command("attach").description("attach this terminal to a running squint daemon (full TUI; --plain for line mode)").option("--plain", "line-mode attach instead of the full TUI").action(async (opts) => {
189
193
  const cwd = process.cwd();
190
194
  if (!opts.plain) {
191
195
  try {
192
- const { RemoteSession } = await import("./remote-6O6FPTUF.js");
196
+ const { RemoteSession } = await import("./remote-C4BACCKX.js");
193
197
  const remote = await RemoteSession.connect(cwd);
194
198
  const config = loadConfig(defaultPaths(cwd));
195
199
  const { render: render2 } = await import("ink");
196
- const { App: App2 } = await import("./App-HWMVSRMF.js");
200
+ const { App: App2 } = await import("./App-MC2XEM7K.js");
197
201
  const React = await import("react");
198
202
  render2(
199
203
  React.createElement(App2, {
@@ -287,7 +291,7 @@ function registerEnv(program2) {
287
291
  console.log(`${status} ${engine.name}${binaryPath ? "" : pc3.dim(` \u2014 install: ${engine.install}`)}`);
288
292
  }
289
293
  if (options.probe) {
290
- const { runAgent: runAgent2 } = await import("./run-NDSNTVYP.js");
294
+ const { runAgent: runAgent2 } = await import("./run-GDDIYEBP.js");
291
295
  console.log(pc3.dim("\nprobing engines with a one-word prompt (verifies auth end to end)\u2026"));
292
296
  for (const { engine, path: binaryPath } of detected) {
293
297
  if (!binaryPath) continue;
@@ -309,8 +313,8 @@ function registerEnv(program2) {
309
313
  );
310
314
  }
311
315
  }
312
- const { findChrome } = await import("./chrome-SBV3H77F.js");
313
- const { hasWebSocket } = await import("./cdp-PBQKXC6R.js");
316
+ const { findChrome } = await import("./chrome-EEXJCG6W.js");
317
+ const { hasWebSocket } = await import("./cdp-WSWOSDEX.js");
314
318
  const chrome = findChrome();
315
319
  console.log(
316
320
  chrome ? `${pc3.green("\u2713")} Chrome ${pc3.dim(chrome)}` : `${pc3.yellow("\u25CB")} Chrome ${pc3.dim("\u2014 screenshots and runtime probing disabled")}`
@@ -451,7 +455,7 @@ function registerProject(program2) {
451
455
  process.exitCode = 1;
452
456
  return;
453
457
  }
454
- const { runVariants, cleanVariants } = await import("./variants-3IEP7DFY.js");
458
+ const { runVariants, cleanVariants } = await import("./variants-VURXVIQO.js");
455
459
  const config = loadConfig(defaultPaths(cwd));
456
460
  const engineId = resolveEngineId(config, options.engine);
457
461
  const engine = getEngine(engineId);
@@ -469,7 +473,7 @@ function registerProject(program2) {
469
473
  );
470
474
  const succeeded = runs.filter((r) => r.result.ok);
471
475
  if (options.shots && succeeded.length > 0) {
472
- const { screenshotVariants } = await import("./shots-OKWOYF7F.js");
476
+ const { screenshotVariants } = await import("./shots-6DRTXFDI.js");
473
477
  console.log(pc4.dim("capturing screenshots\u2026"));
474
478
  const shots = await screenshotVariants(cwd, succeeded.map((r) => r.variant));
475
479
  for (const shot of shots) {
@@ -484,7 +488,7 @@ ${succeeded.length}/${runs.length} variants ready in .squint/variants/ \u2014 `
484
488
  }
485
489
  );
486
490
  variantsCommand.command("list").description("List generated variants").action(async () => {
487
- const { listVariants } = await import("./variants-3IEP7DFY.js");
491
+ const { listVariants } = await import("./variants-VURXVIQO.js");
488
492
  const ids = listVariants(process.cwd());
489
493
  if (ids.length === 0) {
490
494
  console.log(pc4.dim('no variants \u2014 squint variants gen <n> "<ask>"'));
@@ -493,7 +497,7 @@ ${succeeded.length}/${runs.length} variants ready in .squint/variants/ \u2014 `
493
497
  for (const id of ids) console.log(id);
494
498
  });
495
499
  variantsCommand.command("apply").description("Apply one variant\u2019s changes to the main tree and discard the rest").argument("<id>", "family id of the winning variant").action(async (id) => {
496
- const { applyVariant, cleanVariants } = await import("./variants-3IEP7DFY.js");
500
+ const { applyVariant, cleanVariants } = await import("./variants-VURXVIQO.js");
497
501
  const cwd = process.cwd();
498
502
  const result = applyVariant(cwd, id);
499
503
  if (!result.ok) {
@@ -505,7 +509,7 @@ ${succeeded.length}/${runs.length} variants ready in .squint/variants/ \u2014 `
505
509
  console.log(pc4.green(`\u2713 applied ${id} to the working tree`) + pc4.dim(" \u2014 review with git diff"));
506
510
  });
507
511
  variantsCommand.command("clean").description("Discard all variants").action(async () => {
508
- const { cleanVariants } = await import("./variants-3IEP7DFY.js");
512
+ const { cleanVariants } = await import("./variants-VURXVIQO.js");
509
513
  const count = cleanVariants(process.cwd());
510
514
  console.log(pc4.dim(`removed ${count} variant(s)`));
511
515
  });
@@ -532,8 +536,81 @@ function registerQuality(program2) {
532
536
  });
533
537
  if (results.some((r) => !r.ok)) process.exitCode = 1;
534
538
  });
539
+ program2.command("ci").description("One-shot verification for pipelines: gates (+ audits and flows with --url), JSON report, non-zero exit on failure").option("--url <url>", "also audit a running app URL (runtime, a11y, phantoms, jank) and replay flows").option("--json <path>", "write the machine-readable report here").action(async (opts) => {
540
+ const cwd = process.cwd();
541
+ const startedAt = (/* @__PURE__ */ new Date()).toISOString();
542
+ const report = { startedAt, cwd };
543
+ let failed = false;
544
+ const { detectGates, runGates } = await import("./gates-ZC67NMW3.js");
545
+ const gates = detectGates(cwd);
546
+ const gateResults = await runGates(cwd, gates, (result) => {
547
+ console.log(`${result.ok ? pc5.green("\u2713") : pc5.red("\u2717")} ${result.gate.id} ${pc5.dim(`${(result.durationMs / 1e3).toFixed(1)}s`)}`);
548
+ if (!result.ok) console.log(pc5.dim(result.outputTail.split("\n").slice(-12).join("\n")));
549
+ });
550
+ report.gates = gateResults.map((r) => ({ id: r.gate.id, ok: r.ok, durationMs: r.durationMs, outputTail: r.ok ? void 0 : r.outputTail }));
551
+ if (gateResults.some((r) => !r.ok)) failed = true;
552
+ if (opts.url) {
553
+ const { captureViewports, runtimeSummary, previewDir } = await import("./preview-CDEMPHWV.js");
554
+ const capture = await captureViewports(cwd, opts.url);
555
+ if (!capture) {
556
+ console.log(pc5.red("\u2717 audit: capture failed (Chrome missing or URL unreachable)"));
557
+ report.audit = { ok: false };
558
+ failed = true;
559
+ } else {
560
+ const runtime = capture.runtime ? runtimeSummary(capture.runtime) : null;
561
+ const hard = {
562
+ runtime,
563
+ phantoms: capture.phantoms ?? [],
564
+ viewTransitions: (capture.viewTransitions ?? []).filter((v) => v.startsWith("duplicate")),
565
+ checkFailures: capture.checkFailures ?? []
566
+ };
567
+ const advisories = {
568
+ a11y: capture.a11y ?? [],
569
+ slop: capture.slop ?? [],
570
+ jank: capture.jank ?? []
571
+ };
572
+ const hardCount = (runtime ? 1 : 0) + hard.phantoms.length + hard.viewTransitions.length + hard.checkFailures.length;
573
+ report.audit = { ok: hardCount === 0, hard, advisories, shots: capture.shots.map((s) => s.path) };
574
+ if (hardCount > 0) {
575
+ failed = true;
576
+ console.log(pc5.red(`\u2717 audit: ${hardCount} hard finding(s)`));
577
+ for (const [k, v] of Object.entries(hard)) {
578
+ if (Array.isArray(v) && v.length > 0) console.log(pc5.dim(` ${k}: ${v.join(" \xB7 ")}`));
579
+ else if (typeof v === "string" && v) console.log(pc5.dim(` ${k}: ${v}`));
580
+ }
581
+ } else {
582
+ console.log(`${pc5.green("\u2713")} audit ${pc5.dim(`(${advisories.a11y.length} a11y, ${advisories.slop.length} slop, ${advisories.jank.length} jank advisories)`)}`);
583
+ }
584
+ }
585
+ const { loadFlows } = await import("./flows-MXJL45HW.js");
586
+ const flows = loadFlows(cwd);
587
+ if (flows.length > 0) {
588
+ const { findChrome } = await import("./chrome-EEXJCG6W.js");
589
+ const chrome = findChrome();
590
+ const flowReport = [];
591
+ if (chrome) {
592
+ const { runFlow } = await import("./cdp-WSWOSDEX.js");
593
+ for (const flow of flows) {
594
+ const result = await runFlow(chrome, opts.url, flow, previewDir(cwd));
595
+ flowReport.push({ name: flow.name, ok: result.ok, detail: result.ok ? void 0 : `step ${result.failedStep}: ${result.detail}` });
596
+ console.log(result.ok ? `${pc5.green("\u2713")} flow ${flow.name}` : pc5.red(`\u2717 flow ${flow.name} \u2014 step ${result.failedStep}: ${result.detail}`));
597
+ if (!result.ok) failed = true;
598
+ }
599
+ }
600
+ report.flows = flowReport;
601
+ }
602
+ }
603
+ report.ok = !failed;
604
+ report.finishedAt = (/* @__PURE__ */ new Date()).toISOString();
605
+ if (opts.json) {
606
+ const fs2 = await import("fs");
607
+ fs2.writeFileSync(opts.json, JSON.stringify(report, null, 2));
608
+ console.log(pc5.dim(`report \u2192 ${opts.json}`));
609
+ }
610
+ if (failed) process.exitCode = 1;
611
+ });
535
612
  program2.command("shot").description("Screenshot a running app at mobile/tablet/desktop viewports (+ .squint/routes)").argument("<url>", "URL of the running app (e.g. http://localhost:5173)").action(async (url) => {
536
- const { captureViewports } = await import("./preview-WT34NVCH.js");
613
+ const { captureViewports } = await import("./preview-CDEMPHWV.js");
537
614
  const result = await captureViewports(process.cwd(), url);
538
615
  if (!result) {
539
616
  console.error(pc5.red("\u2717 no Chrome/Chromium found"));
@@ -720,7 +797,7 @@ function registerTui(program2) {
720
797
  }
721
798
 
722
799
  // src/cli.tsx
723
- var VERSION = true ? "0.4.7" : "0.0.0-dev";
800
+ var VERSION = true ? "0.5.0" : "0.0.0-dev";
724
801
  var program = new Command();
725
802
  program.name("squint").description("Lovable for your terminal \u2014 a frontend harness on top of Claude Code, Codex, and friends.").version(VERSION);
726
803
  registerRun(program);
@@ -0,0 +1,11 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ contextReport,
4
+ formatContextReport
5
+ } from "./chunk-BKOGVFCO.js";
6
+ import "./chunk-ARDV4XH6.js";
7
+ import "./chunk-WAJXATCO.js";
8
+ export {
9
+ contextReport,
10
+ formatContextReport
11
+ };
@@ -0,0 +1,11 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ loadFlows,
4
+ parseFlow,
5
+ stepExpression
6
+ } from "./chunk-4XSHHGA6.js";
7
+ export {
8
+ loadFlows,
9
+ parseFlow,
10
+ stepExpression
11
+ };
@@ -5,22 +5,24 @@ import {
5
5
  buildRuntimeFixPrompt,
6
6
  captureViewports,
7
7
  comparePulse,
8
+ comparePulseAttributed,
8
9
  loadRoutes,
9
10
  previewDir,
10
11
  probeRuntime,
11
12
  routeShotName,
12
13
  runtimeSummary
13
- } from "./chunk-ZSDCMYZR.js";
14
+ } from "./chunk-CRDRWTON.js";
14
15
  import "./chunk-7CAGWFAQ.js";
15
- import "./chunk-HC4E42SV.js";
16
- import "./chunk-IMDRXXFU.js";
17
- import "./chunk-KVYGPLWW.js";
16
+ import "./chunk-DCFNX2CQ.js";
17
+ import "./chunk-PYC53J7D.js";
18
+ import "./chunk-GV2HAALP.js";
18
19
  export {
19
20
  VIEWPORTS,
20
21
  buildReviewPrompt,
21
22
  buildRuntimeFixPrompt,
22
23
  captureViewports,
23
24
  comparePulse,
25
+ comparePulseAttributed,
24
26
  loadRoutes,
25
27
  previewDir,
26
28
  probeRuntime,
@@ -5,7 +5,7 @@ import {
5
5
  findBinary,
6
6
  findEngineBinary,
7
7
  getEngine
8
- } from "./chunk-KVYGPLWW.js";
8
+ } from "./chunk-GV2HAALP.js";
9
9
  export {
10
10
  detectEngines,
11
11
  engines,
@@ -2,22 +2,22 @@
2
2
  import {
3
3
  connectDaemon,
4
4
  socketPath
5
- } from "./chunk-NX7XQY2X.js";
6
- import "./chunk-7MOKOZOR.js";
5
+ } from "./chunk-YK5DEB7T.js";
6
+ import "./chunk-WASLLXEZ.js";
7
7
  import "./chunk-ARDV4XH6.js";
8
8
  import "./chunk-AUJJGMZG.js";
9
9
  import "./chunk-YHRAOBI2.js";
10
- import "./chunk-QCHBDP46.js";
11
- import "./chunk-YGSF2TSO.js";
12
- import "./chunk-VH7OOFQP.js";
10
+ import "./chunk-2I3YHT6P.js";
11
+ import "./chunk-J7ITMK6X.js";
12
+ import "./chunk-6NPUVPQD.js";
13
13
  import "./chunk-K5QJMSJH.js";
14
14
  import "./chunk-WAJXATCO.js";
15
15
  import "./chunk-62JNF5M2.js";
16
- import "./chunk-ZSDCMYZR.js";
16
+ import "./chunk-CRDRWTON.js";
17
17
  import "./chunk-7CAGWFAQ.js";
18
- import "./chunk-HC4E42SV.js";
19
- import "./chunk-IMDRXXFU.js";
20
- import "./chunk-KVYGPLWW.js";
18
+ import "./chunk-DCFNX2CQ.js";
19
+ import "./chunk-PYC53J7D.js";
20
+ import "./chunk-GV2HAALP.js";
21
21
 
22
22
  // src/daemon/remote.ts
23
23
  var noteId = 0;
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  runAgent
4
- } from "./chunk-VH7OOFQP.js";
5
- import "./chunk-KVYGPLWW.js";
4
+ } from "./chunk-6NPUVPQD.js";
5
+ import "./chunk-GV2HAALP.js";
6
6
  export {
7
7
  runAgent
8
8
  };
Binary file
@@ -0,0 +1,155 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ contextReport,
4
+ formatContextReport
5
+ } from "./chunk-BKOGVFCO.js";
6
+ import "./chunk-ARDV4XH6.js";
7
+ import "./chunk-WAJXATCO.js";
8
+ import {
9
+ detectGates,
10
+ runGates
11
+ } from "./chunk-62JNF5M2.js";
12
+ import {
13
+ loadFlows
14
+ } from "./chunk-4XSHHGA6.js";
15
+ import {
16
+ captureViewports,
17
+ previewDir,
18
+ runtimeSummary
19
+ } from "./chunk-CRDRWTON.js";
20
+ import "./chunk-7CAGWFAQ.js";
21
+ import "./chunk-DCFNX2CQ.js";
22
+ import {
23
+ findChrome
24
+ } from "./chunk-PYC53J7D.js";
25
+ import "./chunk-GV2HAALP.js";
26
+
27
+ // src/mcp/server.ts
28
+ import readline from "readline";
29
+ var TOOLS = [
30
+ {
31
+ name: "squint_check",
32
+ description: "Run the repo quality gates (typecheck, lint, format, test, build as detected). Returns each gate with pass/fail and failure output.",
33
+ inputSchema: { type: "object", properties: {} },
34
+ async run(_args, cwd) {
35
+ const gates = detectGates(cwd);
36
+ if (gates.length === 0) return "no gates detected (no package.json scripts or known tools)";
37
+ const results = await runGates(cwd, gates);
38
+ return results.map((r) => `${r.ok ? "\u2713" : "\u2717"} ${r.gate.display}${r.ok ? "" : `
39
+ ${r.outputTail.slice(-1500)}`}`).join("\n");
40
+ }
41
+ },
42
+ {
43
+ name: "squint_shot",
44
+ description: "Screenshot and audit a running app URL headlessly: runtime errors, accessibility sweep, anti-slop tells, phantom classes, view-transition breakage, jank attribution, screen-reader narration. Returns findings; screenshots land in .squint/preview/.",
45
+ inputSchema: { type: "object", properties: { url: { type: "string" } }, required: ["url"] },
46
+ async run(args, cwd) {
47
+ const url = String(args.url ?? "");
48
+ const result = await captureViewports(cwd, url);
49
+ if (!result) return "capture failed \u2014 is Chrome installed and the URL reachable?";
50
+ const sections = [`shots: ${result.shots.map((s) => s.path).join(", ")}`];
51
+ const runtime = result.runtime ? runtimeSummary(result.runtime) : null;
52
+ if (runtime) sections.push(`runtime: ${runtime}`);
53
+ for (const [label, list] of [
54
+ ["a11y", result.a11y],
55
+ ["slop", result.slop],
56
+ ["phantom classes", result.phantoms],
57
+ ["view transitions", result.viewTransitions],
58
+ ["jank", result.jank],
59
+ ["components", result.components]
60
+ ]) {
61
+ if (list && list.length > 0) sections.push(`${label}:
62
+ ${list.join("\n")}`);
63
+ }
64
+ return sections.join("\n\n");
65
+ }
66
+ },
67
+ {
68
+ name: "squint_flows",
69
+ description: "Replay the repo's declared user journeys (.squint/flows/*.flow) headlessly against a URL. Returns pass/fail per flow with the failing step.",
70
+ inputSchema: { type: "object", properties: { url: { type: "string" } }, required: ["url"] },
71
+ async run(args, cwd) {
72
+ const url = String(args.url ?? "");
73
+ const flows = loadFlows(cwd);
74
+ if (flows.length === 0) return "no flows declared \u2014 add .squint/flows/<name>.flow";
75
+ const chrome = findChrome();
76
+ if (!chrome) return "no Chrome/Chromium found";
77
+ const { runFlow } = await import("./cdp-WSWOSDEX.js");
78
+ const lines = [];
79
+ for (const flow of flows) {
80
+ const result = await runFlow(chrome, url, flow, previewDir(cwd));
81
+ lines.push(
82
+ result.ok ? `\u2713 ${flow.name} (${flow.steps.length} steps)` : `\u2717 ${flow.name} at step ${result.failedStep}: ${result.detail}`
83
+ );
84
+ }
85
+ return lines.join("\n");
86
+ }
87
+ },
88
+ {
89
+ name: "squint_context",
90
+ description: "Itemize what squint injects into engine asks \u2014 token cost per source with staleness warnings (stale locks, generic skill triggers, oversized always-on context).",
91
+ inputSchema: { type: "object", properties: {} },
92
+ async run(_args, cwd) {
93
+ return formatContextReport(contextReport(cwd));
94
+ }
95
+ }
96
+ ];
97
+ function runMcpServer(cwd, input = process.stdin, output = process.stdout) {
98
+ const write = (msg) => output.write(`${JSON.stringify(msg)}
99
+ `);
100
+ const rl = readline.createInterface({ input });
101
+ rl.on("line", (line) => {
102
+ if (!line.trim()) return;
103
+ let req;
104
+ try {
105
+ req = JSON.parse(line);
106
+ } catch {
107
+ return;
108
+ }
109
+ void handle(req, cwd, write);
110
+ });
111
+ }
112
+ async function handle(req, cwd, write) {
113
+ const reply = (result) => {
114
+ if (req.id !== void 0) write({ jsonrpc: "2.0", id: req.id, result });
115
+ };
116
+ const fail = (code, message) => {
117
+ if (req.id !== void 0) write({ jsonrpc: "2.0", id: req.id, error: { code, message } });
118
+ };
119
+ switch (req.method) {
120
+ case "initialize":
121
+ reply({
122
+ protocolVersion: "2025-06-18",
123
+ capabilities: { tools: {} },
124
+ serverInfo: { name: "squint", version: "0" }
125
+ });
126
+ return;
127
+ case "notifications/initialized":
128
+ case "ping":
129
+ if (req.method === "ping") reply({});
130
+ return;
131
+ case "tools/list":
132
+ reply({ tools: TOOLS.map(({ name, description, inputSchema }) => ({ name, description, inputSchema })) });
133
+ return;
134
+ case "tools/call": {
135
+ const name = String(req.params?.name ?? "");
136
+ const tool = TOOLS.find((t) => t.name === name);
137
+ if (!tool) return fail(-32602, `unknown tool: ${name}`);
138
+ try {
139
+ const text = await tool.run(req.params?.arguments ?? {}, cwd);
140
+ reply({ content: [{ type: "text", text }] });
141
+ } catch (error) {
142
+ reply({
143
+ content: [{ type: "text", text: `tool failed: ${error instanceof Error ? error.message : String(error)}` }],
144
+ isError: true
145
+ });
146
+ }
147
+ return;
148
+ }
149
+ default:
150
+ fail(-32601, `method not found: ${req.method}`);
151
+ }
152
+ }
153
+ export {
154
+ runMcpServer
155
+ };
@@ -0,0 +1,13 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ screenshotVariants
4
+ } from "./chunk-2I3YHT6P.js";
5
+ import "./chunk-J7ITMK6X.js";
6
+ import "./chunk-6NPUVPQD.js";
7
+ import "./chunk-K5QJMSJH.js";
8
+ import "./chunk-WAJXATCO.js";
9
+ import "./chunk-PYC53J7D.js";
10
+ import "./chunk-GV2HAALP.js";
11
+ export {
12
+ screenshotVariants
13
+ };
@@ -9,11 +9,11 @@ import {
9
9
  runVariants,
10
10
  variantPrompt,
11
11
  variantsRoot
12
- } from "./chunk-YGSF2TSO.js";
13
- import "./chunk-VH7OOFQP.js";
12
+ } from "./chunk-J7ITMK6X.js";
13
+ import "./chunk-6NPUVPQD.js";
14
14
  import "./chunk-K5QJMSJH.js";
15
15
  import "./chunk-WAJXATCO.js";
16
- import "./chunk-KVYGPLWW.js";
16
+ import "./chunk-GV2HAALP.js";
17
17
  export {
18
18
  MAX_VARIANTS,
19
19
  applyVariant,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aayambansal/squint",
3
- "version": "0.4.7",
3
+ "version": "0.5.0",
4
4
  "description": "Lovable for your terminal \u2014 a frontend harness on top of Claude Code, Codex, and other coding agents.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -1,13 +0,0 @@
1
- #!/usr/bin/env node
2
- import {
3
- screenshotVariants
4
- } from "./chunk-QCHBDP46.js";
5
- import "./chunk-YGSF2TSO.js";
6
- import "./chunk-VH7OOFQP.js";
7
- import "./chunk-K5QJMSJH.js";
8
- import "./chunk-WAJXATCO.js";
9
- import "./chunk-IMDRXXFU.js";
10
- import "./chunk-KVYGPLWW.js";
11
- export {
12
- screenshotVariants
13
- };