@reticlehq/vite-plugin 2.14.0 → 3.2.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.
package/dist/index.cjs CHANGED
@@ -47,13 +47,13 @@ __export(index_exports, {
47
47
  module.exports = __toCommonJS(index_exports);
48
48
  var import_node_fs6 = require("node:fs");
49
49
 
50
- // src/missing-token.ts
50
+ // src/token/missing-token.ts
51
51
  function missingTokenWarning(token) {
52
52
  if (token !== void 0 && token.length > 0) return void 0;
53
53
  return "[reticle] no pairing token was available when this dev server started, so the app will connect and be refused \u2014 you will see NO SESSION even though the SDK loads and the socket opens. The token is written by the Reticle daemon: start it (`reticle serve`, or let your agent start it) and then RESTART this dev server, because the value is inlined at config time.";
54
54
  }
55
55
 
56
- // src/ensure-token.ts
56
+ // src/token/ensure-token.ts
57
57
  var import_node_fs = require("node:fs");
58
58
  var import_node_crypto = require("node:crypto");
59
59
  var import_node_path = require("node:path");
@@ -83,313 +83,7 @@ var import_node_path8 = require("node:path");
83
83
  var import_core6 = require("@babel/core");
84
84
  var import_babel_plugin = __toESM(require("@reticlehq/babel-plugin"), 1);
85
85
 
86
- // ../core/dist/constants.js
87
- var RETICLE_DEFAULT_PORT = 4400;
88
- var RETICLE_WS_PATH = "/reticle";
89
- var RETICLE_CLIENT_HOST = "localhost";
90
- function bridgeWsUrl(port = RETICLE_DEFAULT_PORT, host = RETICLE_CLIENT_HOST) {
91
- return `ws://${host}:${String(port)}${RETICLE_WS_PATH}`;
92
- }
93
- var ReticleEnv = {
94
- /** Shared-secret the browser SDK must present in HELLO; absent ⇒ loopback-trust only. */
95
- TOKEN: "RETICLE_TOKEN",
96
- /** Bridge bind host. Defaults to loopback; setting anything else is opt-in remote exposure. */
97
- HOST: "RETICLE_HOST",
98
- /** Comma-separated WS Origin allow-list for the bridge. */
99
- ALLOWED_ORIGINS: "RETICLE_ALLOWED_ORIGINS",
100
- /** Bridge/daemon WS port override. */
101
- PORT: "RETICLE_PORT",
102
- /** Daemon state — pidfiles, discovery registry, logs. Defaults to `~/.reticle`. Overridable
103
- * because a read-only $HOME (sandboxed agent, locked-down Windows profile, container) otherwise
104
- * makes the daemon unstartable with a raw EACCES naming nothing. Reported by a Windows user. */
105
- STATE_DIR: "RETICLE_STATE_DIR",
106
- /** Attach to an already-running browser over CDP instead of launching one. */
107
- CDP_URL: "RETICLE_CDP_URL",
108
- /** Max simultaneous leased headless contexts in the browser pool (resource cap). */
109
- MAX_CONTEXTS: "RETICLE_MAX_CONTEXTS",
110
- /**
111
- * Inbound events per second before the bridge starts SAMPLING (never disconnecting).
112
- *
113
- * Raise it for a legitimately busy app — a streaming dashboard, a live grid — rather than accept
114
- * partial coverage. Free and local: the daemon runs on the same machine, so a higher ceiling costs
115
- * nothing to anyone.
116
- */
117
- MAX_MESSAGES_PER_SECOND: "RETICLE_MAX_MESSAGES_PER_SECOND",
118
- /** Bearer token required by the optional `reticle serve --http` verify endpoint. */
119
- VERIFY_TOKEN: "RETICLE_VERIFY_TOKEN",
120
- /** Ms of continuous idleness (no agent, no browser session, no lease) before the daemon self-exits;
121
- * `0` disables. Keeps Reticle from lingering on a user's machine after the editor closes. */
122
- IDLE_SHUTDOWN: "RETICLE_IDLE_SHUTDOWN_MS",
123
- /** Idle re-check cadence (default 30s). Overridable so daemon-lifecycle-test can watch a full
124
- * exit/wake cycle in seconds rather than minutes. */
125
- IDLE_CHECK: "RETICLE_IDLE_CHECK_MS",
126
- /**
127
- * Grace for a daemon with an agent ATTACHED. Longer than the base on purpose: quiet with a client
128
- * present means a slow install or a thinking human, not an unwanted daemon — a flat 5 minutes was
129
- * killing live runs mid-install. Derived from the base when unset.
130
- */
131
- IDLE_ATTACHED: "RETICLE_IDLE_ATTACHED_MS",
132
- /**
133
- * How often the daemon writes `reticle_daemon_alive`, so a GAP in its log is evidence it died.
134
- *
135
- * Overridable for the same reason the idle windows are: a spec that has to prove "a killed daemon
136
- * is distinguishable from a tidy one" cannot wait 30 seconds per beat, and a spec that instead
137
- * re-implements the cadence is insensitive to the thing it claims to guard.
138
- */
139
- HEARTBEAT: "RETICLE_HEARTBEAT_MS",
140
- /** Directory holding the auto-provisioned pairing token. Defaults to ~/.reticle; relocatable for CI. */
141
- PAIRING_TOKEN_DIR: "RETICLE_PAIRING_TOKEN_DIR",
142
- /** Force the durable causal journal off (`0`/`false`/`off`) or on (`1`/`true`/`on`); default on. */
143
- JOURNAL: "RETICLE_JOURNAL",
144
- /**
145
- * Verbose internal flow tracing for people working ON Reticle (`1`/`true`/`on`); default off.
146
- *
147
- * Distinct from the journal, which records what the AGENT did to the app. This records what
148
- * RETICLE did to answer it: one line per internal stage, with its duration and nesting, so a
149
- * developer can see which code a tool call actually went through and where the time went.
150
- * Off by default and free when off — a trace on every tool call is a cost on the hot path.
151
- */
152
- TRACE: "RETICLE_TRACE",
153
- /**
154
- * How many consecutive reconnects the MCP proxy attempts before it stops retrying and goes
155
- * dormant. Overridable for the same reason the idle windows are: the real budget takes MINUTES to
156
- * exhaust, so the one spec that proves the proxy SURVIVES exhaustion could not run at all without
157
- * shortening it. A budget nobody can reach in a test is a budget nobody tests.
158
- */
159
- RECONNECT_ATTEMPTS: "RETICLE_RECONNECT_ATTEMPTS",
160
- /** Quiet window before an abandoned MCP stdio proxy exits; `0` disables the watcher. */
161
- MCP_PROXY_IDLE: "RETICLE_MCP_PROXY_IDLE_MS"
162
- };
163
- var TRANSPORT_LIMITS = {
164
- MAX_MESSAGE_BYTES: 1024 * 1024,
165
- /**
166
- * Inbound events per second before the bridge SAMPLES rather than records everything.
167
- *
168
- * This was 1000, and an ordinary React app with an active query cache blew through it: the
169
- * reporter's FIRST `act_and_wait` of the session came back `unknown` with `unclean_capture` and a
170
- * four-figure drop count, and setting the env override to twenty times the default fixed it (#316).
171
- * Reticle was right to refuse the verdict — a sampled window cannot support one, and the guard that
172
- * catches false greens is blindest exactly there — but landing that on the first drive after an
173
- * install, recoverable only by knowing an environment variable exists and inventing a number for
174
- * it, is the worst possible place to spend the honesty.
175
- *
176
- * 20000 is the value that was measured to work on the page that reported it. The cap exists to stop
177
- * a PATHOLOGICAL page (an animation loop firing DOM mutations every frame), not to throttle a busy
178
- * one, and the ceiling it has to defend is cheap: the daemon is on the same machine and a typical
179
- * event is a few hundred bytes, so this is single-digit MB/s over loopback.
180
- *
181
- * Raising it does not raise what a runaway page can make the bridge HOLD, and that separation is
182
- * what makes the change safe. Memory is bounded independently by the ring buffer, which evicts on
183
- * `RING_BUFFER_DEFAULTS.MAX_BYTES` (this same constant, reached through that alias) as well as on
184
- * a count and an age. Grep for `MAX_BUFFER_BYTES` alone and it looks like a constant nobody reads,
185
- * which is exactly the wrong conclusion to draw before touching this number: the rate cap defends
186
- * parse cost, the ring buffer defends memory, and they are not substitutes.
187
- */
188
- MAX_MESSAGES_PER_SECOND: 2e4,
189
- MAX_SESSIONS: 32,
190
- MAX_PENDING_CONNECTIONS: 16,
191
- HELLO_TIMEOUT_MS: 5e3,
192
- MAX_BUFFER_BYTES: 8 * 1024 * 1024,
193
- MAX_SESSION_ID_LENGTH: 128,
194
- MAX_URL_LENGTH: 4096,
195
- MAX_TITLE_LENGTH: 512,
196
- MAX_ADAPTERS: 32,
197
- MAX_ADAPTER_NAME_LENGTH: 128,
198
- MAX_TOKEN_LENGTH: 512,
199
- MAX_COMMAND_ID_LENGTH: 128,
200
- MAX_COMMAND_NAME_LENGTH: 128,
201
- MAX_REF_LENGTH: 128,
202
- MAX_ERROR_LENGTH: 4096,
203
- /** Cap on a captured stack trace before it crosses the wire — the console observer and both React
204
- * error hooks (error-boundary, hydration-error) all truncate to this, so it is one fact. */
205
- MAX_STACK_LENGTH: 4e3,
206
- MAX_SERIALIZE_DEPTH: 8,
207
- MAX_COLLECTION_ITEMS: 200,
208
- MAX_OBJECT_KEYS: 200,
209
- MAX_STRING_LENGTH: 64 * 1024,
210
- /** Human review marks: the note the human types when flagging a mistake on the page. */
211
- MAX_MARK_NOTE_LENGTH: 2e3,
212
- /** Human review marks: the legible element label that pins the mark (e.g. "Submit button"). */
213
- MAX_MARK_LABEL_LENGTH: 256
214
- };
215
- var ReticleDir = {
216
- ROOT: ".reticle",
217
- CONTRACT_FILE: "contract.json",
218
- FLOWS_SUBDIR: "flows",
219
- BASELINES_SUBDIR: "baselines",
220
- /** cross-run memory — outcomes of past runs (the "did it behave like last time?" file). */
221
- PROJECT_FILE: "project.json",
222
- /**
223
- * the user's own record of what Reticle has done for them — .reticle/impact.json.
224
- *
225
- * Local only, never uploaded, and deliberately NOT part of telemetry: telemetry answers our
226
- * questions about the product; this answers the user's question about their own work.
227
- */
228
- IMPACT_FILE: "impact.json",
229
- /** what changes were SUPPOSED to make true —.reticle/intent.json (git-checked, reviewed) */
230
- INTENT_FILE: "intent.json",
231
- /** opt-in pixel baselines —.reticle/visual/<name>.png + <name>.diff.png. */
232
- VISUAL_SUBDIR: "visual",
233
- /** verification-run artifacts —.reticle/runs/<runId>.json (the OEM/CI-consumable verdict). */
234
- RUNS_SUBDIR: "runs",
235
- /** fail-to-pass bug capsules —.reticle/capsules/<id>.json (a minimal failing flow + its evidence). */
236
- CAPSULES_SUBDIR: "capsules",
237
- /** durable causal journal —.reticle/sessions/<id>/{events,actions}.jsonl (the substrate). */
238
- SESSIONS_SUBDIR: "sessions",
239
- /** append-only event ledger inside a session dir (one ReticleEvent per line). */
240
- JOURNAL_EVENTS_FILE: "events.jsonl",
241
- /** append-only action ledger inside a session dir (one JournalAction per line). */
242
- JOURNAL_ACTIONS_FILE: "actions.jsonl",
243
- /** learned expected-envelopes per route, accumulated across runs (the deviation-report baseline). */
244
- ENVELOPES_FILE: "envelopes.json",
245
- /** learned ambient (action-less churn) region map — excluded from settle/summaries/envelopes. */
246
- AMBIENT_FILE: "ambient.json",
247
- /** per-flow flake ledger — replay outcomes that decide intermittent-failure quarantine. */
248
- FLAKE_FILE: "flake.json",
249
- /**
250
- * the project's cloud binding — .reticle/cloud.json, written by `reticle link`. Git-checked and
251
- * non-secret: the project id, the API origin, and where its dashboard lives. The KEY lives in
252
- * ~/.reticle/credentials.json instead, because that one must never reach a repository.
253
- */
254
- CLOUD_LINK_FILE: "cloud.json",
255
- /**
256
- * local sync bookkeeping — .reticle/cloud-state.json. The pull cursor, when each half last ran,
257
- * and the last error. NOT git-checked: it describes this machine's conversation with the server,
258
- * and committing one machine's cursor would make every other machine skip what it had not seen.
259
- */
260
- CLOUD_STATE_FILE: "cloud-state.json",
261
- /**
262
- * triage decisions pulled BACK from the dashboard — .reticle/issues.json. What a human said about
263
- * a defect ("resolved", "not a bug"), so the HUD stops showing it and the next run does not
264
- * re-report it as though nobody had looked.
265
- */
266
- ISSUES_FILE: "issues.json",
267
- /** Per-flow assertion tiers recorded on each PASSING replay — the gate's anti-downgrade baseline. */
268
- TIERS_FILE: "assertion-tiers.json",
269
- /**
270
- * Auto-provisioned bridge pairing token, stored at ~/.reticle/pairing-token (mode 0600). Written by
271
- * the daemon, read Node-side by the build plugins to inject into connect. A browser sandbox cannot
272
- * read it, so a rogue localhost app can't present it — that's what stops cross-app session hijack.
273
- */
274
- PAIRING_TOKEN_FILE: "pairing-token"
275
- };
276
- var UpdateCheckIntervalMs = 24 * 60 * 60 * 1e3;
277
- var RING_BUFFER_DEFAULTS = {
278
- MAX_EVENTS: 2e3,
279
- MAX_AGE_MS: 6e4,
280
- MAX_BYTES: TRANSPORT_LIMITS.MAX_BUFFER_BYTES
281
- };
282
- var EventType = {
283
- DOM_ADDED: "dom.added",
284
- DOM_REMOVED: "dom.removed",
285
- DOM_ATTR: "dom.attr",
286
- DOM_TEXT: "dom.text",
287
- NET_REQUEST: "net.request",
288
- NET_PENDING: "net.pending",
289
- /** An SSE (EventSource) or WebSocket frame — a message on a long-lived streaming connection. */
290
- NET_STREAM: "net.stream",
291
- /** A web-perf metric a screenshot can't verify: LCP, cumulative layout shift, or a long task. */
292
- PERF: "perf",
293
- ROUTE_CHANGE: "route.change",
294
- CONSOLE_LOG: "console.log",
295
- CONSOLE_WARN: "console.warn",
296
- CONSOLE_ERROR: "console.error",
297
- CONSOLE_INFO: "console.info",
298
- CONSOLE_DEBUG: "console.debug",
299
- ERROR_UNCAUGHT: "error.uncaught",
300
- VISIBLE_SHOWN: "visible.shown",
301
- ANIM_START: "anim.start",
302
- ANIM_END: "anim.end",
303
- SCROLL_POSITION: "scroll.position",
304
- REVEAL_SHOWN: "reveal.shown",
305
- SIGNAL: "signal",
306
- STATE_CHANGE: "state.change",
307
- /** a write to localStorage/sessionStorage/cookies — `data: { area, key, old?, new? }` (values redacted). */
308
- STORAGE_CHANGE: "storage.change",
309
- /** page-level visibility/focus health (distinct from element-level VISIBLE_*). */
310
- PAGE_HEALTH: "page.health",
311
- /**
312
- * synthetic: the page called window.open, so the consequence of what was just clicked may live in
313
- * another browsing context this one cannot observe (an OAuth popup is the archetype).
314
- * `data: { href }` — the URL the page asked to open, when it named one.
315
- */
316
- CONTEXT_OPENED: "context.opened",
317
- /**
318
- * The app opened a native `alert`/`confirm`/`prompt` while Reticle was driving it.
319
- *
320
- * Recorded because Reticle ANSWERS these rather than letting them block — a native dialog halts
321
- * the main thread, and the SDK's own message pump is on that thread, so one `confirm` behind a
322
- * driven click made the tab permanently unresponsive with no recovery from inside the session.
323
- * Answering silently would trade a wedge for an invisible one, so the question the app asked, and
324
- * the answer given, ride out as an event.
325
- */
326
- DIALOG_OPENED: "dialog.opened",
327
- /** aggregated React commits over a throttle window (dev builds) — `data: { commits }`. Commit storms /
328
- * wasted re-renders show up here without a per-render flood. */
329
- RENDER_COMMIT: "render.commit",
330
- /** element focus moved — `data: { to, from, toBody }`. Focus dropping to body after an act is a regression. */
331
- FOCUS_CHANGE: "focus.change",
332
- /** browser → bridge: a human recording compiled in-page. */
333
- FLOW_RECORDED: "flow.recorded",
334
- /** synthetic: browser transport queue overflowed; events were dropped. `data: { dropped: number }`. */
335
- TRANSPORT_OVERFLOW: "transport.overflow",
336
- /**
337
- * synthetic: a per-channel cap truncated a batch (e.g. a DOM mutation flood). `data: { channel, dropped }`.
338
- * Marks downstream rollups/envelopes as built on incomplete data — a ledger that lies at scale is worse
339
- * than no ledger, so truncation is never silent.
340
- */
341
- TRUNCATED: "truncated",
342
- /**
343
- * synthetic: the SDK detected a region it CANNOT observe (a cross-origin iframe, a closed shadow root).
344
- * `data: { kind: BlindSpotKind, count }`. Surfaced on results as `coverage: partial` so a green never
345
- * implies it saw everything.
346
- */
347
- BLIND_SPOT: "blind-spot",
348
- /** synthetic: the SDK ITSELF failed (an observer threw). `data: { site, message, errorType }`.
349
- * Rides the existing bridge — no outbound request. See browser/observers/sdk-failure.ts. */
350
- SDK_FAILED: "sdk.failed",
351
- /**
352
- * synthetic (driven only): CDP/Playwright-authoritative network detail for a response the in-page
353
- * fetch/XHR wrapper also saw — full response headers + authoritative status/mimeType the page-side
354
- * wrapper can't reach. `data: { url, method?, status, headers, resourceType? }`. Merged onto the
355
- * matching in-page NET_REQUEST so the driven view never loses fidelity to an outside-in tool.
356
- */
357
- NET_DETAIL: "net.detail",
358
- /**
359
- * Live-control: browser → bridge. A human acted on the presenter panel.
360
- * `data: { kind: HumanControlKind; text?: string }`. Rides the existing EventMessage.
361
- */
362
- HUMAN_CONTROL: "human.control",
363
- /**
364
- * Human review: browser → bridge. A human pinned a mistake to an element on the running page
365
- * (the "annotate the bug where you see it" loop). `data` narrows to HumanMarkDataSchema — a note
366
- * plus a re-resolvable element anchor (and its source file:line when the framework stamped one) so
367
- * the agent that drains the mark knows exactly which element and which source to fix.
368
- */
369
- HUMAN_MARK: "human.mark",
370
- /**
371
- * The app produced a FILE — a Blob handed to `URL.createObjectURL`, usually saved by clicking an
372
- * anchor with `download`. `data: { filename?, mimeType, bytes, lines?, preview? }`. The one artifact
373
- * class no outside-the-browser tool can inspect: it never crosses the network, so there is no
374
- * request to intercept. See `observers/download.ts` for the defect that motivated it.
375
- */
376
- DOWNLOAD: "download"
377
- };
378
- var RETICLE_RENDER_PREHOOK = "__reticleRenderPreHook";
379
- var CONSOLE_LEVEL_PREFIX = "console.";
380
- var CONSOLE_LEVELS = [
381
- EventType.CONSOLE_LOG,
382
- EventType.CONSOLE_WARN,
383
- EventType.CONSOLE_ERROR,
384
- EventType.CONSOLE_INFO
385
- ].map((type) => type.slice(CONSOLE_LEVEL_PREFIX.length));
386
-
387
- // ../core/dist/source-constants.js
388
- var DATA_RETICLE_SOURCE_ATTR = "data-reticle-source";
389
- var RETICLE_ROOT_GLOBAL = "__RETICLE_ROOT__";
390
- var RETICLE_SDK_VERSION_GLOBAL = "__RETICLE_SDK_VERSION__";
391
-
392
- // ../../node_modules/.pnpm/zod@3.25.76/node_modules/zod/v3/external.js
86
+ // ../../../node_modules/.pnpm/zod@3.25.76/node_modules/zod/v3/external.js
393
87
  var external_exports = {};
394
88
  __export(external_exports, {
395
89
  BRAND: () => BRAND,
@@ -501,7 +195,7 @@ __export(external_exports, {
501
195
  void: () => voidType
502
196
  });
503
197
 
504
- // ../../node_modules/.pnpm/zod@3.25.76/node_modules/zod/v3/helpers/util.js
198
+ // ../../../node_modules/.pnpm/zod@3.25.76/node_modules/zod/v3/helpers/util.js
505
199
  var util;
506
200
  (function(util2) {
507
201
  util2.assertEqual = (_) => {
@@ -635,7 +329,7 @@ var getParsedType = (data) => {
635
329
  }
636
330
  };
637
331
 
638
- // ../../node_modules/.pnpm/zod@3.25.76/node_modules/zod/v3/ZodError.js
332
+ // ../../../node_modules/.pnpm/zod@3.25.76/node_modules/zod/v3/ZodError.js
639
333
  var ZodIssueCode = util.arrayToEnum([
640
334
  "invalid_type",
641
335
  "invalid_literal",
@@ -753,7 +447,7 @@ ZodError.create = (issues) => {
753
447
  return error;
754
448
  };
755
449
 
756
- // ../../node_modules/.pnpm/zod@3.25.76/node_modules/zod/v3/locales/en.js
450
+ // ../../../node_modules/.pnpm/zod@3.25.76/node_modules/zod/v3/locales/en.js
757
451
  var errorMap = (issue, _ctx) => {
758
452
  let message;
759
453
  switch (issue.code) {
@@ -856,7 +550,7 @@ var errorMap = (issue, _ctx) => {
856
550
  };
857
551
  var en_default = errorMap;
858
552
 
859
- // ../../node_modules/.pnpm/zod@3.25.76/node_modules/zod/v3/errors.js
553
+ // ../../../node_modules/.pnpm/zod@3.25.76/node_modules/zod/v3/errors.js
860
554
  var overrideErrorMap = en_default;
861
555
  function setErrorMap(map) {
862
556
  overrideErrorMap = map;
@@ -865,7 +559,7 @@ function getErrorMap() {
865
559
  return overrideErrorMap;
866
560
  }
867
561
 
868
- // ../../node_modules/.pnpm/zod@3.25.76/node_modules/zod/v3/helpers/parseUtil.js
562
+ // ../../../node_modules/.pnpm/zod@3.25.76/node_modules/zod/v3/helpers/parseUtil.js
869
563
  var makeIssue = (params) => {
870
564
  const { data, path, errorMaps, issueData } = params;
871
565
  const fullPath = [...path, ...issueData.path || []];
@@ -975,14 +669,14 @@ var isDirty = (x) => x.status === "dirty";
975
669
  var isValid = (x) => x.status === "valid";
976
670
  var isAsync = (x) => typeof Promise !== "undefined" && x instanceof Promise;
977
671
 
978
- // ../../node_modules/.pnpm/zod@3.25.76/node_modules/zod/v3/helpers/errorUtil.js
672
+ // ../../../node_modules/.pnpm/zod@3.25.76/node_modules/zod/v3/helpers/errorUtil.js
979
673
  var errorUtil;
980
674
  (function(errorUtil2) {
981
675
  errorUtil2.errToObj = (message) => typeof message === "string" ? { message } : message || {};
982
676
  errorUtil2.toString = (message) => typeof message === "string" ? message : message?.message;
983
677
  })(errorUtil || (errorUtil = {}));
984
678
 
985
- // ../../node_modules/.pnpm/zod@3.25.76/node_modules/zod/v3/types.js
679
+ // ../../../node_modules/.pnpm/zod@3.25.76/node_modules/zod/v3/types.js
986
680
  var ParseInputLazyPath = class {
987
681
  constructor(parent, value, path, key) {
988
682
  this._cachedPath = [];
@@ -4287,150 +3981,550 @@ var ZodPipeline = class _ZodPipeline extends ZodType {
4287
3981
  });
4288
3982
  }
4289
3983
  };
4290
- var ZodReadonly = class extends ZodType {
4291
- _parse(input) {
4292
- const result = this._def.innerType._parse(input);
4293
- const freeze = (data) => {
4294
- if (isValid(data)) {
4295
- data.value = Object.freeze(data.value);
4296
- }
4297
- return data;
4298
- };
4299
- return isAsync(result) ? result.then((data) => freeze(data)) : freeze(result);
4300
- }
4301
- unwrap() {
4302
- return this._def.innerType;
4303
- }
3984
+ var ZodReadonly = class extends ZodType {
3985
+ _parse(input) {
3986
+ const result = this._def.innerType._parse(input);
3987
+ const freeze = (data) => {
3988
+ if (isValid(data)) {
3989
+ data.value = Object.freeze(data.value);
3990
+ }
3991
+ return data;
3992
+ };
3993
+ return isAsync(result) ? result.then((data) => freeze(data)) : freeze(result);
3994
+ }
3995
+ unwrap() {
3996
+ return this._def.innerType;
3997
+ }
3998
+ };
3999
+ ZodReadonly.create = (type, params) => {
4000
+ return new ZodReadonly({
4001
+ innerType: type,
4002
+ typeName: ZodFirstPartyTypeKind.ZodReadonly,
4003
+ ...processCreateParams(params)
4004
+ });
4005
+ };
4006
+ function cleanParams(params, data) {
4007
+ const p = typeof params === "function" ? params(data) : typeof params === "string" ? { message: params } : params;
4008
+ const p2 = typeof p === "string" ? { message: p } : p;
4009
+ return p2;
4010
+ }
4011
+ function custom(check, _params = {}, fatal) {
4012
+ if (check)
4013
+ return ZodAny.create().superRefine((data, ctx) => {
4014
+ const r = check(data);
4015
+ if (r instanceof Promise) {
4016
+ return r.then((r2) => {
4017
+ if (!r2) {
4018
+ const params = cleanParams(_params, data);
4019
+ const _fatal = params.fatal ?? fatal ?? true;
4020
+ ctx.addIssue({ code: "custom", ...params, fatal: _fatal });
4021
+ }
4022
+ });
4023
+ }
4024
+ if (!r) {
4025
+ const params = cleanParams(_params, data);
4026
+ const _fatal = params.fatal ?? fatal ?? true;
4027
+ ctx.addIssue({ code: "custom", ...params, fatal: _fatal });
4028
+ }
4029
+ return;
4030
+ });
4031
+ return ZodAny.create();
4032
+ }
4033
+ var late = {
4034
+ object: ZodObject.lazycreate
4035
+ };
4036
+ var ZodFirstPartyTypeKind;
4037
+ (function(ZodFirstPartyTypeKind2) {
4038
+ ZodFirstPartyTypeKind2["ZodString"] = "ZodString";
4039
+ ZodFirstPartyTypeKind2["ZodNumber"] = "ZodNumber";
4040
+ ZodFirstPartyTypeKind2["ZodNaN"] = "ZodNaN";
4041
+ ZodFirstPartyTypeKind2["ZodBigInt"] = "ZodBigInt";
4042
+ ZodFirstPartyTypeKind2["ZodBoolean"] = "ZodBoolean";
4043
+ ZodFirstPartyTypeKind2["ZodDate"] = "ZodDate";
4044
+ ZodFirstPartyTypeKind2["ZodSymbol"] = "ZodSymbol";
4045
+ ZodFirstPartyTypeKind2["ZodUndefined"] = "ZodUndefined";
4046
+ ZodFirstPartyTypeKind2["ZodNull"] = "ZodNull";
4047
+ ZodFirstPartyTypeKind2["ZodAny"] = "ZodAny";
4048
+ ZodFirstPartyTypeKind2["ZodUnknown"] = "ZodUnknown";
4049
+ ZodFirstPartyTypeKind2["ZodNever"] = "ZodNever";
4050
+ ZodFirstPartyTypeKind2["ZodVoid"] = "ZodVoid";
4051
+ ZodFirstPartyTypeKind2["ZodArray"] = "ZodArray";
4052
+ ZodFirstPartyTypeKind2["ZodObject"] = "ZodObject";
4053
+ ZodFirstPartyTypeKind2["ZodUnion"] = "ZodUnion";
4054
+ ZodFirstPartyTypeKind2["ZodDiscriminatedUnion"] = "ZodDiscriminatedUnion";
4055
+ ZodFirstPartyTypeKind2["ZodIntersection"] = "ZodIntersection";
4056
+ ZodFirstPartyTypeKind2["ZodTuple"] = "ZodTuple";
4057
+ ZodFirstPartyTypeKind2["ZodRecord"] = "ZodRecord";
4058
+ ZodFirstPartyTypeKind2["ZodMap"] = "ZodMap";
4059
+ ZodFirstPartyTypeKind2["ZodSet"] = "ZodSet";
4060
+ ZodFirstPartyTypeKind2["ZodFunction"] = "ZodFunction";
4061
+ ZodFirstPartyTypeKind2["ZodLazy"] = "ZodLazy";
4062
+ ZodFirstPartyTypeKind2["ZodLiteral"] = "ZodLiteral";
4063
+ ZodFirstPartyTypeKind2["ZodEnum"] = "ZodEnum";
4064
+ ZodFirstPartyTypeKind2["ZodEffects"] = "ZodEffects";
4065
+ ZodFirstPartyTypeKind2["ZodNativeEnum"] = "ZodNativeEnum";
4066
+ ZodFirstPartyTypeKind2["ZodOptional"] = "ZodOptional";
4067
+ ZodFirstPartyTypeKind2["ZodNullable"] = "ZodNullable";
4068
+ ZodFirstPartyTypeKind2["ZodDefault"] = "ZodDefault";
4069
+ ZodFirstPartyTypeKind2["ZodCatch"] = "ZodCatch";
4070
+ ZodFirstPartyTypeKind2["ZodPromise"] = "ZodPromise";
4071
+ ZodFirstPartyTypeKind2["ZodBranded"] = "ZodBranded";
4072
+ ZodFirstPartyTypeKind2["ZodPipeline"] = "ZodPipeline";
4073
+ ZodFirstPartyTypeKind2["ZodReadonly"] = "ZodReadonly";
4074
+ })(ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {}));
4075
+ var instanceOfType = (cls, params = {
4076
+ message: `Input not instance of ${cls.name}`
4077
+ }) => custom((data) => data instanceof cls, params);
4078
+ var stringType = ZodString.create;
4079
+ var numberType = ZodNumber.create;
4080
+ var nanType = ZodNaN.create;
4081
+ var bigIntType = ZodBigInt.create;
4082
+ var booleanType = ZodBoolean.create;
4083
+ var dateType = ZodDate.create;
4084
+ var symbolType = ZodSymbol.create;
4085
+ var undefinedType = ZodUndefined.create;
4086
+ var nullType = ZodNull.create;
4087
+ var anyType = ZodAny.create;
4088
+ var unknownType = ZodUnknown.create;
4089
+ var neverType = ZodNever.create;
4090
+ var voidType = ZodVoid.create;
4091
+ var arrayType = ZodArray.create;
4092
+ var objectType = ZodObject.create;
4093
+ var strictObjectType = ZodObject.strictCreate;
4094
+ var unionType = ZodUnion.create;
4095
+ var discriminatedUnionType = ZodDiscriminatedUnion.create;
4096
+ var intersectionType = ZodIntersection.create;
4097
+ var tupleType = ZodTuple.create;
4098
+ var recordType = ZodRecord.create;
4099
+ var mapType = ZodMap.create;
4100
+ var setType = ZodSet.create;
4101
+ var functionType = ZodFunction.create;
4102
+ var lazyType = ZodLazy.create;
4103
+ var literalType = ZodLiteral.create;
4104
+ var enumType = ZodEnum.create;
4105
+ var nativeEnumType = ZodNativeEnum.create;
4106
+ var promiseType = ZodPromise.create;
4107
+ var effectsType = ZodEffects.create;
4108
+ var optionalType = ZodOptional.create;
4109
+ var nullableType = ZodNullable.create;
4110
+ var preprocessType = ZodEffects.createWithPreprocess;
4111
+ var pipelineType = ZodPipeline.create;
4112
+ var ostring = () => stringType().optional();
4113
+ var onumber = () => numberType().optional();
4114
+ var oboolean = () => booleanType().optional();
4115
+ var coerce = {
4116
+ string: ((arg) => ZodString.create({ ...arg, coerce: true })),
4117
+ number: ((arg) => ZodNumber.create({ ...arg, coerce: true })),
4118
+ boolean: ((arg) => ZodBoolean.create({
4119
+ ...arg,
4120
+ coerce: true
4121
+ })),
4122
+ bigint: ((arg) => ZodBigInt.create({ ...arg, coerce: true })),
4123
+ date: ((arg) => ZodDate.create({ ...arg, coerce: true }))
4124
+ };
4125
+ var NEVER = INVALID;
4126
+
4127
+ // ../../../core/dist/wire/constants/constants.js
4128
+ var RETICLE_DEFAULT_PORT = 4400;
4129
+ var RETICLE_WS_PATH = "/reticle";
4130
+ var RETICLE_CLIENT_HOST = "localhost";
4131
+ function bridgeWsUrl(port = RETICLE_DEFAULT_PORT, host = RETICLE_CLIENT_HOST) {
4132
+ return `ws://${host}:${String(port)}${RETICLE_WS_PATH}`;
4133
+ }
4134
+ var ReticleEnv = {
4135
+ /** Shared-secret the browser SDK must present in HELLO; absent ⇒ loopback-trust only. */
4136
+ TOKEN: "RETICLE_TOKEN",
4137
+ /** Bridge bind host. Defaults to loopback; setting anything else is opt-in remote exposure. */
4138
+ HOST: "RETICLE_HOST",
4139
+ /** Comma-separated WS Origin allow-list for the bridge. */
4140
+ ALLOWED_ORIGINS: "RETICLE_ALLOWED_ORIGINS",
4141
+ /** Bridge/daemon WS port override. */
4142
+ PORT: "RETICLE_PORT",
4143
+ /** Daemon state — pidfiles, discovery registry, logs. Defaults to `~/.reticle`. Overridable
4144
+ * because a read-only $HOME (sandboxed agent, locked-down Windows profile, container) otherwise
4145
+ * makes the daemon unstartable with a raw EACCES naming nothing. Reported by a Windows user. */
4146
+ STATE_DIR: "RETICLE_STATE_DIR",
4147
+ /** Attach to an already-running browser over CDP instead of launching one. */
4148
+ CDP_URL: "RETICLE_CDP_URL",
4149
+ /** Max simultaneous leased headless contexts in the browser pool (resource cap). */
4150
+ MAX_CONTEXTS: "RETICLE_MAX_CONTEXTS",
4151
+ /**
4152
+ * Inbound events per second before the bridge starts SAMPLING (never disconnecting).
4153
+ *
4154
+ * Raise it for a legitimately busy app — a streaming dashboard, a live grid — rather than accept
4155
+ * partial coverage. Free and local: the daemon runs on the same machine, so a higher ceiling costs
4156
+ * nothing to anyone.
4157
+ */
4158
+ MAX_MESSAGES_PER_SECOND: "RETICLE_MAX_MESSAGES_PER_SECOND",
4159
+ /** Bearer token required by the optional `reticle serve --http` verify endpoint. */
4160
+ VERIFY_TOKEN: "RETICLE_VERIFY_TOKEN",
4161
+ /** Ms of continuous idleness (no agent, no browser session, no lease) before the daemon self-exits;
4162
+ * `0` disables. Keeps Reticle from lingering on a user's machine after the editor closes. */
4163
+ IDLE_SHUTDOWN: "RETICLE_IDLE_SHUTDOWN_MS",
4164
+ /** Idle re-check cadence (default 30s). Overridable so daemon-lifecycle-test can watch a full
4165
+ * exit/wake cycle in seconds rather than minutes. */
4166
+ IDLE_CHECK: "RETICLE_IDLE_CHECK_MS",
4167
+ /**
4168
+ * Grace for a daemon with an agent ATTACHED. Longer than the base on purpose: quiet with a client
4169
+ * present means a slow install or a thinking human, not an unwanted daemon — a flat 5 minutes was
4170
+ * killing live runs mid-install. Derived from the base when unset.
4171
+ */
4172
+ IDLE_ATTACHED: "RETICLE_IDLE_ATTACHED_MS",
4173
+ /**
4174
+ * How often the daemon writes `reticle_daemon_alive`, so a GAP in its log is evidence it died.
4175
+ *
4176
+ * Overridable for the same reason the idle windows are: a spec that has to prove "a killed daemon
4177
+ * is distinguishable from a tidy one" cannot wait 30 seconds per beat, and a spec that instead
4178
+ * re-implements the cadence is insensitive to the thing it claims to guard.
4179
+ */
4180
+ HEARTBEAT: "RETICLE_HEARTBEAT_MS",
4181
+ /** Directory holding the auto-provisioned pairing token. Defaults to ~/.reticle; relocatable for CI. */
4182
+ PAIRING_TOKEN_DIR: "RETICLE_PAIRING_TOKEN_DIR",
4183
+ /** Force the durable causal journal off (`0`/`false`/`off`) or on (`1`/`true`/`on`); default on. */
4184
+ JOURNAL: "RETICLE_JOURNAL",
4185
+ /**
4186
+ * Verbose internal flow tracing for people working ON Reticle (`1`/`true`/`on`); default off.
4187
+ *
4188
+ * Distinct from the journal, which records what the AGENT did to the app. This records what
4189
+ * RETICLE did to answer it: one line per internal stage, with its duration and nesting, so a
4190
+ * developer can see which code a tool call actually went through and where the time went.
4191
+ * Off by default and free when off — a trace on every tool call is a cost on the hot path.
4192
+ */
4193
+ TRACE: "RETICLE_TRACE",
4194
+ /**
4195
+ * How many consecutive reconnects the MCP proxy attempts before it stops retrying and goes
4196
+ * dormant. Overridable for the same reason the idle windows are: the real budget takes MINUTES to
4197
+ * exhaust, so the one spec that proves the proxy SURVIVES exhaustion could not run at all without
4198
+ * shortening it. A budget nobody can reach in a test is a budget nobody tests.
4199
+ */
4200
+ RECONNECT_ATTEMPTS: "RETICLE_RECONNECT_ATTEMPTS",
4201
+ /** Quiet window before an abandoned MCP stdio proxy exits; `0` disables the watcher. */
4202
+ MCP_PROXY_IDLE: "RETICLE_MCP_PROXY_IDLE_MS",
4203
+ /**
4204
+ * API key for the harness — the model that drives the app when no coding agent is in the loop.
4205
+ *
4206
+ * The standard Anthropic name, on purpose: a machine that can already run a coding agent can
4207
+ * already run the harness, with nothing to configure. Absent ⇒ the harness is simply unavailable
4208
+ * and every other part of Reticle is unaffected.
4209
+ */
4210
+ HARNESS_KEY: "ANTHROPIC_API_KEY",
4211
+ /** Model the harness drives with. Defaults to a small one — see `DEFAULT_HARNESS_MODEL`. */
4212
+ HARNESS_MODEL: "RETICLE_HARNESS_MODEL",
4213
+ /** Base URL for the harness's model API, for a proxy or a gateway. */
4214
+ HARNESS_BASE_URL: "RETICLE_HARNESS_BASE_URL",
4215
+ /** Hard ceiling on harness model turns in one drive. Bounds cost, not value. */
4216
+ HARNESS_MAX_STEPS: "RETICLE_HARNESS_MAX_STEPS",
4217
+ /**
4218
+ * Which model backs the harness driver: `anthropic` (the default) or `jev`.
4219
+ *
4220
+ * Two drivers exist because the driver's job turns out not to need a text generator. It picks the
4221
+ * next action and says what it expects; the verdict is decided afterwards by the engine, with no
4222
+ * model anywhere near it. A System One model answers typed questions against a state, which is
4223
+ * exactly that job and none of the rest, so it can drive without being able to generate a
4224
+ * sentence. Unset keeps the existing behaviour for everybody who has not asked for this.
4225
+ */
4226
+ HARNESS_DRIVER: "RETICLE_HARNESS_DRIVER",
4227
+ /**
4228
+ * A direct Jev key, for this repo's own benchmarks and for anyone holding a TypeSafe account.
4229
+ *
4230
+ * Users are not expected to have one. The ordinary path is the key they already minted on the
4231
+ * platform (`RETICLE_API_KEY`), which reaches Jev through the proxy at `RETICLE_CLOUD_URL` —
4232
+ * this variable is the escape hatch that skips it, and it takes precedence when both are set so
4233
+ * that a developer debugging the upstream is never silently talking to the proxy instead.
4234
+ */
4235
+ HARNESS_JEV_KEY: "JEV_API_KEY",
4236
+ /** Base URL for the Jev API, for a proxy or a gateway. Defaults to TypeSafe's own endpoint. */
4237
+ HARNESS_JEV_URL: "RETICLE_HARNESS_JEV_URL",
4238
+ /** A direct OpenAI key. As with Jev, the ordinary path is the platform key and its proxy. */
4239
+ HARNESS_OPENAI_KEY: "OPENAI_API_KEY",
4240
+ /** Model the OpenAI driver uses. Defaults to a small one — see `DEFAULT_OPENAI_MODEL`. */
4241
+ HARNESS_OPENAI_MODEL: "RETICLE_HARNESS_OPENAI_MODEL",
4242
+ /**
4243
+ * The API key minted on the platform, and the host it belongs to.
4244
+ *
4245
+ * Read through `apiKeyFrom()`, never directly — see the note on that function for why there are
4246
+ * two names for one key and why neither may be dropped.
4247
+ */
4248
+ API_KEY: "RETICLE_API_KEY",
4249
+ /** The name this key had until 2026-09. Still honoured; see `apiKeyFrom()`. */
4250
+ CLOUD_KEY: "RETICLE_CLOUD_KEY",
4251
+ CLOUD_URL: "RETICLE_CLOUD_URL"
4304
4252
  };
4305
- ZodReadonly.create = (type, params) => {
4306
- return new ZodReadonly({
4307
- innerType: type,
4308
- typeName: ZodFirstPartyTypeKind.ZodReadonly,
4309
- ...processCreateParams(params)
4310
- });
4253
+ var TRANSPORT_LIMITS = {
4254
+ MAX_MESSAGE_BYTES: 1024 * 1024,
4255
+ /**
4256
+ * Inbound events per second before the bridge SAMPLES rather than records everything.
4257
+ *
4258
+ * The cap exists to stop a PATHOLOGICAL page (an animation loop firing DOM mutations every frame),
4259
+ * not to throttle a busy one: an ordinary React app with an active query cache passes 1000/s, and a
4260
+ * sampled window cannot support a verdict, so too low a cap answers `unknown` with
4261
+ * `unclean_capture` on the first `act_and_wait` after an install (#316).
4262
+ *
4263
+ * The ceiling it defends is cheap — the daemon is on the same machine and a typical event is a few
4264
+ * hundred bytes, so 20000/s is single-digit MB/s over loopback. Raising it does not raise what a
4265
+ * runaway page can make the bridge HOLD: memory is bounded independently by the ring buffer, which
4266
+ * evicts on `RING_BUFFER_DEFAULTS.MAX_BYTES` (this same constant through that alias) as well as on
4267
+ * a count and an age. The rate cap defends parse cost, the ring buffer defends memory, and they are
4268
+ * not substitutes.
4269
+ */
4270
+ MAX_MESSAGES_PER_SECOND: 2e4,
4271
+ MAX_SESSIONS: 32,
4272
+ MAX_PENDING_CONNECTIONS: 16,
4273
+ HELLO_TIMEOUT_MS: 5e3,
4274
+ MAX_BUFFER_BYTES: 8 * 1024 * 1024,
4275
+ MAX_SESSION_ID_LENGTH: 128,
4276
+ MAX_URL_LENGTH: 4096,
4277
+ MAX_TITLE_LENGTH: 512,
4278
+ MAX_ADAPTERS: 32,
4279
+ /*
4280
+ * The cap on ONE vocabulary in `contractParts` — the commands, events or actions a peer says it
4281
+ * speaks.
4282
+ *
4283
+ * Its own number, and the reason is an outage this nearly shipped. `contractParts` reused
4284
+ * `MAX_ADAPTERS` (32), and this implementation's own event vocabulary is larger than that. The
4285
+ * field had never had a producer, so nothing had ever tried: the first build to actually send it
4286
+ * had every HELLO rejected by the schema and NO SESSION COULD CONNECT AT ALL. The unit gate is
4287
+ * blind to it — the schema accepts the shape in isolation, and only a real page talking to a real
4288
+ * daemon fails — so the e2e battery is what caught it.
4289
+ *
4290
+ * 128 leaves the vocabularies room to roughly triple. A cap here is still worth having: this
4291
+ * arrives on an unauthenticated HELLO, so it is an untrusted list and the bound is chosen here.
4292
+ */
4293
+ MAX_CONTRACT_NAMES: 128,
4294
+ MAX_ADAPTER_NAME_LENGTH: 128,
4295
+ MAX_TOKEN_LENGTH: 512,
4296
+ MAX_COMMAND_ID_LENGTH: 128,
4297
+ MAX_COMMAND_NAME_LENGTH: 128,
4298
+ MAX_REF_LENGTH: 128,
4299
+ MAX_ERROR_LENGTH: 4096,
4300
+ /** Cap on a captured stack trace before it crosses the wire — the console observer and both React
4301
+ * error hooks (error-boundary, hydration-error) all truncate to this, so it is one fact. */
4302
+ MAX_STACK_LENGTH: 4e3,
4303
+ MAX_SERIALIZE_DEPTH: 8,
4304
+ MAX_COLLECTION_ITEMS: 200,
4305
+ MAX_OBJECT_KEYS: 200,
4306
+ MAX_STRING_LENGTH: 64 * 1024,
4307
+ /** Human review marks: the note the human types when flagging a mistake on the page. */
4308
+ MAX_MARK_NOTE_LENGTH: 2e3,
4309
+ /** Human review marks: the legible element label that pins the mark (e.g. "Submit button"). */
4310
+ MAX_MARK_LABEL_LENGTH: 256
4311
4311
  };
4312
- function cleanParams(params, data) {
4313
- const p = typeof params === "function" ? params(data) : typeof params === "string" ? { message: params } : params;
4314
- const p2 = typeof p === "string" ? { message: p } : p;
4315
- return p2;
4316
- }
4317
- function custom(check, _params = {}, fatal) {
4318
- if (check)
4319
- return ZodAny.create().superRefine((data, ctx) => {
4320
- const r = check(data);
4321
- if (r instanceof Promise) {
4322
- return r.then((r2) => {
4323
- if (!r2) {
4324
- const params = cleanParams(_params, data);
4325
- const _fatal = params.fatal ?? fatal ?? true;
4326
- ctx.addIssue({ code: "custom", ...params, fatal: _fatal });
4327
- }
4328
- });
4329
- }
4330
- if (!r) {
4331
- const params = cleanParams(_params, data);
4332
- const _fatal = params.fatal ?? fatal ?? true;
4333
- ctx.addIssue({ code: "custom", ...params, fatal: _fatal });
4334
- }
4335
- return;
4336
- });
4337
- return ZodAny.create();
4338
- }
4339
- var late = {
4340
- object: ZodObject.lazycreate
4312
+ var ReticleDir = {
4313
+ ROOT: ".reticle",
4314
+ CONTRACT_FILE: "contract.json",
4315
+ FLOWS_SUBDIR: "flows",
4316
+ BASELINES_SUBDIR: "baselines",
4317
+ /** cross-run memory — outcomes of past runs (the "did it behave like last time?" file). */
4318
+ PROJECT_FILE: "project.json",
4319
+ /**
4320
+ * the user's own record of what Reticle has done for them — .reticle/impact.json.
4321
+ *
4322
+ * Local only, never uploaded, and deliberately NOT part of telemetry: telemetry answers questions
4323
+ * about the product; this answers the user's question about their own work.
4324
+ */
4325
+ IMPACT_FILE: "impact.json",
4326
+ /** what changes were SUPPOSED to make true —.reticle/intent.json (git-checked, reviewed) */
4327
+ INTENT_FILE: "intent.json",
4328
+ /**
4329
+ * The sharded form of the same ledger —.reticle/intent/ (git-checked, for the same reason).
4330
+ *
4331
+ * Named here rather than inline at the store that writes it, because the partition guard over
4332
+ * this object is what decides whether a name Reticle creates is committed or ignored. A directory
4333
+ * spelled as a free string somewhere else is one the guard cannot see, so nobody ever makes that
4334
+ * decision: `intent/` was written into users' repositories for that reason alone, neither
4335
+ * classified nor ignored.
4336
+ */
4337
+ INTENT_SUBDIR: "intent",
4338
+ /**
4339
+ * what a drive types into a field, keyed by the field's label —.reticle/fill-values.json.
4340
+ *
4341
+ * Git-checked on purpose. A generated value is paid for once and then belongs to the project: the
4342
+ * next drive reuses it for free, a replay sends exactly what the recording sent, and a human who
4343
+ * dislikes one can edit the file rather than argue with a model.
4344
+ */
4345
+ FILL_VALUES_FILE: "fill-values.json",
4346
+ /** opt-in pixel baselines —.reticle/visual/<name>.png + <name>.diff.png. */
4347
+ VISUAL_SUBDIR: "visual",
4348
+ /** verification-run artifacts —.reticle/runs/<runId>.json (the OEM/CI-consumable verdict). */
4349
+ RUNS_SUBDIR: "runs",
4350
+ /** fail-to-pass bug capsules —.reticle/capsules/<id>.json (a minimal failing flow + its evidence). */
4351
+ CAPSULES_SUBDIR: "capsules",
4352
+ /** durable causal journal —.reticle/sessions/<id>/{events,actions}.jsonl (the substrate). */
4353
+ SESSIONS_SUBDIR: "sessions",
4354
+ /** append-only event ledger inside a session dir (one ReticleEvent per line). */
4355
+ JOURNAL_EVENTS_FILE: "events.jsonl",
4356
+ /** append-only action ledger inside a session dir (one JournalAction per line). */
4357
+ JOURNAL_ACTIONS_FILE: "actions.jsonl",
4358
+ /**
4359
+ * the event ledger's closure report — .reticle/sessions/<id>/events.closed.json.
4360
+ *
4361
+ * Written once, when the byte ceiling refuses a batch, and never removed. Its PRESENCE is what
4362
+ * makes the ceiling durable: a session id survives a reload, so the next connection reopens the
4363
+ * same ledger with an empty memory, and a bound that lives only in one object's field is a bound
4364
+ * per connection. Its CONTENTS are the loss report a time-windowed query cannot filter away.
4365
+ */
4366
+ JOURNAL_EVENTS_CLOSED_FILE: "events.closed.json",
4367
+ /** learned expected-envelopes per route, accumulated across runs (the deviation-report baseline). */
4368
+ ENVELOPES_FILE: "envelopes.json",
4369
+ /** learned ambient (action-less churn) region map — excluded from settle/summaries/envelopes. */
4370
+ AMBIENT_FILE: "ambient.json",
4371
+ /** per-flow flake ledger — replay outcomes that decide intermittent-failure quarantine. */
4372
+ FLAKE_FILE: "flake.json",
4373
+ /**
4374
+ * the project's cloud binding — .reticle/cloud.json, written by `reticle link`. Git-checked and
4375
+ * non-secret: the project id, the API origin, and where its dashboard lives. The KEY lives in
4376
+ * ~/.reticle/credentials.json instead, because that one must never reach a repository.
4377
+ */
4378
+ CLOUD_LINK_FILE: "cloud.json",
4379
+ /**
4380
+ * local sync bookkeeping — .reticle/cloud-state.json. The pull cursor, when each half last ran,
4381
+ * and the last error. NOT git-checked: it describes this machine's conversation with the server,
4382
+ * and committing one machine's cursor would make every other machine skip what it had not seen.
4383
+ */
4384
+ CLOUD_STATE_FILE: "cloud-state.json",
4385
+ /**
4386
+ * triage decisions pulled BACK from the dashboard — .reticle/issues.json. What a human said about
4387
+ * a defect ("resolved", "not a bug"), so the HUD stops showing it and the next run does not
4388
+ * re-report it as though nobody had looked.
4389
+ */
4390
+ ISSUES_FILE: "issues.json",
4391
+ /** Per-flow assertion tiers recorded on each PASSING replay — the gate's anti-downgrade baseline. */
4392
+ TIERS_FILE: "assertion-tiers.json",
4393
+ /**
4394
+ * what to run when something happens — .reticle/hooks.json. Git-checked on purpose: a hook is a
4395
+ * decision the whole team shares, the same way a git hook or a package script is, and one that
4396
+ * only exists on the machine that wrote it is a rule nobody else is following.
4397
+ *
4398
+ * It names COMMANDS, so it is exactly as trusted as `package.json` scripts in the same repository
4399
+ * and no more: opening a repo does not run them, and Reticle runs one only when the event it is
4400
+ * attached to actually happens. The event payload is handed over on stdin, never interpolated into
4401
+ * the command line, so a defect's own text can never become part of a command.
4402
+ */
4403
+ HOOKS_FILE: "hooks.json",
4404
+ /**
4405
+ * Auto-provisioned bridge pairing token, stored at ~/.reticle/pairing-token (mode 0600). Written by
4406
+ * the daemon, read Node-side by the build plugins to inject into connect. A browser sandbox cannot
4407
+ * read it, so a rogue localhost app can't present it — that's what stops cross-app session hijack.
4408
+ */
4409
+ PAIRING_TOKEN_FILE: "pairing-token"
4341
4410
  };
4342
- var ZodFirstPartyTypeKind;
4343
- (function(ZodFirstPartyTypeKind2) {
4344
- ZodFirstPartyTypeKind2["ZodString"] = "ZodString";
4345
- ZodFirstPartyTypeKind2["ZodNumber"] = "ZodNumber";
4346
- ZodFirstPartyTypeKind2["ZodNaN"] = "ZodNaN";
4347
- ZodFirstPartyTypeKind2["ZodBigInt"] = "ZodBigInt";
4348
- ZodFirstPartyTypeKind2["ZodBoolean"] = "ZodBoolean";
4349
- ZodFirstPartyTypeKind2["ZodDate"] = "ZodDate";
4350
- ZodFirstPartyTypeKind2["ZodSymbol"] = "ZodSymbol";
4351
- ZodFirstPartyTypeKind2["ZodUndefined"] = "ZodUndefined";
4352
- ZodFirstPartyTypeKind2["ZodNull"] = "ZodNull";
4353
- ZodFirstPartyTypeKind2["ZodAny"] = "ZodAny";
4354
- ZodFirstPartyTypeKind2["ZodUnknown"] = "ZodUnknown";
4355
- ZodFirstPartyTypeKind2["ZodNever"] = "ZodNever";
4356
- ZodFirstPartyTypeKind2["ZodVoid"] = "ZodVoid";
4357
- ZodFirstPartyTypeKind2["ZodArray"] = "ZodArray";
4358
- ZodFirstPartyTypeKind2["ZodObject"] = "ZodObject";
4359
- ZodFirstPartyTypeKind2["ZodUnion"] = "ZodUnion";
4360
- ZodFirstPartyTypeKind2["ZodDiscriminatedUnion"] = "ZodDiscriminatedUnion";
4361
- ZodFirstPartyTypeKind2["ZodIntersection"] = "ZodIntersection";
4362
- ZodFirstPartyTypeKind2["ZodTuple"] = "ZodTuple";
4363
- ZodFirstPartyTypeKind2["ZodRecord"] = "ZodRecord";
4364
- ZodFirstPartyTypeKind2["ZodMap"] = "ZodMap";
4365
- ZodFirstPartyTypeKind2["ZodSet"] = "ZodSet";
4366
- ZodFirstPartyTypeKind2["ZodFunction"] = "ZodFunction";
4367
- ZodFirstPartyTypeKind2["ZodLazy"] = "ZodLazy";
4368
- ZodFirstPartyTypeKind2["ZodLiteral"] = "ZodLiteral";
4369
- ZodFirstPartyTypeKind2["ZodEnum"] = "ZodEnum";
4370
- ZodFirstPartyTypeKind2["ZodEffects"] = "ZodEffects";
4371
- ZodFirstPartyTypeKind2["ZodNativeEnum"] = "ZodNativeEnum";
4372
- ZodFirstPartyTypeKind2["ZodOptional"] = "ZodOptional";
4373
- ZodFirstPartyTypeKind2["ZodNullable"] = "ZodNullable";
4374
- ZodFirstPartyTypeKind2["ZodDefault"] = "ZodDefault";
4375
- ZodFirstPartyTypeKind2["ZodCatch"] = "ZodCatch";
4376
- ZodFirstPartyTypeKind2["ZodPromise"] = "ZodPromise";
4377
- ZodFirstPartyTypeKind2["ZodBranded"] = "ZodBranded";
4378
- ZodFirstPartyTypeKind2["ZodPipeline"] = "ZodPipeline";
4379
- ZodFirstPartyTypeKind2["ZodReadonly"] = "ZodReadonly";
4380
- })(ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {}));
4381
- var instanceOfType = (cls, params = {
4382
- message: `Input not instance of ${cls.name}`
4383
- }) => custom((data) => data instanceof cls, params);
4384
- var stringType = ZodString.create;
4385
- var numberType = ZodNumber.create;
4386
- var nanType = ZodNaN.create;
4387
- var bigIntType = ZodBigInt.create;
4388
- var booleanType = ZodBoolean.create;
4389
- var dateType = ZodDate.create;
4390
- var symbolType = ZodSymbol.create;
4391
- var undefinedType = ZodUndefined.create;
4392
- var nullType = ZodNull.create;
4393
- var anyType = ZodAny.create;
4394
- var unknownType = ZodUnknown.create;
4395
- var neverType = ZodNever.create;
4396
- var voidType = ZodVoid.create;
4397
- var arrayType = ZodArray.create;
4398
- var objectType = ZodObject.create;
4399
- var strictObjectType = ZodObject.strictCreate;
4400
- var unionType = ZodUnion.create;
4401
- var discriminatedUnionType = ZodDiscriminatedUnion.create;
4402
- var intersectionType = ZodIntersection.create;
4403
- var tupleType = ZodTuple.create;
4404
- var recordType = ZodRecord.create;
4405
- var mapType = ZodMap.create;
4406
- var setType = ZodSet.create;
4407
- var functionType = ZodFunction.create;
4408
- var lazyType = ZodLazy.create;
4409
- var literalType = ZodLiteral.create;
4410
- var enumType = ZodEnum.create;
4411
- var nativeEnumType = ZodNativeEnum.create;
4412
- var promiseType = ZodPromise.create;
4413
- var effectsType = ZodEffects.create;
4414
- var optionalType = ZodOptional.create;
4415
- var nullableType = ZodNullable.create;
4416
- var preprocessType = ZodEffects.createWithPreprocess;
4417
- var pipelineType = ZodPipeline.create;
4418
- var ostring = () => stringType().optional();
4419
- var onumber = () => numberType().optional();
4420
- var oboolean = () => booleanType().optional();
4421
- var coerce = {
4422
- string: ((arg) => ZodString.create({ ...arg, coerce: true })),
4423
- number: ((arg) => ZodNumber.create({ ...arg, coerce: true })),
4424
- boolean: ((arg) => ZodBoolean.create({
4425
- ...arg,
4426
- coerce: true
4427
- })),
4428
- bigint: ((arg) => ZodBigInt.create({ ...arg, coerce: true })),
4429
- date: ((arg) => ZodDate.create({ ...arg, coerce: true }))
4411
+ var UpdateCheckIntervalMs = 24 * 60 * 60 * 1e3;
4412
+ var RING_BUFFER_DEFAULTS = {
4413
+ MAX_EVENTS: 2e3,
4414
+ MAX_AGE_MS: 6e4,
4415
+ MAX_BYTES: TRANSPORT_LIMITS.MAX_BUFFER_BYTES
4430
4416
  };
4431
- var NEVER = INVALID;
4417
+ var EventType = {
4418
+ DOM_ADDED: "dom.added",
4419
+ DOM_REMOVED: "dom.removed",
4420
+ DOM_ATTR: "dom.attr",
4421
+ DOM_TEXT: "dom.text",
4422
+ NET_REQUEST: "net.request",
4423
+ NET_PENDING: "net.pending",
4424
+ /** An SSE (EventSource) or WebSocket frame — a message on a long-lived streaming connection. */
4425
+ NET_STREAM: "net.stream",
4426
+ /** A web-perf metric a screenshot can't verify: LCP, cumulative layout shift, or a long task. */
4427
+ PERF: "perf",
4428
+ ROUTE_CHANGE: "route.change",
4429
+ CONSOLE_LOG: "console.log",
4430
+ CONSOLE_WARN: "console.warn",
4431
+ CONSOLE_ERROR: "console.error",
4432
+ CONSOLE_INFO: "console.info",
4433
+ CONSOLE_DEBUG: "console.debug",
4434
+ ERROR_UNCAUGHT: "error.uncaught",
4435
+ VISIBLE_SHOWN: "visible.shown",
4436
+ ANIM_START: "anim.start",
4437
+ ANIM_END: "anim.end",
4438
+ SCROLL_POSITION: "scroll.position",
4439
+ REVEAL_SHOWN: "reveal.shown",
4440
+ SIGNAL: "signal",
4441
+ STATE_CHANGE: "state.change",
4442
+ /** a write to localStorage/sessionStorage/cookies — `data: { area, key, old?, new? }` (values redacted). */
4443
+ STORAGE_CHANGE: "storage.change",
4444
+ /** page-level visibility/focus health (distinct from element-level VISIBLE_*). */
4445
+ PAGE_HEALTH: "page.health",
4446
+ /**
4447
+ * synthetic: the page called window.open, so the consequence of what was just clicked may live in
4448
+ * another browsing context this one cannot observe (an OAuth popup is the archetype).
4449
+ * `data: { href }` — the URL the page asked to open, when it named one.
4450
+ */
4451
+ CONTEXT_OPENED: "context.opened",
4452
+ /**
4453
+ * The app opened a native `alert`/`confirm`/`prompt` while Reticle was driving it.
4454
+ *
4455
+ * Recorded because Reticle ANSWERS these rather than letting them block — a native dialog halts
4456
+ * the main thread, and the SDK's own message pump is on that thread, so one `confirm` behind a
4457
+ * driven click made the tab permanently unresponsive with no recovery from inside the session.
4458
+ * Answering silently would trade a wedge for an invisible one, so the question the app asked, and
4459
+ * the answer given, ride out as an event.
4460
+ */
4461
+ DIALOG_OPENED: "dialog.opened",
4462
+ /** aggregated React commits over a throttle window (dev builds) — `data: { commits }`. Commit storms /
4463
+ * wasted re-renders show up here without a per-render flood. */
4464
+ RENDER_COMMIT: "render.commit",
4465
+ /** element focus moved — `data: { to, from, toBody }`. Focus dropping to body after an act is a regression. */
4466
+ FOCUS_CHANGE: "focus.change",
4467
+ /** browser → bridge: a human recording compiled in-page. */
4468
+ FLOW_RECORDED: "flow.recorded",
4469
+ /** synthetic: browser transport queue overflowed; events were dropped. `data: { dropped: number }`. */
4470
+ TRANSPORT_OVERFLOW: "transport.overflow",
4471
+ /**
4472
+ * synthetic: a per-channel cap truncated a batch (e.g. a DOM mutation flood). `data: { channel, dropped }`.
4473
+ * Marks downstream rollups/envelopes as built on incomplete data — a ledger that lies at scale is worse
4474
+ * than no ledger, so truncation is never silent.
4475
+ */
4476
+ TRUNCATED: "truncated",
4477
+ /**
4478
+ * synthetic: the SDK detected a region it CANNOT observe (a cross-origin iframe, a closed shadow root).
4479
+ * `data: { kind: BlindSpotKind, count }`. Surfaced on results as `coverage: partial` so a green never
4480
+ * implies it saw everything.
4481
+ */
4482
+ BLIND_SPOT: "blind-spot",
4483
+ /** synthetic: the SDK ITSELF failed (an observer threw). `data: { site, message, errorType }`.
4484
+ * Rides the existing bridge — no outbound request. See browser/observers/sdk-failure.ts. */
4485
+ SDK_FAILED: "sdk.failed",
4486
+ /**
4487
+ * synthetic (driven only): CDP/Playwright-authoritative network detail for a response the in-page
4488
+ * fetch/XHR wrapper also saw — full response headers + authoritative status/mimeType the page-side
4489
+ * wrapper can't reach. `data: { url, method?, status, headers, resourceType? }`. Merged onto the
4490
+ * matching in-page NET_REQUEST so the driven view never loses fidelity to an outside-in tool.
4491
+ */
4492
+ NET_DETAIL: "net.detail",
4493
+ /**
4494
+ * Live-control: browser → bridge. A human acted on the presenter panel.
4495
+ * `data: { kind: HumanControlKind; text?: string }`. Rides the existing EventMessage.
4496
+ */
4497
+ HUMAN_CONTROL: "human.control",
4498
+ /**
4499
+ * Human review: browser → bridge. A human pinned a mistake to an element on the running page
4500
+ * (the "annotate the bug where you see it" loop). `data` narrows to HumanMarkDataSchema — a note
4501
+ * plus a re-resolvable element anchor (and its source file:line when the framework stamped one) so
4502
+ * the agent that drains the mark knows exactly which element and which source to fix.
4503
+ */
4504
+ HUMAN_MARK: "human.mark",
4505
+ /**
4506
+ * The app produced a FILE — a Blob handed to `URL.createObjectURL`, usually saved by clicking an
4507
+ * anchor with `download`. `data: { filename?, mimeType, bytes, lines?, preview? }`. The one artifact
4508
+ * class no outside-the-browser tool can inspect: it never crosses the network, so there is no
4509
+ * request to intercept. See `observers/download.ts` for the defect that motivated it.
4510
+ */
4511
+ DOWNLOAD: "download"
4512
+ };
4513
+ var RETICLE_RENDER_PREHOOK = "__reticleRenderPreHook";
4514
+ var CONSOLE_LEVEL_PREFIX = "console.";
4515
+ var CONSOLE_LEVELS = [
4516
+ EventType.CONSOLE_LOG,
4517
+ EventType.CONSOLE_WARN,
4518
+ EventType.CONSOLE_ERROR,
4519
+ EventType.CONSOLE_INFO
4520
+ ].map((type) => type.slice(CONSOLE_LEVEL_PREFIX.length));
4521
+
4522
+ // ../../../core/dist/identity/source-constants.js
4523
+ var DATA_RETICLE_SOURCE_ATTR = "data-reticle-source";
4524
+ var RETICLE_ROOT_GLOBAL = "__RETICLE_ROOT__";
4525
+ var RETICLE_SDK_VERSION_GLOBAL = "__RETICLE_SDK_VERSION__";
4432
4526
 
4433
- // ../core/dist/project-id.js
4527
+ // ../../../core/dist/identity/project-id.js
4434
4528
  var PROJECT_ID_HASH_LENGTH = 8;
4435
4529
  var FALLBACK_PROJECT_BASE = "app";
4436
4530
  function slugifyPackageName(name) {
@@ -4460,7 +4554,7 @@ function projectIdFrom(pkgName, rootPath, hash) {
4460
4554
  return `${base}-${hash(rootPath)}`;
4461
4555
  }
4462
4556
 
4463
- // ../core/dist/daemon-registry.js
4557
+ // ../../../core/dist/registry/daemon-registry.js
4464
4558
  var DAEMON_REGISTRY_PREFIX = "daemon-";
4465
4559
  var DAEMON_REGISTRY_SUFFIX = ".json";
4466
4560
  function daemonRegistryPort(fileName) {
@@ -4484,7 +4578,7 @@ function pickDaemonPort(entries, projectId, isAlive2) {
4484
4578
  return matches[0] ?? null;
4485
4579
  }
4486
4580
 
4487
- // ../core/dist/dev-server-registry.js
4581
+ // ../../../core/dist/registry/dev-server-registry.js
4488
4582
  var DEV_SERVER_PREFIX = "devserver-";
4489
4583
  var DEV_SERVER_SUFFIX = ".json";
4490
4584
  function devServerRegistryFileName(port) {
@@ -4517,25 +4611,26 @@ var DevServerEntrySchema = external_exports.object({
4517
4611
  var import_node_crypto2 = require("node:crypto");
4518
4612
  var import_node_path2 = require("node:path");
4519
4613
  var import_node_fs2 = require("node:fs");
4614
+ var RETICLE_CONFIG_BASENAME = ".reticle.json";
4520
4615
  function shortHash(input) {
4521
4616
  return (0, import_node_crypto2.createHash)("sha1").update(input).digest("hex").slice(0, PROJECT_ID_HASH_LENGTH);
4522
4617
  }
4523
4618
  function deriveProjectId(pkgName, rootPath) {
4524
4619
  return projectIdFrom(pkgName, rootPath, shortHash);
4525
4620
  }
4526
- function readNearestPackageName(startDir) {
4621
+ var readFileOrThrow = (path) => (0, import_node_fs2.readFileSync)(path, "utf8");
4622
+ var MAX_CONFIG_SEARCH_DEPTH = 6;
4623
+ var MAX_PACKAGE_SEARCH_DEPTH = 50;
4624
+ function readNearestField(startDir, basename, field, readFile, maxDepth) {
4527
4625
  let dir = startDir;
4528
- for (let depth = 0; depth < 50; depth++) {
4529
- const pkgPath = (0, import_node_path2.join)(dir, "package.json");
4530
- if ((0, import_node_fs2.existsSync)(pkgPath)) {
4531
- try {
4532
- const parsed = JSON.parse((0, import_node_fs2.readFileSync)(pkgPath, "utf8"));
4533
- if ("object" === typeof parsed && parsed !== null) {
4534
- const name = parsed["name"];
4535
- if ("string" === typeof name && name.length > 0) return name;
4536
- }
4537
- } catch {
4626
+ for (let depth = 0; depth <= maxDepth; depth++) {
4627
+ try {
4628
+ const parsed = JSON.parse(readFile((0, import_node_path2.join)(dir, basename)));
4629
+ if ("object" === typeof parsed && parsed !== null) {
4630
+ const value = parsed[field];
4631
+ if ("string" === typeof value && value.length > 0) return value;
4538
4632
  }
4633
+ } catch {
4539
4634
  }
4540
4635
  const parent = (0, import_node_path2.dirname)(dir);
4541
4636
  if (parent === dir) break;
@@ -4543,8 +4638,22 @@ function readNearestPackageName(startDir) {
4543
4638
  }
4544
4639
  return void 0;
4545
4640
  }
4546
- function resolveProjectId(explicit, cwd, readPkgName = readNearestPackageName) {
4641
+ function readNearestPackageName(startDir, readFile = readFileOrThrow) {
4642
+ return readNearestField(startDir, "package.json", "name", readFile, MAX_PACKAGE_SEARCH_DEPTH);
4643
+ }
4644
+ function readConfiguredProjectId(startDir, readFile = readFileOrThrow) {
4645
+ return readNearestField(
4646
+ startDir,
4647
+ RETICLE_CONFIG_BASENAME,
4648
+ "projectId",
4649
+ readFile,
4650
+ MAX_CONFIG_SEARCH_DEPTH
4651
+ );
4652
+ }
4653
+ function resolveProjectId(explicit, cwd, readPkgName = readNearestPackageName, readConfiguredId = readConfiguredProjectId) {
4547
4654
  if (explicit !== void 0 && explicit.length > 0) return explicit;
4655
+ const configured = readConfiguredId(cwd);
4656
+ if (configured !== void 0 && configured.length > 0) return configured;
4548
4657
  return deriveProjectId(readPkgName(cwd), cwd);
4549
4658
  }
4550
4659
 
@@ -4800,6 +4909,41 @@ function optimizerOptions(key, inherited, add) {
4800
4909
  };
4801
4910
  }
4802
4911
 
4912
+ // src/plugin-name.ts
4913
+ var RETICLE_VITE_PLUGIN_NAME = "reticle";
4914
+
4915
+ // src/injection-postcondition.ts
4916
+ var DEV_INJECTION_GRACE_MS = 1e4;
4917
+ var notInjectedMessage = () => `[${RETICLE_VITE_PLUGIN_NAME}] could not inject reticle.connect(): the HTML entry module was never matched, so this app carries no instrumentation and will never connect. Check that index.html references your entry with a <script type="module" src="...">, or pass \`inject: false\` and call reticle.connect({ token: __RETICLE_TOKEN__ }) yourself. The plugin still inlines that define; a connect without it is refused.`;
4918
+ var unconfirmedInjectionMessage = () => `[${RETICLE_VITE_PLUGIN_NAME}] could not confirm reticle.connect() was injected: the HTML entry module was not transformed this session. That is expected when Vite served it from its transform cache. If the app does not appear in \`reticle status\`, restart the dev server with \`--force\` to bypass the cache, then check that index.html references your entry with a <script type="module" src="...">.`;
4919
+ var htmlHookNeverRanMessage = () => `[${RETICLE_VITE_PLUGIN_NAME}] this app will never connect: the dev server never asked this plugin to transform any HTML, so reticle.connect() was never added to the page. That usually means your framework renders its own HTML instead of serving index.html \u2014 SvelteKit, Nuxt, Astro, React Router (framework mode) and TanStack Start all do. Fix: import '@reticlehq/browser' and call reticle.connect({ token: __RETICLE_TOKEN__ }) yourself from your app entry file, and pass \`inject: false\` to this plugin so the two do not both try.`;
4920
+ var defaultSchedule = (run, ms) => {
4921
+ const timer = setTimeout(run, ms);
4922
+ timer.unref?.();
4923
+ };
4924
+ function createInjectionWatch(deps) {
4925
+ let htmlRequested = false;
4926
+ const isDocumentRequest = (req) => true === req.headers?.accept?.includes("text/html");
4927
+ const checkHtmlHookRan = () => {
4928
+ if (deps.desktop || !deps.inject || deps.htmlTransformed()) return;
4929
+ if (!htmlRequested) return;
4930
+ deps.warn(htmlHookNeverRanMessage());
4931
+ };
4932
+ const noteHtmlRequest = () => {
4933
+ if (htmlRequested) return;
4934
+ htmlRequested = true;
4935
+ (deps.schedule ?? defaultSchedule)(checkHtmlHookRan, DEV_INJECTION_GRACE_MS);
4936
+ };
4937
+ const checkInjected = () => {
4938
+ if (!deps.desktop || !deps.inject || deps.injected()) return;
4939
+ deps.warn(unconfirmedInjectionMessage());
4940
+ };
4941
+ const armDesktopCheck = () => {
4942
+ (deps.schedule ?? defaultSchedule)(checkInjected, DEV_INJECTION_GRACE_MS);
4943
+ };
4944
+ return { noteHtmlRequest, isDocumentRequest, checkHtmlHookRan, checkInjected, armDesktopCheck };
4945
+ }
4946
+
4803
4947
  // src/watch-ignore.ts
4804
4948
  function mergeIgnored(existing, ours) {
4805
4949
  if (void 0 === existing || null === existing) return [ours];
@@ -4820,7 +4964,6 @@ function isVitestBrowserServer(config) {
4820
4964
  }
4821
4965
 
4822
4966
  // src/index.ts
4823
- var RETICLE_VITE_PLUGIN_NAME = "reticle";
4824
4967
  var RETICLE_PACKAGE2 = "@reticlehq/react";
4825
4968
  var RETICLE_SENSOR = "@reticlehq/browser";
4826
4969
  var RETICLE_TOKEN_GLOBAL = "__RETICLE_TOKEN__";
@@ -4848,7 +4991,6 @@ onScheduleFiberRoot:function(){},onCommitFiberRoot:fire,onPostCommitFiberRoot:fu
4848
4991
  }else{var prev=h.onCommitFiberRoot;h.onCommitFiberRoot=function(){try{fire.apply(null,arguments);}catch(e){}
4849
4992
  if(typeof prev==='function')return prev.apply(this,arguments);};}
4850
4993
  }catch(e){}})();`;
4851
- var DEV_INJECTION_GRACE_MS = 1e4;
4852
4994
  var CONNECT_CHURN_LIMIT = 5;
4853
4995
  var connectChurnWarning = () => `[${RETICLE_VITE_PLUGIN_NAME}] the injected connect module has changed ${String(CONNECT_CHURN_LIMIT)} times in one dev-server session. Something it depends on (the bridge port, the pairing token, or a reticle-dev module appearing and disappearing) is not settling, and that can make the page reload repeatedly. Reticle will keep serving the newest version. Please report this at https://github.com/ReticleHQ/reticle/issues with your vite.config and whether more than one daemon is running (\`npx @reticlehq/server status\`).`;
4854
4996
  function isHtmlEntry(id, specifier, root) {
@@ -4993,6 +5135,7 @@ function reticle(options = {}) {
4993
5135
  let vitestBrowser = false;
4994
5136
  const warn = options.onWarn ?? ((message) => globalThis.console.warn(message));
4995
5137
  let injected = false;
5138
+ let htmlTransformed = false;
4996
5139
  const resolveLazy = () => {
4997
5140
  const port = resolved.port ?? discoverDaemonPort(resolved.projectId);
4998
5141
  const withPort = port !== void 0 ? { ...resolved, port } : resolved;
@@ -5005,12 +5148,13 @@ function reticle(options = {}) {
5005
5148
  const currentConnectSource = () => connectModuleSource(resolveLazy(), root === void 0 ? null : findDevModule(root, import_node_fs6.existsSync));
5006
5149
  let lastServedConnectSource;
5007
5150
  let connectChanges = 0;
5008
- const notInjectedMessage = () => `[${RETICLE_VITE_PLUGIN_NAME}] could not inject reticle.connect(): the HTML entry module was never matched, so this app carries no instrumentation and will never connect. Check that index.html references your entry with a <script type="module" src="...">, or pass \`inject: false\` and call reticle.connect({ token: __RETICLE_TOKEN__ }) yourself. The plugin still inlines that define; a connect without it is refused.`;
5009
- const unconfirmedInjectionMessage = () => `[${RETICLE_VITE_PLUGIN_NAME}] could not confirm reticle.connect() was injected: the HTML entry module was not transformed this session. That is expected when Vite served it from its transform cache. If the app does not appear in \`reticle status\`, restart the dev server with \`--force\` to bypass the cache, then check that index.html references your entry with a <script type="module" src="...">.`;
5010
- const checkInjected = () => {
5011
- if (!desktop || !inject || injected) return;
5012
- warn(unconfirmedInjectionMessage());
5013
- };
5151
+ const watch = createInjectionWatch({
5152
+ desktop,
5153
+ inject,
5154
+ injected: () => injected,
5155
+ htmlTransformed: () => htmlTransformed,
5156
+ warn
5157
+ });
5014
5158
  return {
5015
5159
  name: RETICLE_VITE_PLUGIN_NAME,
5016
5160
  // Web: serve-only, so a production bundle can never carry the SDK — gating is the tool's job.
@@ -5021,9 +5165,9 @@ function reticle(options = {}) {
5021
5165
  /**
5022
5166
  * Declare the SDK itself and the optimizer cache fingerprint.
5023
5167
  *
5024
- * The browser SDK used to need extra CJS query-engine deps here. It no longer imports that
5025
- * second accessibility engine, so keeping those names would make Vite pre-bundle packages the
5026
- * app may not have and blame Reticle for a false `Failed to resolve dependency` warning.
5168
+ * The SDK itself only. It does not import a second accessibility engine, and naming CJS deps it
5169
+ * does not use would make Vite pre-bundle packages the app may not have, then blame Reticle for a
5170
+ * false `Failed to resolve dependency` warning.
5027
5171
  */
5028
5172
  config(config) {
5029
5173
  const appRoot = config.root ?? process.cwd();
@@ -5032,29 +5176,18 @@ function reticle(options = {}) {
5032
5176
  // Keep the daemon's journal out of the dev server's watcher.
5033
5177
  //
5034
5178
  // The daemon writes `.reticle/` into the PROJECT root — session journals, and `ambient.json`
5035
- // rewritten atomically as `ambient.json.tmp` + rename on a live session. Vite watches the
5036
- // project root and does not ignore that directory, so every journal write read as a project
5037
- // file changing and Vite answered with a full page reload.
5038
- //
5039
- // That is a loop with no exit: page loads -> SDK connects and streams events -> daemon
5040
- // journals them -> Vite reloads the page -> SDK reconnects -> more events. It ran several
5041
- // times a second for as long as the dev server was up, and the damage was total but
5042
- // misattributed: every ref went stale, every act_and_wait died mid-flight, and the log
5043
- // filled with connect/disconnect pairs that looked like a flapping SDK rather than a
5044
- // watcher chasing its own tail.
5179
+ // rewritten atomically as `ambient.json.tmp` + rename on a live session. Unignored, every
5180
+ // journal write reads as a project file changing and Vite answers with a full page reload,
5181
+ // which is a loop with no exit: page loads -> SDK connects and streams events -> daemon
5182
+ // journals them -> Vite reloads the page -> SDK reconnects.
5045
5183
  //
5046
- // A RegExp, not a glob, and that is the whole difference between this working and not.
5047
- // chokidar dropped glob support in v4 — Vite 7+ ships v4/v5, where a pattern like
5048
- // `**/.reticle/**` is silently accepted and matches nothing. MEASURED against the chokidar
5049
- // this repo resolves: with the glob, a write to `.reticle/ambient.json` still fires; with
5050
- // this RegExp it does not, while a normal file still does. Vite's own defaults are globs and
5051
- // have the same problem, which is why it is not safe to copy their shape here.
5184
+ // A RegExp, NOT a glob: chokidar dropped glob support in v4, and Vite 7+ ships v4/v5, where
5185
+ // `**/.reticle/**` is silently accepted and matches nothing. Vite's own defaults are globs
5186
+ // and have the same problem, so their shape is not safe to copy here.
5052
5187
  //
5053
5188
  // Anchored on `^` or a separator so it matches the directory and not a file that merely ends
5054
5189
  // in those characters, and both separators are accepted because chokidar reports the path in
5055
- // the platform's own form.
5056
- //
5057
- // Appends to the app's list rather than replacing it, so nothing it already excluded is lost.
5190
+ // the platform's own form. Appends to the app's list rather than replacing it.
5058
5191
  server: {
5059
5192
  watch: {
5060
5193
  ignored: mergeIgnored(config.server?.watch?.ignored, JOURNAL_IGNORE)
@@ -5062,8 +5195,8 @@ function reticle(options = {}) {
5062
5195
  },
5063
5196
  // Expose the daemon's pairing token to hand-written connects in the same Vite app. The
5064
5197
  // plugin's own injected connect gets the token directly, but a connect the USER writes —
5065
- // SvelteKit's client hook, a custom entry — had no way to reach a file only Node can read,
5066
- // so it called connect() with no credential and the bridge answered "authentication
5198
+ // SvelteKit's client hook, a custom entry — cannot reach a file only Node can read, so it
5199
+ // would call connect() with no credential and the bridge would answer "authentication
5067
5200
  // failed". Empty until the daemon has provisioned one; the page reloads once it has.
5068
5201
  define: {
5069
5202
  ...config.define ?? {},
@@ -5148,25 +5281,21 @@ ${code}`;
5148
5281
  * Serve the connect module fresh, every time.
5149
5282
  *
5150
5283
  * `load` reads the daemon's pairing token at serve time precisely because the daemon may start
5151
- * after the dev server — but Vite caches the module it produced, and answers every later request
5152
- * from that cache, INCLUDING after a full page reload. So a dev server started first served a
5153
- * tokenless connect module once and then kept serving it: the SDK got a 1008 `authentication
5154
- * failed`, stopped retrying (correctly — a wrong token does not fix itself), and `reticle status`
5155
- * showed no session while the page demonstrably contained `/@reticle-connect`. Only restarting
5156
- * the dev server cleared it, which is not a step anybody guesses.
5284
+ * after the dev server — but Vite caches the module it produced and answers every later request
5285
+ * from that cache, INCLUDING after a full page reload. A dev server started first therefore keeps
5286
+ * serving a tokenless connect module: the SDK gets a 1008 `authentication failed` and stops
5287
+ * retrying (correctly — a wrong token does not fix itself), so `reticle status` shows no session
5288
+ * while the page demonstrably contains `/@reticle-connect`, and only a dev-server restart clears
5289
+ * it.
5157
5290
  *
5158
5291
  * Dropping the cached module before it is served makes `load` re-read the token, so starting the
5159
5292
  * daemon and reloading the page is enough.
5160
5293
  *
5161
- * Only when the source would ACTUALLY differ, though. This used to invalidate on every request
5162
- * for the module, forever — and a module that is force-invalidated on every request is
5163
- * re-resolved against Vite's dep optimizer on every page load, which is the shape of a
5164
- * self-sustaining reload loop: reload → request → invalidate → re-resolve → reload. Reported
5165
- * from the field on a Vite + React Router app pinned to a non-default port: every route
5166
- * reloaded the whole page about once a second, `/@reticle-connect` was fetched in every cycle,
5167
- * and removing the plugin stopped it instantly. Comparing the source first costs one string
5168
- * compare, keeps the late-daemon fix intact (the token appearing IS a change), and makes the
5169
- * module inert once it has settled.
5294
+ * Only when the source would ACTUALLY differ, though. A module force-invalidated on every
5295
+ * request is re-resolved against Vite's dep optimizer on every page load, which is a
5296
+ * self-sustaining reload loop: reload → request → invalidate → re-resolve → reload. Comparing
5297
+ * the source first costs one string compare, keeps the late-daemon fix intact (the token
5298
+ * appearing IS a change), and makes the module inert once it has settled.
5170
5299
  */
5171
5300
  configureServer(server) {
5172
5301
  if (!inject) return;
@@ -5195,6 +5324,7 @@ ${code}`;
5195
5324
  announce();
5196
5325
  }
5197
5326
  server.middlewares.use((req, _res, next) => {
5327
+ if (!desktop && watch.isDocumentRequest(req)) watch.noteHtmlRequest();
5198
5328
  const requestPath = (req.url ?? "").split("?")[0];
5199
5329
  if (requestPath === RETICLE_CONNECT_MODULE || requestPath === connectModuleUrl(base)) {
5200
5330
  if (currentConnectSource() !== lastServedConnectSource) {
@@ -5216,12 +5346,12 @@ ${code}`;
5216
5346
  if (!desktop || !inject || injected) return;
5217
5347
  throw new Error(notInjectedMessage());
5218
5348
  },
5219
- checkInjectedForTest: checkInjected,
5349
+ checkInjectedForTest: watch.checkInjected,
5350
+ checkHtmlHookForTest: watch.checkHtmlHookRan,
5351
+ injectionWatchForTest: watch,
5220
5352
  transformIndexHtml() {
5221
- if (desktop && inject && "serve" === command) {
5222
- const timer = setTimeout(checkInjected, DEV_INJECTION_GRACE_MS);
5223
- timer.unref?.();
5224
- }
5353
+ htmlTransformed = true;
5354
+ if (desktop && inject && "serve" === command) watch.armDesktopCheck();
5225
5355
  if (!inject || desktop) return [];
5226
5356
  if (true !== options.inject && vitestBrowser) return [];
5227
5357
  return [