@aayambansal/squint 0.6.1 → 0.7.1
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 +17 -7
- package/dist/{App-54M5DS3T.js → App-VKYR3SNL.js} +7 -6
- package/dist/{cdp-IGPGFMC6.js → cdp-V47GTLXG.js} +1 -1
- package/dist/checks-2XOF2K3L.js +9 -0
- package/dist/{chunk-3XO3GF5X.js → chunk-CGOFK6EM.js} +2 -2
- package/dist/{chunk-UMDV4ITQ.js → chunk-GE6UL6C7.js} +11 -36
- package/dist/{chunk-3GHZOVAA.js → chunk-LNJ7O5O4.js} +1 -0
- package/dist/{chunk-N7YEWQGZ.js → chunk-S7LDOWMS.js} +1 -1
- package/dist/{chunk-SKNZ2PJF.js → chunk-SQAGOX4M.js} +17 -1
- package/dist/{chunk-ZG3XQDUW.js → chunk-XKKMU7VP.js} +4 -4
- package/dist/{chunk-5MQOJYUC.js → chunk-XPU3TFMH.js} +75 -20
- package/dist/chunk-ZNV63ZIK.js +60 -0
- package/dist/cli.js +20 -19
- package/dist/{commands-RVCKIMNV.js → commands-OBCLYABX.js} +1 -1
- package/dist/{flows-OY4BSCKD.js → flows-SYDNB5IW.js} +1 -1
- package/dist/{preview-4OF6JB5B.js → preview-NPLDCOP6.js} +3 -2
- package/dist/{receipts-JEOMBMFI.js → receipts-UFNVTZXE.js} +1 -1
- package/dist/{remote-LOQ5WKAS.js → remote-QHLHW7EI.js} +6 -5
- package/dist/{server-N2PGAV2C.js → server-RWLRX6YF.js} +10 -9
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -102,6 +102,7 @@ From source: `git clone https://github.com/aayambansal/squint.git && cd squint &
|
|
|
102
102
|
| --- | --- | --- |
|
|
103
103
|
| Claude Code | `claude` | `npm i -g @anthropic-ai/claude-code` |
|
|
104
104
|
| Codex CLI | `codex` | `npm i -g @openai/codex` |
|
|
105
|
+
| Codex (app-server) | `codex-app` | same binary — drives the published JSON-RPC protocol |
|
|
105
106
|
| Gemini CLI | `gemini` | `npm i -g @google/gemini-cli` |
|
|
106
107
|
| OpenCode | `opencode` | `npm i -g opencode-ai` |
|
|
107
108
|
| Amp | `amp` | `npm i -g @sourcegraph/amp` |
|
|
@@ -149,8 +150,10 @@ squint doctor --probe # run every engine end to end, verify auth act
|
|
|
149
150
|
- **Editing**: a real line editor — arrows move, `alt+←/→` jump words, `ctrl+a/e/k/u/w`,
|
|
150
151
|
`↑/↓` history. `ctrl+c` twice exits with a session summary.
|
|
151
152
|
- **Flows**: declare user journeys as six readable lines in `.squint/flows/`; `/flows`
|
|
152
|
-
replays them headlessly and failing steps join the fix loop
|
|
153
|
-
|
|
153
|
+
replays them headlessly and failing steps join the fix loop; `/flows suggest` drafts
|
|
154
|
+
a smoke flow per route from the live page's own headings. `/score` snapshots quality
|
|
155
|
+
deterministically. `/goal <objective>` arms a standing goal — auto-fix presses to 6
|
|
156
|
+
attempts until squint's checks come back clean.
|
|
154
157
|
- **Problems**: findings from gates, the dev server, the runtime probe, a11y sweeps, and flows
|
|
155
158
|
collect into a list — `/problems` shows it, `/fix` sends everything as one turn,
|
|
156
159
|
`/fix <n>` targets one. The footer counts what's open.
|
|
@@ -176,7 +179,11 @@ squint doctor --probe # run every engine end to end, verify auth act
|
|
|
176
179
|
the injected-context bill per source, with staleness warnings.
|
|
177
180
|
- **Two more doors in**: `squint mcp` serves the gates as MCP tools (any
|
|
178
181
|
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
|
|
182
|
+
whole loop headlessly in a pipeline — JSON report, non-zero exit on hard findings,
|
|
183
|
+
and a digest-sealed receipt per run at `.squint/receipts/` tying the green claim
|
|
184
|
+
to the exact pixels it was green about.
|
|
185
|
+
- **The review lane**: `/lane on` — every ask gets a second read-only reviewer in
|
|
186
|
+
fresh context over the diff alone; blind spots don't inherit.
|
|
180
187
|
- **The sentinel**: gate evasion (deleted tests, added skips, suppressed
|
|
181
188
|
diagnostics, weakened checks, locked-path touches) detected deterministically per
|
|
182
189
|
turn and reported to you — never sent back to the engine that did it.
|
|
@@ -189,8 +196,10 @@ squint doctor --probe # run every engine end to end, verify auth act
|
|
|
189
196
|
- **Visual approval**: engines ask before contested changes — the request renders
|
|
190
197
|
with its screenshot, `/yes` / `/no` answer it, the ledger remembers.
|
|
191
198
|
- **The design ledger**: `/decide` (plus chosen variants, rollbacks, accepted
|
|
192
|
-
sandboxes) appends to a committed `.squint/design-log.jsonl`; recent
|
|
193
|
-
into every ask so they stop getting silently undone between
|
|
199
|
+
sandboxes, approvals) appends to a committed `.squint/design-log.jsonl`; recent
|
|
200
|
+
decisions ride into every ask so they stop getting silently undone between
|
|
201
|
+
sessions. `/distill` compresses the ledger into always-on rules and proposed
|
|
202
|
+
persistent checks — accumulated taste becomes deterministic gates.
|
|
194
203
|
- **`/btw <question>`** asks about the codebase read-only without touching the main
|
|
195
204
|
thread's context. `.squint/locks` lists paths the engine must never touch; `/save`
|
|
196
205
|
exports the transcript as markdown.
|
|
@@ -235,11 +244,12 @@ and an avoid-list tuned to that family's failure modes. Plain markdown, made to
|
|
|
235
244
|
| stream | | headless screenshots |
|
|
236
245
|
+------+-------+ +-------------------------+
|
|
237
246
|
|
|
|
238
|
-
claude . codex . gemini . opencode . amp . cursor . copilot . aider
|
|
247
|
+
claude . codex . codex-app . gemini . opencode . amp . cursor . copilot . aider
|
|
239
248
|
```
|
|
240
249
|
|
|
241
250
|
Engines are dumb translators: build a headless invocation, parse the stream into one
|
|
242
|
-
normalized event model. Claude Code, Amp, and Cursor share a single wire-protocol
|
|
251
|
+
normalized event model. Claude Code, Amp, and Cursor share a single wire-protocol
|
|
252
|
+
parser; `codex-app` speaks the app-server JSON-RPC protocol through an embedded driver.
|
|
243
253
|
All product behavior lives in the harness, so a new engine is ~80 lines.
|
|
244
254
|
|
|
245
255
|
## Repo layout
|
|
@@ -1,24 +1,25 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
App
|
|
4
|
-
} from "./chunk-
|
|
5
|
-
import "./chunk-
|
|
6
|
-
import "./chunk-
|
|
4
|
+
} from "./chunk-XKKMU7VP.js";
|
|
5
|
+
import "./chunk-XPU3TFMH.js";
|
|
6
|
+
import "./chunk-KNPLVGZB.js";
|
|
7
|
+
import "./chunk-LNJ7O5O4.js";
|
|
8
|
+
import "./chunk-GE6UL6C7.js";
|
|
7
9
|
import "./chunk-UBADCBPT.js";
|
|
8
10
|
import "./chunk-IEQGTVWN.js";
|
|
9
11
|
import "./chunk-AUJJGMZG.js";
|
|
10
12
|
import "./chunk-YHRAOBI2.js";
|
|
11
|
-
import "./chunk-KNPLVGZB.js";
|
|
12
13
|
import "./chunk-GDGKEMBS.js";
|
|
13
14
|
import "./chunk-DOWWBLX2.js";
|
|
14
15
|
import "./chunk-K5QJMSJH.js";
|
|
15
16
|
import "./chunk-WAJXATCO.js";
|
|
16
17
|
import "./chunk-62JNF5M2.js";
|
|
17
|
-
import "./chunk-UMDV4ITQ.js";
|
|
18
18
|
import "./chunk-7CAGWFAQ.js";
|
|
19
|
-
import "./chunk-
|
|
19
|
+
import "./chunk-S7LDOWMS.js";
|
|
20
20
|
import "./chunk-PZV2FJE5.js";
|
|
21
21
|
import "./chunk-DABIMOMV.js";
|
|
22
|
+
import "./chunk-ZNV63ZIK.js";
|
|
22
23
|
export {
|
|
23
24
|
App
|
|
24
25
|
};
|
|
@@ -143,8 +143,8 @@ function stepExpression(step) {
|
|
|
143
143
|
async function suggestFlows(cwd, baseUrl, chromePath) {
|
|
144
144
|
const fs2 = await import("fs");
|
|
145
145
|
const path2 = await import("path");
|
|
146
|
-
const { cdpCapture } = await import("./cdp-
|
|
147
|
-
const { loadRoutes } = await import("./preview-
|
|
146
|
+
const { cdpCapture } = await import("./cdp-V47GTLXG.js");
|
|
147
|
+
const { loadRoutes } = await import("./preview-NPLDCOP6.js");
|
|
148
148
|
const dir = path2.join(cwd, ".squint", "flows");
|
|
149
149
|
fs2.mkdirSync(dir, { recursive: true });
|
|
150
150
|
const created = [];
|
|
@@ -5,44 +5,19 @@ import {
|
|
|
5
5
|
import {
|
|
6
6
|
cdpCapture,
|
|
7
7
|
hasWebSocket
|
|
8
|
-
} from "./chunk-
|
|
8
|
+
} from "./chunk-S7LDOWMS.js";
|
|
9
9
|
import {
|
|
10
10
|
findChrome,
|
|
11
11
|
screenshot
|
|
12
12
|
} from "./chunk-PZV2FJE5.js";
|
|
13
|
+
import {
|
|
14
|
+
loadChecks
|
|
15
|
+
} from "./chunk-ZNV63ZIK.js";
|
|
13
16
|
|
|
14
17
|
// src/preview/preview.ts
|
|
15
|
-
import fs2 from "fs";
|
|
16
|
-
import os from "os";
|
|
17
|
-
import path2 from "path";
|
|
18
|
-
|
|
19
|
-
// src/preview/checks.ts
|
|
20
18
|
import fs from "fs";
|
|
19
|
+
import os from "os";
|
|
21
20
|
import path from "path";
|
|
22
|
-
var MAX_CHECKS = 20;
|
|
23
|
-
var MAX_BYTES = 1e4;
|
|
24
|
-
function loadChecks(cwd, context = "audit") {
|
|
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
|
-
const pragma = /^\s*\/\/\s*squint-trigger:\s*(turn|audit)/.exec(source);
|
|
38
|
-
checks.push({ name: entry.replace(/\.js$/, ""), source, trigger: pragma?.[1] === "audit" ? "audit" : "turn" });
|
|
39
|
-
} catch {
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
return context === "audit" ? checks : checks.filter((c) => c.trigger === "turn");
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
// src/preview/preview.ts
|
|
46
21
|
var VIEWPORTS = [
|
|
47
22
|
{ name: "mobile", width: 390, height: 844 },
|
|
48
23
|
{ name: "tablet", width: 768, height: 1024 },
|
|
@@ -51,7 +26,7 @@ var VIEWPORTS = [
|
|
|
51
26
|
function loadRoutes(cwd) {
|
|
52
27
|
let lines = [];
|
|
53
28
|
try {
|
|
54
|
-
lines =
|
|
29
|
+
lines = fs.readFileSync(path.join(cwd, ".squint", "routes"), "utf8").split("\n").map((l) => l.trim()).filter((l) => l.length > 0 && !l.startsWith("#"));
|
|
55
30
|
} catch {
|
|
56
31
|
}
|
|
57
32
|
const routes = ["/", ...lines.filter((l) => l !== "/")];
|
|
@@ -62,8 +37,8 @@ function routeShotName(route) {
|
|
|
62
37
|
return clean.length > 0 ? clean : "root";
|
|
63
38
|
}
|
|
64
39
|
function previewDir(cwd) {
|
|
65
|
-
const dir =
|
|
66
|
-
|
|
40
|
+
const dir = path.join(cwd, ".squint", "preview");
|
|
41
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
67
42
|
ensureSquintIgnore(cwd);
|
|
68
43
|
return dir;
|
|
69
44
|
}
|
|
@@ -98,7 +73,7 @@ async function captureViewports(cwd, url) {
|
|
|
98
73
|
const shots = [];
|
|
99
74
|
const errors = [];
|
|
100
75
|
for (const viewport of VIEWPORTS) {
|
|
101
|
-
const outPath =
|
|
76
|
+
const outPath = path.join(dir, `${viewport.name}.png`);
|
|
102
77
|
const result = await screenshot(chrome, url, outPath, {
|
|
103
78
|
width: viewport.width,
|
|
104
79
|
height: viewport.height
|
|
@@ -164,13 +139,13 @@ async function probeRuntime(url, cwd) {
|
|
|
164
139
|
async function comparePulse(previous, current) {
|
|
165
140
|
const chrome = findChrome();
|
|
166
141
|
if (!chrome || !hasWebSocket()) return null;
|
|
167
|
-
const { pixelDiffPct } = await import("./cdp-
|
|
142
|
+
const { pixelDiffPct } = await import("./cdp-V47GTLXG.js");
|
|
168
143
|
return pixelDiffPct(chrome, previous, current);
|
|
169
144
|
}
|
|
170
145
|
async function comparePulseAttributed(previous, current, url, outPath) {
|
|
171
146
|
const chrome = findChrome();
|
|
172
147
|
if (!chrome || !hasWebSocket()) return null;
|
|
173
|
-
const { pixelDiffAttributed } = await import("./cdp-
|
|
148
|
+
const { pixelDiffAttributed } = await import("./cdp-V47GTLXG.js");
|
|
174
149
|
return pixelDiffAttributed(chrome, previous, current, url, outPath);
|
|
175
150
|
}
|
|
176
151
|
function buildRuntimeFixPrompt(report) {
|
|
@@ -29,6 +29,7 @@ var COMMANDS = [
|
|
|
29
29
|
{ name: "decide", args: "<text>", group: "session", description: "record a design decision; injected into every future ask" },
|
|
30
30
|
{ name: "goal", args: "[text|off]", group: "build", description: "arm a standing objective; auto-fix presses until checks are clean" },
|
|
31
31
|
{ name: "distill", group: "session", description: "compress the design ledger into rules.md lines and proposed checks" },
|
|
32
|
+
{ name: "lane", args: "on|off", group: "verify", description: "second read-only reviewer over each turn's diff, fresh context" },
|
|
32
33
|
{ name: "context", group: "session", description: "what squint injects per ask, token-costed, with staleness warnings" },
|
|
33
34
|
{ name: "yes", args: "[note]", group: "build", description: "approve the engine's pending visual-approval request" },
|
|
34
35
|
{ name: "no", args: "[note]", group: "build", description: "reject the engine's pending visual-approval request" },
|
|
@@ -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-
|
|
182
|
+
const { stepExpression } = await import("./flows-SYDNB5IW.js");
|
|
183
183
|
const { child, wsUrl, profileDir } = await launchChrome(chromePath);
|
|
184
184
|
const shots = [];
|
|
185
185
|
let connection = null;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
Session
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-XPU3TFMH.js";
|
|
5
5
|
|
|
6
6
|
// src/daemon/client.ts
|
|
7
7
|
import net from "net";
|
|
@@ -117,6 +117,21 @@ function startDaemon(opts) {
|
|
|
117
117
|
socket.on("close", drop);
|
|
118
118
|
socket.on("error", drop);
|
|
119
119
|
});
|
|
120
|
+
const lastRun = /* @__PURE__ */ new Map();
|
|
121
|
+
const sweep = setInterval(async () => {
|
|
122
|
+
const state = session.getState();
|
|
123
|
+
if (!state.devUrl || state.running) return;
|
|
124
|
+
try {
|
|
125
|
+
const { runIntervalSweep } = await import("./checks-2XOF2K3L.js");
|
|
126
|
+
const failures = await runIntervalSweep(opts.cwd, state.devUrl, lastRun);
|
|
127
|
+
if (failures.length > 0) {
|
|
128
|
+
session.note(`\u23F0 interval check(s) failing:
|
|
129
|
+
${failures.join("\n")}`);
|
|
130
|
+
}
|
|
131
|
+
} catch {
|
|
132
|
+
}
|
|
133
|
+
}, opts.intervalSweepMs ?? 6e4);
|
|
134
|
+
sweep.unref?.();
|
|
120
135
|
const unsubscribe = session.subscribe(() => {
|
|
121
136
|
const payload = serialize();
|
|
122
137
|
for (const client of clients) {
|
|
@@ -130,6 +145,7 @@ function startDaemon(opts) {
|
|
|
130
145
|
session,
|
|
131
146
|
clientCount: () => clients.length,
|
|
132
147
|
close: () => {
|
|
148
|
+
clearInterval(sweep);
|
|
133
149
|
unsubscribe();
|
|
134
150
|
for (const client of clients) client.destroy();
|
|
135
151
|
server.close();
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
3
|
-
completeCommand
|
|
4
|
-
} from "./chunk-3GHZOVAA.js";
|
|
5
2
|
import {
|
|
6
3
|
Session
|
|
7
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-XPU3TFMH.js";
|
|
5
|
+
import {
|
|
6
|
+
completeCommand
|
|
7
|
+
} from "./chunk-LNJ7O5O4.js";
|
|
8
8
|
|
|
9
9
|
// src/tui/App.tsx
|
|
10
10
|
import { Box as Box3, Static, Text as Text3, useApp, useInput } from "ink";
|
|
@@ -1,4 +1,18 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
DevServer,
|
|
4
|
+
buildFixPrompt,
|
|
5
|
+
detectDevCommand,
|
|
6
|
+
screenshotVariants
|
|
7
|
+
} from "./chunk-KNPLVGZB.js";
|
|
8
|
+
import {
|
|
9
|
+
buildReviewPrompt,
|
|
10
|
+
buildRuntimeFixPrompt,
|
|
11
|
+
captureViewports,
|
|
12
|
+
comparePulseAttributed,
|
|
13
|
+
probeRuntime,
|
|
14
|
+
runtimeSummary
|
|
15
|
+
} from "./chunk-GE6UL6C7.js";
|
|
2
16
|
import {
|
|
3
17
|
enrich
|
|
4
18
|
} from "./chunk-UBADCBPT.js";
|
|
@@ -20,12 +34,6 @@ import {
|
|
|
20
34
|
restoreSnapshot,
|
|
21
35
|
takeSnapshot
|
|
22
36
|
} from "./chunk-YHRAOBI2.js";
|
|
23
|
-
import {
|
|
24
|
-
DevServer,
|
|
25
|
-
buildFixPrompt,
|
|
26
|
-
detectDevCommand,
|
|
27
|
-
screenshotVariants
|
|
28
|
-
} from "./chunk-KNPLVGZB.js";
|
|
29
37
|
import {
|
|
30
38
|
applyVariant,
|
|
31
39
|
cleanVariants,
|
|
@@ -44,14 +52,6 @@ import {
|
|
|
44
52
|
detectGates,
|
|
45
53
|
runGates
|
|
46
54
|
} from "./chunk-62JNF5M2.js";
|
|
47
|
-
import {
|
|
48
|
-
buildReviewPrompt,
|
|
49
|
-
buildRuntimeFixPrompt,
|
|
50
|
-
captureViewports,
|
|
51
|
-
comparePulseAttributed,
|
|
52
|
-
probeRuntime,
|
|
53
|
-
runtimeSummary
|
|
54
|
-
} from "./chunk-UMDV4ITQ.js";
|
|
55
55
|
import {
|
|
56
56
|
clearState,
|
|
57
57
|
loadState,
|
|
@@ -157,6 +157,8 @@ var Session = class {
|
|
|
157
157
|
reviewTipShown = false;
|
|
158
158
|
pendingApproval = null;
|
|
159
159
|
goal = null;
|
|
160
|
+
laneEnabled = false;
|
|
161
|
+
inLane = false;
|
|
160
162
|
lastPulse = null;
|
|
161
163
|
lastPerf = null;
|
|
162
164
|
autoReviewedThisAsk = false;
|
|
@@ -678,8 +680,49 @@ ${probe.checkFailures.slice(0, 5).join("\n")}`);
|
|
|
678
680
|
}
|
|
679
681
|
}
|
|
680
682
|
this.notify({ running: false });
|
|
683
|
+
await this.maybeLaneReview(display);
|
|
681
684
|
this.drainQueue();
|
|
682
685
|
}
|
|
686
|
+
/**
|
|
687
|
+
* The review lane: a second read-only pass in fresh context after
|
|
688
|
+
* each real ask — the diff, not the conversation, so it can't inherit
|
|
689
|
+
* the implementer's blind spots. Findings render as critique; nothing
|
|
690
|
+
* here edits. Costs a cheap turn (fixModel when set); off by default.
|
|
691
|
+
*/
|
|
692
|
+
async maybeLaneReview(display) {
|
|
693
|
+
if (!this.laneEnabled || this.inLane) return;
|
|
694
|
+
if (display.startsWith("\u26D1") || display.startsWith("\u{1F441}") || display.startsWith("\u{1F50E}") || display.startsWith("/distill")) return;
|
|
695
|
+
const checkpoint = this.checkpoints.at(-1);
|
|
696
|
+
if (!checkpoint) return;
|
|
697
|
+
let diff;
|
|
698
|
+
try {
|
|
699
|
+
const { execFileSync } = await import("child_process");
|
|
700
|
+
diff = execFileSync("git", ["diff", checkpoint.snapshot.stashHash ?? "HEAD"], {
|
|
701
|
+
cwd: this.execCwd(),
|
|
702
|
+
encoding: "utf8",
|
|
703
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
704
|
+
});
|
|
705
|
+
} catch {
|
|
706
|
+
return;
|
|
707
|
+
}
|
|
708
|
+
if (diff.trim().length === 0) return;
|
|
709
|
+
const capped = diff.length > 2e4 ? `${diff.slice(0, 2e4)}
|
|
710
|
+
[diff truncated]` : diff;
|
|
711
|
+
this.inLane = true;
|
|
712
|
+
try {
|
|
713
|
+
await this.runTurn(
|
|
714
|
+
`You are a second reviewer in fresh context. The diff below just landed for the ask "${display}". Review ONLY the diff \u2014 do not edit anything, do not run commands that modify state. Report at most 3 concrete findings ranked by severity (bugs, regressions, accessibility, design-system violations), each with file and line. If it is clean, say so in one line.
|
|
715
|
+
|
|
716
|
+
\`\`\`diff
|
|
717
|
+
${capped}
|
|
718
|
+
\`\`\``,
|
|
719
|
+
"\u{1F50E} lane review",
|
|
720
|
+
this.opts.fixModel
|
|
721
|
+
);
|
|
722
|
+
} finally {
|
|
723
|
+
this.inLane = false;
|
|
724
|
+
}
|
|
725
|
+
}
|
|
683
726
|
async submit(ask) {
|
|
684
727
|
this.fixAttempts = 0;
|
|
685
728
|
this.autoReviewedThisAsk = false;
|
|
@@ -769,7 +812,7 @@ squint verifies every turn \u2014 gates, runtime probe, page audits. Do not decl
|
|
|
769
812
|
this.push("image", pulsePath);
|
|
770
813
|
return null;
|
|
771
814
|
}
|
|
772
|
-
const { previewDir } = await import("./preview-
|
|
815
|
+
const { previewDir } = await import("./preview-NPLDCOP6.js");
|
|
773
816
|
const diff = await comparePulseAttributed(
|
|
774
817
|
previous,
|
|
775
818
|
current,
|
|
@@ -967,6 +1010,18 @@ They are objective defects, not style preferences.`
|
|
|
967
1010
|
}
|
|
968
1011
|
break;
|
|
969
1012
|
}
|
|
1013
|
+
case "lane": {
|
|
1014
|
+
if (arg === "on") {
|
|
1015
|
+
this.laneEnabled = true;
|
|
1016
|
+
this.push("status", "review lane on \u2014 every ask gets a second read-only pass over its diff (fixModel when set)");
|
|
1017
|
+
} else if (arg === "off") {
|
|
1018
|
+
this.laneEnabled = false;
|
|
1019
|
+
this.push("status", "review lane off");
|
|
1020
|
+
} else {
|
|
1021
|
+
this.push("status", `review lane is ${this.laneEnabled ? "on" : "off"} \u2014 /lane on|off`);
|
|
1022
|
+
}
|
|
1023
|
+
break;
|
|
1024
|
+
}
|
|
970
1025
|
case "distill": {
|
|
971
1026
|
void (async () => {
|
|
972
1027
|
const { loadDecisions } = await import("./designLog-S67J7SO2.js");
|
|
@@ -1135,7 +1190,7 @@ every ask now carries it; auto-fix presses to ${MAX_GOAL_FIX_ATTEMPTS} attempts
|
|
|
1135
1190
|
this.push("error", "no Chrome/Chromium found");
|
|
1136
1191
|
return;
|
|
1137
1192
|
}
|
|
1138
|
-
const { suggestFlows } = await import("./flows-
|
|
1193
|
+
const { suggestFlows } = await import("./flows-SYDNB5IW.js");
|
|
1139
1194
|
this.push("status", "drafting flows from the live routes\u2026");
|
|
1140
1195
|
const { created, skipped } = await suggestFlows(this.opts.cwd, this.state.devUrl, chrome);
|
|
1141
1196
|
if (created.length > 0) this.push("status", `drafted ${created.length} flow(s): ${created.join(", ")} \u2014 edit .squint/flows/*.flow or ask the engine to deepen them`);
|
|
@@ -1145,7 +1200,7 @@ every ask now carries it; auto-fix presses to ${MAX_GOAL_FIX_ATTEMPTS} attempts
|
|
|
1145
1200
|
break;
|
|
1146
1201
|
}
|
|
1147
1202
|
void (async () => {
|
|
1148
|
-
const { loadFlows } = await import("./flows-
|
|
1203
|
+
const { loadFlows } = await import("./flows-SYDNB5IW.js");
|
|
1149
1204
|
const flows = loadFlows(this.opts.cwd);
|
|
1150
1205
|
if (flows.length === 0) {
|
|
1151
1206
|
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");
|
|
@@ -1156,8 +1211,8 @@ every ask now carries it; auto-fix presses to ${MAX_GOAL_FIX_ATTEMPTS} attempts
|
|
|
1156
1211
|
this.push("error", "no Chrome/Chromium found for flows");
|
|
1157
1212
|
return;
|
|
1158
1213
|
}
|
|
1159
|
-
const { runFlow } = await import("./cdp-
|
|
1160
|
-
const { previewDir } = await import("./preview-
|
|
1214
|
+
const { runFlow } = await import("./cdp-V47GTLXG.js");
|
|
1215
|
+
const { previewDir } = await import("./preview-NPLDCOP6.js");
|
|
1161
1216
|
this.push("status", `replaying ${flows.length} flow(s)\u2026`);
|
|
1162
1217
|
this.notify({ running: true, runStartedAt: Date.now() });
|
|
1163
1218
|
const failures = [];
|
|
@@ -1481,7 +1536,7 @@ ${sandboxFiles(this.opts.cwd).join("\n")}`);
|
|
|
1481
1536
|
this.notify({ items: [], totals: { costUsd: 0, turns: 0 } });
|
|
1482
1537
|
break;
|
|
1483
1538
|
case "help": {
|
|
1484
|
-
void import("./commands-
|
|
1539
|
+
void import("./commands-OBCLYABX.js").then(({ commandHelp }) => this.push("status", commandHelp()));
|
|
1485
1540
|
break;
|
|
1486
1541
|
}
|
|
1487
1542
|
case "quit":
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/preview/checks.ts
|
|
4
|
+
import fs from "fs";
|
|
5
|
+
import path from "path";
|
|
6
|
+
var MAX_CHECKS = 20;
|
|
7
|
+
var MAX_BYTES = 1e4;
|
|
8
|
+
function loadChecks(cwd, context = "audit") {
|
|
9
|
+
const dir = path.join(cwd, ".squint", "checks");
|
|
10
|
+
let entries;
|
|
11
|
+
try {
|
|
12
|
+
entries = fs.readdirSync(dir).filter((f) => f.endsWith(".js"));
|
|
13
|
+
} catch {
|
|
14
|
+
return [];
|
|
15
|
+
}
|
|
16
|
+
const checks = [];
|
|
17
|
+
for (const entry of entries.sort().slice(0, MAX_CHECKS)) {
|
|
18
|
+
try {
|
|
19
|
+
const source = fs.readFileSync(path.join(dir, entry), "utf8");
|
|
20
|
+
if (source.trim().length === 0 || Buffer.byteLength(source) > MAX_BYTES) continue;
|
|
21
|
+
const pragma = /^\s*\/\/\s*squint-trigger:\s*(turn|audit|interval)(?::(\d+))?/.exec(source);
|
|
22
|
+
const trigger = pragma?.[1] === "audit" ? "audit" : pragma?.[1] === "interval" ? "interval" : "turn";
|
|
23
|
+
checks.push({
|
|
24
|
+
name: entry.replace(/\.js$/, ""),
|
|
25
|
+
source,
|
|
26
|
+
trigger,
|
|
27
|
+
intervalSec: trigger === "interval" ? Number.parseInt(pragma?.[2] ?? "300", 10) : void 0
|
|
28
|
+
});
|
|
29
|
+
} catch {
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
if (context === "audit") return checks.filter((c) => c.trigger !== "interval");
|
|
33
|
+
if (context === "interval") return checks.filter((c) => c.trigger === "interval");
|
|
34
|
+
return checks.filter((c) => c.trigger === "turn");
|
|
35
|
+
}
|
|
36
|
+
async function runIntervalSweep(cwd, url, lastRun) {
|
|
37
|
+
const now = Date.now();
|
|
38
|
+
const due = loadChecks(cwd, "interval").filter(
|
|
39
|
+
(c) => now - (lastRun.get(c.name) ?? 0) >= (c.intervalSec ?? 300) * 1e3
|
|
40
|
+
);
|
|
41
|
+
if (due.length === 0) return [];
|
|
42
|
+
for (const check of due) lastRun.set(check.name, now);
|
|
43
|
+
const { findChrome } = await import("./chrome-CAN7TVH6.js");
|
|
44
|
+
const chrome = findChrome();
|
|
45
|
+
if (!chrome) return [];
|
|
46
|
+
const os = await import("os");
|
|
47
|
+
const { cdpCapture } = await import("./cdp-V47GTLXG.js");
|
|
48
|
+
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "squint-interval-"));
|
|
49
|
+
try {
|
|
50
|
+
const result = await cdpCapture(chrome, url, tmp, [], 800, false, due);
|
|
51
|
+
return result.checkFailures;
|
|
52
|
+
} finally {
|
|
53
|
+
fs.rmSync(tmp, { recursive: true, force: true });
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export {
|
|
58
|
+
loadChecks,
|
|
59
|
+
runIntervalSweep
|
|
60
|
+
};
|
package/dist/cli.js
CHANGED
|
@@ -1,19 +1,20 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
3
|
-
App
|
|
4
|
-
} from "./chunk-ZG3XQDUW.js";
|
|
5
|
-
import "./chunk-3GHZOVAA.js";
|
|
6
2
|
import {
|
|
7
3
|
connectDaemon,
|
|
8
4
|
socketPath,
|
|
9
5
|
startDaemon
|
|
10
|
-
} from "./chunk-
|
|
11
|
-
import
|
|
6
|
+
} from "./chunk-SQAGOX4M.js";
|
|
7
|
+
import {
|
|
8
|
+
App
|
|
9
|
+
} from "./chunk-XKKMU7VP.js";
|
|
10
|
+
import "./chunk-XPU3TFMH.js";
|
|
11
|
+
import "./chunk-KNPLVGZB.js";
|
|
12
|
+
import "./chunk-LNJ7O5O4.js";
|
|
13
|
+
import "./chunk-GE6UL6C7.js";
|
|
12
14
|
import "./chunk-UBADCBPT.js";
|
|
13
15
|
import "./chunk-IEQGTVWN.js";
|
|
14
16
|
import "./chunk-AUJJGMZG.js";
|
|
15
17
|
import "./chunk-YHRAOBI2.js";
|
|
16
|
-
import "./chunk-KNPLVGZB.js";
|
|
17
18
|
import "./chunk-GDGKEMBS.js";
|
|
18
19
|
import {
|
|
19
20
|
runAgent
|
|
@@ -23,14 +24,14 @@ import {
|
|
|
23
24
|
composePrompt
|
|
24
25
|
} from "./chunk-WAJXATCO.js";
|
|
25
26
|
import "./chunk-62JNF5M2.js";
|
|
26
|
-
import "./chunk-UMDV4ITQ.js";
|
|
27
27
|
import "./chunk-7CAGWFAQ.js";
|
|
28
|
-
import "./chunk-
|
|
28
|
+
import "./chunk-S7LDOWMS.js";
|
|
29
29
|
import "./chunk-PZV2FJE5.js";
|
|
30
30
|
import {
|
|
31
31
|
detectEngines,
|
|
32
32
|
getEngine
|
|
33
33
|
} from "./chunk-DABIMOMV.js";
|
|
34
|
+
import "./chunk-ZNV63ZIK.js";
|
|
34
35
|
|
|
35
36
|
// src/cli.tsx
|
|
36
37
|
import { Command } from "commander";
|
|
@@ -187,18 +188,18 @@ function registerDaemon(program2) {
|
|
|
187
188
|
process.on("SIGTERM", stop);
|
|
188
189
|
});
|
|
189
190
|
program2.command("mcp").description("serve the gates as MCP tools over stdio (check, shot, flows, context)").action(async () => {
|
|
190
|
-
const { runMcpServer } = await import("./server-
|
|
191
|
+
const { runMcpServer } = await import("./server-RWLRX6YF.js");
|
|
191
192
|
runMcpServer(process.cwd());
|
|
192
193
|
});
|
|
193
194
|
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) => {
|
|
194
195
|
const cwd = process.cwd();
|
|
195
196
|
if (!opts.plain) {
|
|
196
197
|
try {
|
|
197
|
-
const { RemoteSession } = await import("./remote-
|
|
198
|
+
const { RemoteSession } = await import("./remote-QHLHW7EI.js");
|
|
198
199
|
const remote = await RemoteSession.connect(cwd);
|
|
199
200
|
const config = loadConfig(defaultPaths(cwd));
|
|
200
201
|
const { render: render2 } = await import("ink");
|
|
201
|
-
const { App: App2 } = await import("./App-
|
|
202
|
+
const { App: App2 } = await import("./App-VKYR3SNL.js");
|
|
202
203
|
const React = await import("react");
|
|
203
204
|
render2(
|
|
204
205
|
React.createElement(App2, {
|
|
@@ -315,7 +316,7 @@ function registerEnv(program2) {
|
|
|
315
316
|
}
|
|
316
317
|
}
|
|
317
318
|
const { findChrome } = await import("./chrome-CAN7TVH6.js");
|
|
318
|
-
const { hasWebSocket } = await import("./cdp-
|
|
319
|
+
const { hasWebSocket } = await import("./cdp-V47GTLXG.js");
|
|
319
320
|
const chrome = findChrome();
|
|
320
321
|
console.log(
|
|
321
322
|
chrome ? `${pc3.green("\u2713")} Chrome ${pc3.dim(chrome)}` : `${pc3.yellow("\u25CB")} Chrome ${pc3.dim("\u2014 screenshots and runtime probing disabled")}`
|
|
@@ -551,7 +552,7 @@ function registerQuality(program2) {
|
|
|
551
552
|
report.gates = gateResults.map((r) => ({ id: r.gate.id, ok: r.ok, durationMs: r.durationMs, outputTail: r.ok ? void 0 : r.outputTail }));
|
|
552
553
|
if (gateResults.some((r) => !r.ok)) failed = true;
|
|
553
554
|
if (opts.url) {
|
|
554
|
-
const { captureViewports, runtimeSummary, previewDir } = await import("./preview-
|
|
555
|
+
const { captureViewports, runtimeSummary, previewDir } = await import("./preview-NPLDCOP6.js");
|
|
555
556
|
const capture = await captureViewports(cwd, opts.url);
|
|
556
557
|
if (!capture) {
|
|
557
558
|
console.log(pc5.red("\u2717 audit: capture failed (Chrome missing or URL unreachable)"));
|
|
@@ -583,14 +584,14 @@ function registerQuality(program2) {
|
|
|
583
584
|
console.log(`${pc5.green("\u2713")} audit ${pc5.dim(`(${advisories.a11y.length} a11y, ${advisories.slop.length} slop, ${advisories.jank.length} jank advisories)`)}`);
|
|
584
585
|
}
|
|
585
586
|
}
|
|
586
|
-
const { loadFlows } = await import("./flows-
|
|
587
|
+
const { loadFlows } = await import("./flows-SYDNB5IW.js");
|
|
587
588
|
const flows = loadFlows(cwd);
|
|
588
589
|
if (flows.length > 0) {
|
|
589
590
|
const { findChrome } = await import("./chrome-CAN7TVH6.js");
|
|
590
591
|
const chrome = findChrome();
|
|
591
592
|
const flowReport = [];
|
|
592
593
|
if (chrome) {
|
|
593
|
-
const { runFlow } = await import("./cdp-
|
|
594
|
+
const { runFlow } = await import("./cdp-V47GTLXG.js");
|
|
594
595
|
for (const flow of flows) {
|
|
595
596
|
const result = await runFlow(chrome, opts.url, flow, previewDir(cwd));
|
|
596
597
|
flowReport.push({ name: flow.name, ok: result.ok, detail: result.ok ? void 0 : `step ${result.failedStep}: ${result.detail}` });
|
|
@@ -604,7 +605,7 @@ function registerQuality(program2) {
|
|
|
604
605
|
report.ok = !failed;
|
|
605
606
|
report.finishedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
606
607
|
try {
|
|
607
|
-
const { writeReceipt } = await import("./receipts-
|
|
608
|
+
const { writeReceipt } = await import("./receipts-UFNVTZXE.js");
|
|
608
609
|
console.log(pc5.dim(`receipt \u2192 ${writeReceipt(cwd, report)}`));
|
|
609
610
|
} catch {
|
|
610
611
|
}
|
|
@@ -616,7 +617,7 @@ function registerQuality(program2) {
|
|
|
616
617
|
if (failed) process.exitCode = 1;
|
|
617
618
|
});
|
|
618
619
|
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) => {
|
|
619
|
-
const { captureViewports } = await import("./preview-
|
|
620
|
+
const { captureViewports } = await import("./preview-NPLDCOP6.js");
|
|
620
621
|
const result = await captureViewports(process.cwd(), url);
|
|
621
622
|
if (!result) {
|
|
622
623
|
console.error(pc5.red("\u2717 no Chrome/Chromium found"));
|
|
@@ -803,7 +804,7 @@ function registerTui(program2) {
|
|
|
803
804
|
}
|
|
804
805
|
|
|
805
806
|
// src/cli.tsx
|
|
806
|
-
var VERSION = true ? "0.
|
|
807
|
+
var VERSION = true ? "0.7.1" : "0.0.0-dev";
|
|
807
808
|
var program = new Command();
|
|
808
809
|
program.name("squint").description("Lovable for your terminal \u2014 a frontend harness on top of Claude Code, Codex, and friends.").version(VERSION);
|
|
809
810
|
registerRun(program);
|
|
@@ -11,11 +11,12 @@ import {
|
|
|
11
11
|
probeRuntime,
|
|
12
12
|
routeShotName,
|
|
13
13
|
runtimeSummary
|
|
14
|
-
} from "./chunk-
|
|
14
|
+
} from "./chunk-GE6UL6C7.js";
|
|
15
15
|
import "./chunk-7CAGWFAQ.js";
|
|
16
|
-
import "./chunk-
|
|
16
|
+
import "./chunk-S7LDOWMS.js";
|
|
17
17
|
import "./chunk-PZV2FJE5.js";
|
|
18
18
|
import "./chunk-DABIMOMV.js";
|
|
19
|
+
import "./chunk-ZNV63ZIK.js";
|
|
19
20
|
export {
|
|
20
21
|
VIEWPORTS,
|
|
21
22
|
buildReviewPrompt,
|
|
@@ -2,23 +2,24 @@
|
|
|
2
2
|
import {
|
|
3
3
|
connectDaemon,
|
|
4
4
|
socketPath
|
|
5
|
-
} from "./chunk-
|
|
6
|
-
import "./chunk-
|
|
5
|
+
} from "./chunk-SQAGOX4M.js";
|
|
6
|
+
import "./chunk-XPU3TFMH.js";
|
|
7
|
+
import "./chunk-KNPLVGZB.js";
|
|
8
|
+
import "./chunk-GE6UL6C7.js";
|
|
7
9
|
import "./chunk-UBADCBPT.js";
|
|
8
10
|
import "./chunk-IEQGTVWN.js";
|
|
9
11
|
import "./chunk-AUJJGMZG.js";
|
|
10
12
|
import "./chunk-YHRAOBI2.js";
|
|
11
|
-
import "./chunk-KNPLVGZB.js";
|
|
12
13
|
import "./chunk-GDGKEMBS.js";
|
|
13
14
|
import "./chunk-DOWWBLX2.js";
|
|
14
15
|
import "./chunk-K5QJMSJH.js";
|
|
15
16
|
import "./chunk-WAJXATCO.js";
|
|
16
17
|
import "./chunk-62JNF5M2.js";
|
|
17
|
-
import "./chunk-UMDV4ITQ.js";
|
|
18
18
|
import "./chunk-7CAGWFAQ.js";
|
|
19
|
-
import "./chunk-
|
|
19
|
+
import "./chunk-S7LDOWMS.js";
|
|
20
20
|
import "./chunk-PZV2FJE5.js";
|
|
21
21
|
import "./chunk-DABIMOMV.js";
|
|
22
|
+
import "./chunk-ZNV63ZIK.js";
|
|
22
23
|
|
|
23
24
|
// src/daemon/remote.ts
|
|
24
25
|
var noteId = 0;
|
|
@@ -3,6 +3,11 @@ import {
|
|
|
3
3
|
contextReport,
|
|
4
4
|
formatContextReport
|
|
5
5
|
} from "./chunk-LESZB7DT.js";
|
|
6
|
+
import {
|
|
7
|
+
captureViewports,
|
|
8
|
+
previewDir,
|
|
9
|
+
runtimeSummary
|
|
10
|
+
} from "./chunk-GE6UL6C7.js";
|
|
6
11
|
import "./chunk-UBADCBPT.js";
|
|
7
12
|
import "./chunk-IEQGTVWN.js";
|
|
8
13
|
import "./chunk-WAJXATCO.js";
|
|
@@ -10,20 +15,16 @@ import {
|
|
|
10
15
|
detectGates,
|
|
11
16
|
runGates
|
|
12
17
|
} from "./chunk-62JNF5M2.js";
|
|
18
|
+
import "./chunk-7CAGWFAQ.js";
|
|
13
19
|
import {
|
|
14
20
|
loadFlows
|
|
15
|
-
} from "./chunk-
|
|
16
|
-
import
|
|
17
|
-
captureViewports,
|
|
18
|
-
previewDir,
|
|
19
|
-
runtimeSummary
|
|
20
|
-
} from "./chunk-UMDV4ITQ.js";
|
|
21
|
-
import "./chunk-7CAGWFAQ.js";
|
|
22
|
-
import "./chunk-N7YEWQGZ.js";
|
|
21
|
+
} from "./chunk-CGOFK6EM.js";
|
|
22
|
+
import "./chunk-S7LDOWMS.js";
|
|
23
23
|
import {
|
|
24
24
|
findChrome
|
|
25
25
|
} from "./chunk-PZV2FJE5.js";
|
|
26
26
|
import "./chunk-DABIMOMV.js";
|
|
27
|
+
import "./chunk-ZNV63ZIK.js";
|
|
27
28
|
|
|
28
29
|
// src/mcp/server.ts
|
|
29
30
|
import readline from "readline";
|
|
@@ -75,7 +76,7 @@ ${list.join("\n")}`);
|
|
|
75
76
|
if (flows.length === 0) return "no flows declared \u2014 add .squint/flows/<name>.flow";
|
|
76
77
|
const chrome = findChrome();
|
|
77
78
|
if (!chrome) return "no Chrome/Chromium found";
|
|
78
|
-
const { runFlow } = await import("./cdp-
|
|
79
|
+
const { runFlow } = await import("./cdp-V47GTLXG.js");
|
|
79
80
|
const lines = [];
|
|
80
81
|
for (const flow of flows) {
|
|
81
82
|
const result = await runFlow(chrome, url, flow, previewDir(cwd));
|
package/package.json
CHANGED