@aayambansal/squint 0.4.4 → 0.4.6

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.
package/README.md CHANGED
@@ -174,6 +174,11 @@ 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
+ - **Persistent checks**: assertions the engine verifies once persist as
178
+ `.squint/checks/*.js` and replay against the live page every turn — one-off
179
+ verifications compound into repo-versioned regression checks.
180
+ - **Visual approval**: engines ask before contested changes — the request renders
181
+ with its screenshot, `/yes` / `/no` answer it, the ledger remembers.
177
182
  - **The design ledger**: `/decide` (plus chosen variants, rollbacks, accepted
178
183
  sandboxes) appends to a committed `.squint/design-log.jsonl`; recent decisions ride
179
184
  into every ask so they stop getting silently undone between sessions.
@@ -4,7 +4,7 @@ import {
4
4
  hasWebSocket,
5
5
  pixelDiffPct,
6
6
  runFlow
7
- } from "./chunk-JFFNQIBU.js";
7
+ } from "./chunk-X2RZJT4L.js";
8
8
  export {
9
9
  cdpCapture,
10
10
  hasWebSocket,
@@ -28,6 +28,8 @@ var COMMANDS = [
28
28
  { name: "find", args: "<term>", group: "session", description: "search this session and saved transcripts" },
29
29
  { name: "decide", args: "<text>", group: "session", description: "record a design decision; injected into every future ask" },
30
30
  { name: "context", group: "session", description: "what squint injects per ask, token-costed, with staleness warnings" },
31
+ { name: "yes", args: "[note]", group: "build", description: "approve the engine's pending visual-approval request" },
32
+ { name: "no", args: "[note]", group: "build", description: "reject the engine's pending visual-approval request" },
31
33
  { name: "resume", group: "session", description: "pick up the previous session for this repo" },
32
34
  { name: "clear", group: "session", description: "new session (transcript, totals, persisted state)" },
33
35
  { name: "help", group: "session", description: "list commands" },
@@ -156,6 +156,15 @@ ${locks.map((l) => `- ${l}`).join("\n")}
156
156
  If the task appears to require changing them, stop and explain instead.`
157
157
  );
158
158
  }
159
+ parts.push(
160
+ `## Requesting visual approval
161
+
162
+ For a visual decision you should not make alone (a redesign direction, removing something deliberate, reversing a design decision on record): write .squint/approval-request.json containing {"summary": "<one line>", "screenshot": "<path, optional>"} and end your turn immediately without making the change. The user's verdict arrives as the next message.
163
+
164
+ ## Persistent checks
165
+
166
+ When you verify something about the page that should stay true (an element exists, a state renders, a metric holds), persist it as .squint/checks/<name>.js \u2014 plain JS that evaluates IN THE PAGE to an array of failure strings (empty array = pass). squint replays every check against the live page after each turn.`
167
+ );
159
168
  const matched = matchSkills(loadSkills(cwd), ask);
160
169
  for (const skill of matched) {
161
170
  parts.push(`## Project notes: ${skill.name}
@@ -9,12 +9,39 @@ import {
9
9
  import {
10
10
  cdpCapture,
11
11
  hasWebSocket
12
- } from "./chunk-JFFNQIBU.js";
12
+ } from "./chunk-X2RZJT4L.js";
13
13
 
14
14
  // src/preview/preview.ts
15
- import fs from "fs";
15
+ import fs2 from "fs";
16
16
  import os from "os";
17
+ import path2 from "path";
18
+
19
+ // src/preview/checks.ts
20
+ import fs from "fs";
17
21
  import path from "path";
22
+ var MAX_CHECKS = 20;
23
+ var MAX_BYTES = 1e4;
24
+ function loadChecks(cwd) {
25
+ const dir = path.join(cwd, ".squint", "checks");
26
+ let entries;
27
+ try {
28
+ entries = fs.readdirSync(dir).filter((f) => f.endsWith(".js"));
29
+ } catch {
30
+ return [];
31
+ }
32
+ const checks = [];
33
+ for (const entry of entries.sort().slice(0, MAX_CHECKS)) {
34
+ try {
35
+ const source = fs.readFileSync(path.join(dir, entry), "utf8");
36
+ if (source.trim().length === 0 || Buffer.byteLength(source) > MAX_BYTES) continue;
37
+ checks.push({ name: entry.replace(/\.js$/, ""), source });
38
+ } catch {
39
+ }
40
+ }
41
+ return checks;
42
+ }
43
+
44
+ // src/preview/preview.ts
18
45
  var VIEWPORTS = [
19
46
  { name: "mobile", width: 390, height: 844 },
20
47
  { name: "tablet", width: 768, height: 1024 },
@@ -23,7 +50,7 @@ var VIEWPORTS = [
23
50
  function loadRoutes(cwd) {
24
51
  let lines = [];
25
52
  try {
26
- lines = fs.readFileSync(path.join(cwd, ".squint", "routes"), "utf8").split("\n").map((l) => l.trim()).filter((l) => l.length > 0 && !l.startsWith("#"));
53
+ lines = fs2.readFileSync(path2.join(cwd, ".squint", "routes"), "utf8").split("\n").map((l) => l.trim()).filter((l) => l.length > 0 && !l.startsWith("#"));
27
54
  } catch {
28
55
  }
29
56
  const routes = ["/", ...lines.filter((l) => l !== "/")];
@@ -34,8 +61,8 @@ function routeShotName(route) {
34
61
  return clean.length > 0 ? clean : "root";
35
62
  }
36
63
  function previewDir(cwd) {
37
- const dir = path.join(cwd, ".squint", "preview");
38
- fs.mkdirSync(dir, { recursive: true });
64
+ const dir = path2.join(cwd, ".squint", "preview");
65
+ fs2.mkdirSync(dir, { recursive: true });
39
66
  ensureSquintIgnore(cwd);
40
67
  return dir;
41
68
  }
@@ -47,7 +74,8 @@ async function captureViewports(cwd, url) {
47
74
  const base = url.replace(/\/+$/, "");
48
75
  if (hasWebSocket()) {
49
76
  try {
50
- const { report, shots: shots2, a11y, slop, narration, phantoms, viewTransitions } = await cdpCapture(chrome, url, dir, VIEWPORTS, 2500, true);
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);
51
79
  const errors2 = [];
52
80
  for (const route of routes.slice(1)) {
53
81
  try {
@@ -62,14 +90,14 @@ async function captureViewports(cwd, url) {
62
90
  errors2.push(`${route}: capture failed`);
63
91
  }
64
92
  }
65
- return { shots: shots2, errors: errors2, runtime: report, a11y, slop, narration, phantoms, viewTransitions };
93
+ return { shots: shots2, errors: errors2, runtime: report, a11y, slop, narration, phantoms, viewTransitions, components, checkFailures, webmcp };
66
94
  } catch {
67
95
  }
68
96
  }
69
97
  const shots = [];
70
98
  const errors = [];
71
99
  for (const viewport of VIEWPORTS) {
72
- const outPath = path.join(dir, `${viewport.name}.png`);
100
+ const outPath = path2.join(dir, `${viewport.name}.png`);
73
101
  const result = await screenshot(chrome, url, outPath, {
74
102
  width: viewport.width,
75
103
  height: viewport.height
@@ -118,14 +146,16 @@ async function probeRuntime(url, cwd) {
118
146
  if (!chrome || !hasWebSocket()) return null;
119
147
  try {
120
148
  const dir = cwd ? previewDir(cwd) : os.tmpdir();
121
- const { report, shots, perf } = await cdpCapture(
149
+ const { report, shots, perf, checkFailures } = await cdpCapture(
122
150
  chrome,
123
151
  url,
124
152
  dir,
125
153
  cwd ? [{ name: "pulse", width: 1280, height: 800 }] : [],
126
- 1500
154
+ 1500,
155
+ false,
156
+ cwd ? loadChecks(cwd) : []
127
157
  );
128
- return { report, pulsePath: shots[0]?.path, perf };
158
+ return { report, pulsePath: shots[0]?.path, perf, checkFailures };
129
159
  } catch {
130
160
  return null;
131
161
  }
@@ -133,7 +163,7 @@ async function probeRuntime(url, cwd) {
133
163
  async function comparePulse(previous, current) {
134
164
  const chrome = findChrome();
135
165
  if (!chrome || !hasWebSocket()) return null;
136
- const { pixelDiffPct } = await import("./cdp-C6MOKVEN.js");
166
+ const { pixelDiffPct } = await import("./cdp-OYRHOPVP.js");
137
167
  return pixelDiffPct(chrome, previous, current);
138
168
  }
139
169
  function buildRuntimeFixPrompt(report) {
@@ -161,6 +191,26 @@ ${narration.join("\n")}
161
191
 
162
192
  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.`;
163
193
  }
194
+ function webmcpSection(webmcp) {
195
+ if (!webmcp || webmcp.length === 0) return "";
196
+ return `
197
+
198
+ ## Page-declared WebMCP tools
199
+
200
+ ${webmcp.join("\n")}
201
+
202
+ The page registers these for agents via navigator.modelContext \u2014 keep them working, and prefer extending them over inventing parallel affordances.`;
203
+ }
204
+ function componentSection(components) {
205
+ if (!components || components.length === 0) return "";
206
+ return `
207
+
208
+ ## Component map (from React fibers)
209
+
210
+ ${components.join("\n")}
211
+
212
+ Use these owner chains to name the component (and file) an issue lives in instead of describing regions.`;
213
+ }
164
214
  function vtSection(viewTransitions) {
165
215
  if (!viewTransitions || viewTransitions.length === 0) return "";
166
216
  return `
@@ -191,13 +241,13 @@ ${findings.join("\n")}
191
241
 
192
242
  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.`;
193
243
  }
194
- function buildReviewPrompt(shots, extra, runtime, a11y, slop, narration, phantoms, viewTransitions) {
244
+ function buildReviewPrompt(shots, extra, runtime, a11y, slop, narration, phantoms, viewTransitions, components, webmcp) {
195
245
  const list = shots.map((s) => `- ${s.name}: ${s.path}`).join("\n");
196
246
  return `Screenshots of the running app were just captured:
197
247
 
198
248
  ${list}
199
249
 
200
- 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)}`;
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)}`;
201
251
  }
202
252
 
203
253
  export {
@@ -188,6 +188,7 @@ async function runFlow(chromePath, baseUrl, flow, outDir) {
188
188
  const { targetId } = await connection.send("Target.createTarget", { url: "about:blank" });
189
189
  const { sessionId } = await connection.send("Target.attachToTarget", { targetId, flatten: true });
190
190
  await connection.send("Page.enable", {}, sessionId);
191
+ await connection.send("Page.addScriptToEvaluateOnNewDocument", { source: WEBMCP_SHIM }, sessionId).catch(() => null);
191
192
  await connection.send(
192
193
  "Emulation.setDeviceMetricsOverride",
193
194
  { width: 1280, height: 800, deviceScaleFactor: 1, mobile: false },
@@ -400,7 +401,57 @@ var VT_AUDIT = `(() => {
400
401
  }
401
402
  return findings;
402
403
  })()`;
403
- async function cdpCapture(chromePath, url, outDir, viewports, settleMs = 2500, audit = false) {
404
+ var FIBER_AUDIT = `(() => {
405
+ const fiberKey = (el) => Object.keys(el).find((k) => k.startsWith('__reactFiber$'));
406
+ const all = document.querySelectorAll('*');
407
+ let reactSeen = false;
408
+ for (let i = 0; i < all.length && i < 300; i++) {
409
+ if (fiberKey(all[i])) { reactSeen = true; break; }
410
+ }
411
+ if (!reactSeen) return [];
412
+ const nameOf = (t) => {
413
+ if (typeof t === 'function') return t.displayName || t.name || '';
414
+ if (t && typeof t === 'object') return t.displayName || (t.render && (t.render.displayName || t.render.name)) || '';
415
+ return '';
416
+ };
417
+ const chainFor = (el) => {
418
+ const key = fiberKey(el);
419
+ if (!key) return null;
420
+ let fiber = el[key];
421
+ const names = [];
422
+ let hops = 0;
423
+ while (fiber && hops < 50 && names.length < 3) {
424
+ const n = nameOf(fiber.type);
425
+ if (n && !names.includes(n)) names.push(n);
426
+ fiber = fiber.return;
427
+ hops++;
428
+ }
429
+ return names.length > 0 ? names.join(' < ') : null;
430
+ };
431
+ const out = [];
432
+ for (const sel of ['header', 'nav', 'main', 'footer', 'h1', 'h2', 'form', 'aside', '[role="dialog"]', 'table']) {
433
+ const el = document.querySelector(sel);
434
+ if (!el) continue;
435
+ const chain = chainFor(el);
436
+ if (chain) out.push(sel + ' \u2014 ' + chain);
437
+ if (out.length >= 10) break;
438
+ }
439
+ return out;
440
+ })()`;
441
+ var WEBMCP_SHIM = `(() => {
442
+ window.__squintWebMcp = [];
443
+ const record = (tools) => {
444
+ for (const t of tools || []) {
445
+ if (t && t.name) window.__squintWebMcp.push(t.name + (t.description ? ' \u2014 ' + t.description : ''));
446
+ }
447
+ };
448
+ const target = navigator.modelContext || (navigator.modelContext = {});
449
+ const provide = target.provideContext && target.provideContext.bind(target);
450
+ target.provideContext = (params) => { record(params && params.tools); return provide ? provide(params) : undefined; };
451
+ const register = target.registerTool && target.registerTool.bind(target);
452
+ target.registerTool = (tool) => { record([tool]); return register ? register(tool) : undefined; };
453
+ })()`;
454
+ async function cdpCapture(chromePath, url, outDir, viewports, settleMs = 2500, audit = false, checks = []) {
404
455
  const { child, wsUrl, profileDir } = await launchChrome(chromePath);
405
456
  const report = { consoleErrors: [], pageErrors: [], failedRequests: [] };
406
457
  const shots = [];
@@ -410,6 +461,9 @@ async function cdpCapture(chromePath, url, outDir, viewports, settleMs = 2500, a
410
461
  let narration = [];
411
462
  let phantoms = [];
412
463
  let viewTransitions = [];
464
+ let components = [];
465
+ const checkFailures = [];
466
+ let webmcp = [];
413
467
  const requests = /* @__PURE__ */ new Map();
414
468
  let connection = null;
415
469
  try {
@@ -448,6 +502,7 @@ async function cdpCapture(chromePath, url, outDir, viewports, settleMs = 2500, a
448
502
  await connection.send("Runtime.enable", {}, sessionId);
449
503
  await connection.send("Network.enable", {}, sessionId);
450
504
  await connection.send("Page.enable", {}, sessionId);
505
+ await connection.send("Page.addScriptToEvaluateOnNewDocument", { source: WEBMCP_SHIM }, sessionId).catch(() => null);
451
506
  await connection.send("Page.navigate", { url }, sessionId);
452
507
  const deadline = Date.now() + 12e3;
453
508
  while (!loaded && Date.now() < deadline) {
@@ -463,6 +518,31 @@ async function cdpCapture(chromePath, url, outDir, viewports, settleMs = 2500, a
463
518
  if (result?.value && typeof result.value === "object") perf = result.value;
464
519
  } catch {
465
520
  }
521
+ try {
522
+ const { result } = await connection.send(
523
+ "Runtime.evaluate",
524
+ { expression: "window.__squintWebMcp || []", returnByValue: true },
525
+ sessionId
526
+ );
527
+ if (Array.isArray(result?.value)) webmcp = result.value.map(String).slice(0, 12);
528
+ } catch {
529
+ }
530
+ for (const check of checks) {
531
+ try {
532
+ const { result, exceptionDetails } = await connection.send(
533
+ "Runtime.evaluate",
534
+ { expression: check.source, returnByValue: true, timeout: 2e3 },
535
+ sessionId
536
+ );
537
+ if (exceptionDetails) {
538
+ checkFailures.push(`${check.name}: threw ${exceptionDetails.exception?.description?.split("\n")[0] ?? exceptionDetails.text ?? "an error"}`);
539
+ } else if (Array.isArray(result?.value)) {
540
+ for (const finding of result.value.slice(0, 5)) checkFailures.push(`${check.name}: ${String(finding)}`);
541
+ }
542
+ } catch {
543
+ }
544
+ if (checkFailures.length >= 15) break;
545
+ }
466
546
  if (audit) {
467
547
  try {
468
548
  const { result } = await connection.send(
@@ -500,6 +580,15 @@ async function cdpCapture(chromePath, url, outDir, viewports, settleMs = 2500, a
500
580
  if (Array.isArray(result?.value)) viewTransitions = result.value.map(String);
501
581
  } catch {
502
582
  }
583
+ try {
584
+ const { result } = await connection.send(
585
+ "Runtime.evaluate",
586
+ { expression: FIBER_AUDIT, returnByValue: true },
587
+ sessionId
588
+ );
589
+ if (Array.isArray(result?.value)) components = result.value.map(String);
590
+ } catch {
591
+ }
503
592
  try {
504
593
  await connection.send("Accessibility.enable", {}, sessionId);
505
594
  const { nodes } = await connection.send("Accessibility.getFullAXTree", {}, sessionId);
@@ -564,7 +653,7 @@ async function cdpCapture(chromePath, url, outDir, viewports, settleMs = 2500, a
564
653
  child.kill("SIGKILL");
565
654
  setTimeout(() => fs.rmSync(profileDir, { recursive: true, force: true }), 500).unref?.();
566
655
  }
567
- return { report, shots, a11y, slop, perf, narration, phantoms, viewTransitions };
656
+ return { report, shots, a11y, slop, perf, narration, phantoms, viewTransitions, components, checkFailures, webmcp };
568
657
  }
569
658
 
570
659
  export {
package/dist/cli.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  completeCommand
4
- } from "./chunk-O7NWGLS3.js";
4
+ } from "./chunk-43NQNIJY.js";
5
5
  import {
6
6
  applySandbox,
7
7
  discardSandbox,
@@ -42,7 +42,7 @@ import {
42
42
  comparePulse,
43
43
  probeRuntime,
44
44
  runtimeSummary
45
- } from "./chunk-ZUEGDTAM.js";
45
+ } from "./chunk-D7Q3M6PA.js";
46
46
  import {
47
47
  clearState,
48
48
  loadState,
@@ -58,11 +58,11 @@ import {
58
58
  detectEngines,
59
59
  getEngine
60
60
  } from "./chunk-KVYGPLWW.js";
61
- import "./chunk-JFFNQIBU.js";
61
+ import "./chunk-X2RZJT4L.js";
62
62
  import {
63
63
  appendDecision,
64
64
  enrich
65
- } from "./chunk-YT6K2YIS.js";
65
+ } from "./chunk-ARDV4XH6.js";
66
66
  import "./chunk-K5QJMSJH.js";
67
67
  import {
68
68
  composePrompt
@@ -240,7 +240,7 @@ function registerEnv(program2) {
240
240
  }
241
241
  }
242
242
  const { findChrome: findChrome2 } = await import("./chrome-SBV3H77F.js");
243
- const { hasWebSocket } = await import("./cdp-C6MOKVEN.js");
243
+ const { hasWebSocket } = await import("./cdp-OYRHOPVP.js");
244
244
  const chrome = findChrome2();
245
245
  console.log(
246
246
  chrome ? `${pc2.green("\u2713")} Chrome ${pc2.dim(chrome)}` : `${pc2.yellow("\u25CB")} Chrome ${pc2.dim("\u2014 screenshots and runtime probing disabled")}`
@@ -264,7 +264,7 @@ import pc3 from "picocolors";
264
264
  function registerProject(program2) {
265
265
  const skillsCommand = program2.command("skills").description("Project knowledge injected into asks (.squint/rules.md + .squint/skills/)");
266
266
  skillsCommand.command("list").description("Show always-on rules and trigger-matched skills").action(async () => {
267
- const { loadRules, loadSkills } = await import("./skills-GTTF4PNU.js");
267
+ const { loadRules, loadSkills } = await import("./skills-POB4ZZY5.js");
268
268
  const cwd = process.cwd();
269
269
  const rules = loadRules(cwd);
270
270
  console.log(
@@ -280,22 +280,22 @@ function registerProject(program2) {
280
280
  }
281
281
  });
282
282
  skillsCommand.command("init").description("Scaffold .squint/rules.md and an example skill").action(async () => {
283
- const fs3 = await import("fs");
283
+ const fs4 = await import("fs");
284
284
  const nodePath = await import("path");
285
285
  const cwd = process.cwd();
286
286
  const skillsDir = nodePath.join(cwd, ".squint", "skills");
287
- fs3.mkdirSync(skillsDir, { recursive: true });
287
+ fs4.mkdirSync(skillsDir, { recursive: true });
288
288
  const rules = nodePath.join(cwd, ".squint", "rules.md");
289
- if (!fs3.existsSync(rules)) {
290
- fs3.writeFileSync(
289
+ if (!fs4.existsSync(rules)) {
290
+ fs4.writeFileSync(
291
291
  rules,
292
292
  "# Project rules\n\nThese ride along on every squint ask. Keep them short \u2014 cut anything that would not cause a mistake if removed.\n"
293
293
  );
294
294
  console.log(pc3.green("\u2713 .squint/rules.md"));
295
295
  }
296
296
  const example = nodePath.join(skillsDir, "example.md");
297
- if (!fs3.existsSync(example)) {
298
- fs3.writeFileSync(
297
+ if (!fs4.existsSync(example)) {
298
+ fs4.writeFileSync(
299
299
  example,
300
300
  "---\ntriggers: example, sample\n---\n\nThis note is injected only when an ask mentions one of the triggers above.\nDocument the parts of this repo an agent would otherwise rediscover every time:\nwhere state lives, which helpers to reuse, what not to touch.\n"
301
301
  );
@@ -304,7 +304,7 @@ function registerProject(program2) {
304
304
  console.log(pc3.dim("rules are always-on; skills inject when an ask mentions a trigger"));
305
305
  });
306
306
  program2.command("brief").description("Set a committed design direction for this project (.squint/brief.md)").argument("[family]", "aesthetic family id (omit to list)").option("--force", "overwrite an existing project brief").action(async (familyId, options) => {
307
- const fs3 = await import("fs");
307
+ const fs4 = await import("fs");
308
308
  const nodePath = await import("path");
309
309
  const { FAMILIES, getFamily, renderFamilyBrief } = await import("./families-3ARYRBMH.js");
310
310
  if (!familyId) {
@@ -322,13 +322,13 @@ function registerProject(program2) {
322
322
  return;
323
323
  }
324
324
  const target = nodePath.join(process.cwd(), ".squint", "brief.md");
325
- if (fs3.existsSync(target) && !options.force) {
325
+ if (fs4.existsSync(target) && !options.force) {
326
326
  console.error(pc3.red(`\u2717 ${target} exists \u2014 use --force to overwrite`));
327
327
  process.exitCode = 1;
328
328
  return;
329
329
  }
330
- fs3.mkdirSync(nodePath.dirname(target), { recursive: true });
331
- fs3.writeFileSync(target, renderFamilyBrief(family) + "\n");
330
+ fs4.mkdirSync(nodePath.dirname(target), { recursive: true });
331
+ fs4.writeFileSync(target, renderFamilyBrief(family) + "\n");
332
332
  console.log(pc3.green(`\u2713 ${family.name} direction written to .squint/brief.md`));
333
333
  console.log(pc3.dim("every squint ask in this repo now holds this direction \u2014 edit the file to remix"));
334
334
  });
@@ -463,7 +463,7 @@ function registerQuality(program2) {
463
463
  if (results.some((r) => !r.ok)) process.exitCode = 1;
464
464
  });
465
465
  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) => {
466
- const { captureViewports: captureViewports2 } = await import("./preview-Q43LOK6P.js");
466
+ const { captureViewports: captureViewports2 } = await import("./preview-XREFPMHX.js");
467
467
  const result = await captureViewports2(process.cwd(), url);
468
468
  if (!result) {
469
469
  console.error(pc4.red("\u2717 no Chrome/Chromium found"));
@@ -581,21 +581,21 @@ Next: ${pc6.bold(`${cd}${options.install ? "" : "npm install && "}squint`)}`);
581
581
  console.log(pc6.dim("then describe what to build \u2014 /dev starts the preview server"));
582
582
  });
583
583
  program2.command("tag").description("Add the element picker to this Vite app (Alt+S in the browser \u2192 click \u2192 file:line:col)").action(async () => {
584
- const fs3 = await import("fs");
584
+ const fs4 = await import("fs");
585
585
  const nodePath = await import("path");
586
586
  const { patchViteConfig, TAGGER_FILENAME, TAGGER_SOURCE } = await import("./source-ZZU245VN.js");
587
587
  const cwd = process.cwd();
588
588
  const taggerPath = nodePath.join(cwd, TAGGER_FILENAME);
589
- fs3.writeFileSync(taggerPath, TAGGER_SOURCE);
589
+ fs4.writeFileSync(taggerPath, TAGGER_SOURCE);
590
590
  console.log(pc6.green(`\u2713 ${TAGGER_FILENAME} written`));
591
- const configPath = ["vite.config.ts", "vite.config.js", "vite.config.mjs"].map((name) => nodePath.join(cwd, name)).find((candidate) => fs3.existsSync(candidate));
591
+ const configPath = ["vite.config.ts", "vite.config.js", "vite.config.mjs"].map((name) => nodePath.join(cwd, name)).find((candidate) => fs4.existsSync(candidate));
592
592
  if (!configPath) {
593
593
  console.log(pc6.yellow("\u25CB no vite config found \u2014 add the plugin manually:"));
594
594
  console.log(pc6.dim(` import squintTagger from './${TAGGER_FILENAME}'
595
595
  plugins: [squintTagger(), \u2026]`));
596
596
  return;
597
597
  }
598
- const source = fs3.readFileSync(configPath, "utf8");
598
+ const source = fs4.readFileSync(configPath, "utf8");
599
599
  const patched = patchViteConfig(source);
600
600
  if (patched === "already") {
601
601
  console.log(pc6.dim("vite config already wired"));
@@ -604,7 +604,7 @@ Next: ${pc6.bold(`${cd}${options.install ? "" : "npm install && "}squint`)}`);
604
604
  console.log(pc6.dim(` import squintTagger from './${TAGGER_FILENAME}'
605
605
  plugins: [squintTagger(), \u2026]`));
606
606
  } else {
607
- fs3.writeFileSync(configPath, patched);
607
+ fs4.writeFileSync(configPath, patched);
608
608
  console.log(pc6.green(`\u2713 ${nodePath.basename(configPath)} wired`));
609
609
  }
610
610
  console.log(
@@ -623,6 +623,7 @@ import path4 from "path";
623
623
  import { useMemo, useRef, useState as useState2, useSyncExternalStore } from "react";
624
624
 
625
625
  // src/session/engine.ts
626
+ import fs3 from "fs";
626
627
  import path3 from "path";
627
628
 
628
629
  // src/session/hooks.ts
@@ -711,6 +712,7 @@ var Session = class {
711
712
  checkpoints = [];
712
713
  fixAttempts = 0;
713
714
  reviewTipShown = false;
715
+ pendingApproval = null;
714
716
  lastPulse = null;
715
717
  lastPerf = null;
716
718
  autoReviewedThisAsk = false;
@@ -825,7 +827,7 @@ ${question}`,
825
827
  return;
826
828
  }
827
829
  await this.runTurn(
828
- buildReviewPrompt(result.shots, void 0, result.runtime, result.a11y, result.slop, result.narration, result.phantoms, result.viewTransitions),
830
+ buildReviewPrompt(result.shots, void 0, result.runtime, result.a11y, result.slop, result.narration, result.phantoms, result.viewTransitions, result.components, result.webmcp),
829
831
  `\u{1F441} polish round ${round}/${rounds}`
830
832
  );
831
833
  }
@@ -1041,6 +1043,21 @@ ${question}`,
1041
1043
  const stat = checkpoint ? diffStatSince(this.opts.cwd, checkpoint.snapshot) : null;
1042
1044
  const work = stat ? ` \xB7 ${stat}` : this.turnEdits > 0 ? ` \xB7 ${this.turnEdits} edit${this.turnEdits === 1 ? "" : "s"}` : this.turnTools > 0 ? ` \xB7 ${this.turnTools} tool call${this.turnTools === 1 ? "" : "s"}` : "";
1043
1045
  this.push("status", `done${secs}${cost}${work}`);
1046
+ try {
1047
+ const reqPath = path3.join(this.opts.cwd, ".squint", "approval-request.json");
1048
+ if (fs3.existsSync(reqPath)) {
1049
+ const req = JSON.parse(fs3.readFileSync(reqPath, "utf8"));
1050
+ fs3.rmSync(reqPath, { force: true });
1051
+ if (typeof req?.summary === "string" && req.summary.length > 0) {
1052
+ this.pendingApproval = req.summary;
1053
+ const shot = typeof req.screenshot === "string" ? path3.resolve(this.execCwd(), req.screenshot) : null;
1054
+ if (shot && fs3.existsSync(shot)) this.push("image", shot);
1055
+ this.push("status", `\u23F8 approval requested: ${req.summary}
1056
+ /yes approves \xB7 /no rejects \xB7 or type feedback`);
1057
+ }
1058
+ }
1059
+ } catch {
1060
+ }
1044
1061
  if (checkpoint) {
1045
1062
  try {
1046
1063
  const { driftSummary, loadTokenIndex, scanDrift } = await import("./tokens-XYGRRAXC.js");
@@ -1158,6 +1175,21 @@ ${errors.slice(-5).join("\n")}`);
1158
1175
  this.clearProblems("dev");
1159
1176
  if (this.opts.autoProbe !== false && this.state.devUrl) {
1160
1177
  const probe = await probeRuntime(this.state.devUrl, this.opts.cwd);
1178
+ if (probe?.checkFailures && probe.checkFailures.length > 0) {
1179
+ this.addProblem(
1180
+ "check",
1181
+ `${probe.checkFailures.length} persistent check failure(s)`,
1182
+ `Repo checks in .squint/checks/ failed against the live page. Each line is <check>: <failure>:
1183
+
1184
+ ${probe.checkFailures.join("\n")}
1185
+
1186
+ Fix the page (or, only if the assertion itself is genuinely outdated, update that check file and say so).`
1187
+ );
1188
+ this.push("error", `checks: ${probe.checkFailures.length} failure(s)
1189
+ ${probe.checkFailures.slice(0, 5).join("\n")}`);
1190
+ } else {
1191
+ this.clearProblems("check");
1192
+ }
1161
1193
  const summary = probe ? runtimeSummary(probe.report) : null;
1162
1194
  if (probe && summary) {
1163
1195
  this.addProblem("runtime", summary, buildRuntimeFixPrompt(probe.report));
@@ -1175,7 +1207,7 @@ ${errors.slice(-5).join("\n")}`);
1175
1207
  const captureResult = await this.capture();
1176
1208
  if (captureResult) {
1177
1209
  await this.runTurn(
1178
- buildReviewPrompt(captureResult.shots, void 0, captureResult.runtime, captureResult.a11y, captureResult.slop, captureResult.narration, captureResult.phantoms, captureResult.viewTransitions),
1210
+ buildReviewPrompt(captureResult.shots, void 0, captureResult.runtime, captureResult.a11y, captureResult.slop, captureResult.narration, captureResult.phantoms, captureResult.viewTransitions, captureResult.components, captureResult.webmcp),
1179
1211
  "\u{1F441} auto-review rendered UI"
1180
1212
  );
1181
1213
  }
@@ -1463,13 +1495,32 @@ They are objective defects, not style preferences.`
1463
1495
  break;
1464
1496
  }
1465
1497
  case "context": {
1466
- import("./contextDoctor-M3R3PICP.js").then(({ contextReport, formatContextReport }) => {
1498
+ import("./contextDoctor-A26C2ZDN.js").then(({ contextReport, formatContextReport }) => {
1467
1499
  this.push("status", formatContextReport(contextReport(this.execCwd())));
1468
1500
  }).catch((error) => {
1469
1501
  this.push("status", `context report failed: ${error instanceof Error ? error.message : String(error)}`);
1470
1502
  });
1471
1503
  break;
1472
1504
  }
1505
+ case "yes":
1506
+ case "no": {
1507
+ if (!this.pendingApproval) {
1508
+ this.push("status", `nothing awaiting approval \u2014 /${name} answers an engine's approval request`);
1509
+ break;
1510
+ }
1511
+ const summary = this.pendingApproval;
1512
+ this.pendingApproval = null;
1513
+ const approved = name === "yes";
1514
+ appendDecision(this.opts.cwd, {
1515
+ decision: `${approved ? "approved" : "rejected"}: ${summary}${arg ? ` \u2014 ${arg}` : ""}`,
1516
+ source: "approval"
1517
+ });
1518
+ void this.runTurn(
1519
+ approved ? `Approved: "${summary}". Proceed.${arg ? ` Note from the user: ${arg}` : ""}` : `Rejected: "${summary}". Do not proceed with it.${arg ? ` The user says: ${arg}` : " Stop and await direction."}`,
1520
+ approved ? `\u2713 approved${arg ? ` \u2014 ${arg}` : ""}` : `\u2717 rejected${arg ? ` \u2014 ${arg}` : ""}`
1521
+ );
1522
+ break;
1523
+ }
1473
1524
  case "decide": {
1474
1525
  if (!arg) {
1475
1526
  this.push("status", "usage: /decide <the decision> \u2014 recorded in .squint/design-log.jsonl and injected into every future ask");
@@ -1500,13 +1551,13 @@ They are objective defects, not style preferences.`
1500
1551
  if (matches.length < 8) {
1501
1552
  void (async () => {
1502
1553
  try {
1503
- const fs3 = await import("fs");
1554
+ const fs4 = await import("fs");
1504
1555
  const path5 = await import("path");
1505
1556
  const dir = path5.join(this.opts.cwd, ".squint", "transcripts");
1506
- const files = fs3.readdirSync(dir).filter((f) => f.endsWith(".md")).sort().reverse().slice(0, 20);
1557
+ const files = fs4.readdirSync(dir).filter((f) => f.endsWith(".md")).sort().reverse().slice(0, 20);
1507
1558
  for (const file of files) {
1508
1559
  if (matches.length >= 8) break;
1509
- const text = fs3.readFileSync(path5.join(dir, file), "utf8");
1560
+ const text = fs4.readFileSync(path5.join(dir, file), "utf8");
1510
1561
  for (const line of text.split("\n")) {
1511
1562
  if (line.toLowerCase().includes(needle)) {
1512
1563
  matches.push(`[${file.replace(/\.md$/, "")}] ${line.trim().slice(0, 90)}`);
@@ -1529,10 +1580,10 @@ They are objective defects, not style preferences.`
1529
1580
  }
1530
1581
  case "save": {
1531
1582
  void (async () => {
1532
- const fs3 = await import("fs");
1583
+ const fs4 = await import("fs");
1533
1584
  const path5 = await import("path");
1534
1585
  const dir = path5.join(this.opts.cwd, ".squint", "transcripts");
1535
- fs3.mkdirSync(dir, { recursive: true });
1586
+ fs4.mkdirSync(dir, { recursive: true });
1536
1587
  const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-").slice(0, 19);
1537
1588
  const file = path5.join(dir, `${stamp}.md`);
1538
1589
  const lines = [`# squint session \u2014 ${(/* @__PURE__ */ new Date()).toISOString().slice(0, 16)}`, ""];
@@ -1557,7 +1608,7 @@ They are objective defects, not style preferences.`
1557
1608
  }
1558
1609
  }
1559
1610
  lines.push("", `> ${this.summary()}`);
1560
- fs3.writeFileSync(file, lines.join("\n") + "\n");
1611
+ fs4.writeFileSync(file, lines.join("\n") + "\n");
1561
1612
  this.push("status", `saved transcript \u2192 ${path5.relative(this.opts.cwd, file)}`);
1562
1613
  })();
1563
1614
  break;
@@ -1579,8 +1630,8 @@ They are objective defects, not style preferences.`
1579
1630
  this.push("error", "no Chrome/Chromium found for flows");
1580
1631
  return;
1581
1632
  }
1582
- const { runFlow } = await import("./cdp-C6MOKVEN.js");
1583
- const { previewDir } = await import("./preview-Q43LOK6P.js");
1633
+ const { runFlow } = await import("./cdp-OYRHOPVP.js");
1634
+ const { previewDir } = await import("./preview-XREFPMHX.js");
1584
1635
  this.push("status", `replaying ${flows.length} flow(s)\u2026`);
1585
1636
  this.notify({ running: true, runStartedAt: Date.now() });
1586
1637
  const failures = [];
@@ -1718,7 +1769,7 @@ They are objective defects, not style preferences.`
1718
1769
  const result = await this.capture();
1719
1770
  if (result) {
1720
1771
  await this.runTurn(
1721
- buildReviewPrompt(result.shots, arg || void 0, result.runtime, result.a11y, result.slop, result.narration, result.phantoms, result.viewTransitions),
1772
+ buildReviewPrompt(result.shots, arg || void 0, result.runtime, result.a11y, result.slop, result.narration, result.phantoms, result.viewTransitions, result.components, result.webmcp),
1722
1773
  `\u{1F441} review rendered UI${arg ? ` \xB7 ${arg}` : ""}`
1723
1774
  );
1724
1775
  }
@@ -1904,7 +1955,7 @@ ${sandboxFiles(this.opts.cwd).join("\n")}`);
1904
1955
  this.notify({ items: [], totals: { costUsd: 0, turns: 0 } });
1905
1956
  break;
1906
1957
  case "help": {
1907
- void import("./commands-VDICJJT2.js").then(({ commandHelp }) => this.push("status", commandHelp()));
1958
+ void import("./commands-BY44HDQ6.js").then(({ commandHelp }) => this.push("status", commandHelp()));
1908
1959
  break;
1909
1960
  }
1910
1961
  case "quit":
@@ -2523,7 +2574,7 @@ function registerTui(program2) {
2523
2574
  }
2524
2575
 
2525
2576
  // src/cli.tsx
2526
- var VERSION = true ? "0.4.4" : "0.0.0-dev";
2577
+ var VERSION = true ? "0.4.6" : "0.0.0-dev";
2527
2578
  var program = new Command();
2528
2579
  program.name("squint").description("Lovable for your terminal \u2014 a frontend harness on top of Claude Code, Codex, and friends.").version(VERSION);
2529
2580
  registerRun(program);
@@ -3,7 +3,7 @@ import {
3
3
  COMMANDS,
4
4
  commandHelp,
5
5
  completeCommand
6
- } from "./chunk-O7NWGLS3.js";
6
+ } from "./chunk-43NQNIJY.js";
7
7
  export {
8
8
  COMMANDS,
9
9
  commandHelp,
@@ -7,7 +7,7 @@ import {
7
7
  loadLocks,
8
8
  loadRules,
9
9
  loadSkills
10
- } from "./chunk-YT6K2YIS.js";
10
+ } from "./chunk-ARDV4XH6.js";
11
11
  import {
12
12
  loadBrief
13
13
  } from "./chunk-WAJXATCO.js";
@@ -54,6 +54,7 @@ function contextReport(cwd) {
54
54
  warnings.push(`skill "${skill.name}" has trigger(s) ${generic.map((t) => `"${t}"`).join(", ")} short enough to match almost any ask \u2014 make them more specific`);
55
55
  }
56
56
  }
57
+ lines.push({ source: "approval protocol (built-in)", tokens: 70, when: "every ask" });
57
58
  const totalAlways = lines.filter((l) => l.when === "every ask").reduce((sum, l) => sum + l.tokens, 0);
58
59
  if (totalAlways > 3e3) warnings.push(`~${totalAlways} tokens ride on every single ask \u2014 that is real money and real attention; trim the always-on set`);
59
60
  return { lines, warnings, totalAlways };
@@ -10,11 +10,11 @@ import {
10
10
  probeRuntime,
11
11
  routeShotName,
12
12
  runtimeSummary
13
- } from "./chunk-ZUEGDTAM.js";
13
+ } from "./chunk-D7Q3M6PA.js";
14
14
  import "./chunk-O2S6PAJE.js";
15
15
  import "./chunk-IMDRXXFU.js";
16
16
  import "./chunk-KVYGPLWW.js";
17
- import "./chunk-JFFNQIBU.js";
17
+ import "./chunk-X2RZJT4L.js";
18
18
  export {
19
19
  VIEWPORTS,
20
20
  buildReviewPrompt,
@@ -6,7 +6,7 @@ import {
6
6
  loadSkills,
7
7
  matchSkills,
8
8
  parseSkill
9
- } from "./chunk-YT6K2YIS.js";
9
+ } from "./chunk-ARDV4XH6.js";
10
10
  export {
11
11
  enrich,
12
12
  loadLocks,
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@aayambansal/squint",
3
- "version": "0.4.4",
4
- "description": "Lovable for your terminal a frontend harness on top of Claude Code, Codex, and other coding agents.",
3
+ "version": "0.4.6",
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",
7
7
  "author": "Aayam Bansal",