@reticlehq/vite-plugin 3.6.0 → 3.8.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.
@@ -14,3 +14,5 @@ export interface AnnounceIo {
14
14
  * throw inside a signal handler does not degrade, it takes the shutdown with it.
15
15
  */
16
16
  export declare function announceDevServer(entry: DevServerEntry, home?: string, io?: AnnounceIo): () => void;
17
+ /** The URL to announce: the dev proxy's when one runs this server, else the one Vite reports. */
18
+ export declare function announcedUrl(env: Readonly<Record<string, string | undefined>>, reported: string | undefined, port: number): string;
package/dist/announce.js CHANGED
@@ -58,3 +58,16 @@ export function announceDevServer(entry, home = stateHome(), io = nodeIo) {
58
58
  }
59
59
  };
60
60
  }
61
+ /**
62
+ * The environment variable a dev proxy sets to the URL the app is served on. portless runs each app,
63
+ * and each git worktree of it, at `https://<branch>.<app>.localhost`, while the dev server listens on
64
+ * a port of its own. The page's origin is the proxy URL, so that is what the daemon must match.
65
+ */
66
+ const DEV_PROXY_URL_ENV = 'PORTLESS_URL';
67
+ /** The URL to announce: the dev proxy's when one runs this server, else the one Vite reports. */
68
+ export function announcedUrl(env, reported, port) {
69
+ const proxied = env[DEV_PROXY_URL_ENV];
70
+ if (proxied !== undefined && proxied.length > 0)
71
+ return proxied;
72
+ return reported ?? `http://localhost:${String(port)}/`;
73
+ }
@@ -12,4 +12,4 @@ export declare const RETICLE_DISABLED_STUB = "\0reticle-disabled-stub";
12
12
  * production, because the cost of guessing wrong the other way is an instrumented release.
13
13
  */
14
14
  export declare const isReticleDisabledBuild: (desktop: boolean, command: string | undefined, mode: string | undefined) => boolean;
15
- export declare const RETICLE_DISABLED_STUB_CODE = "\nexport class Reticle {\n connect() {}\n signal() {}\n state() {}\n renderCommit() {}\n describe() {}\n endSession() {}\n observeHotUpdates() {}\n disconnect() {}\n\n get connected() {\n return false;\n }\n}\n\nexport const reticle = new Reticle();\n\nexport const SESSION_AUTO = 'auto';\n\nexport function setIgnoreSelectors() {}\n\nexport function registerAdapter() {}\nexport function identifyComponent() {\n return null;\n}\nexport function readComponentState() {\n return undefined;\n}\nexport function elementHasHoverHandlers() {\n return false;\n}\nexport function adapterNames() {\n return [];\n}\n\nexport function registerStore() {}\nexport function unregisterStore() {}\nexport function storeNames() {\n return [];\n}\nexport function readStores() {\n return {};\n}\nexport function readStoresWithTruncation() {\n return { stores: {} };\n}\nexport function sourceOwner() {\n return undefined;\n}\nexport function markAdapterSource() {}\n\nexport function tanstackQueryStore() {\n return undefined;\n}\nexport function jotaiStore() {\n return undefined;\n}\nexport function xstateStore() {\n return undefined;\n}\nexport function valtioStore() {\n return undefined;\n}\nexport function mobxStore() {\n return undefined;\n}\nexport function recoilStore() {\n return undefined;\n}\nexport function svelteStore() {\n return undefined;\n}\nexport function piniaStore() {\n return undefined;\n}\nexport function pushStore() {\n return undefined;\n}\n\nexport function registerCapabilities() {}\nexport function getCapabilities() {\n return {};\n}\nexport function hasCapabilities() {\n return false;\n}\nexport function setCapabilitiesListener() {}\n\nexport function createReticleEmitter() {\n return {\n signal() {},\n state() {},\n };\n}\n\nexport function commitAndSignal() {}\nexport function registerReticleDomain() {}\n\nexport function buildSnapshot() {\n return {};\n}\n\nexport function inspectChart() {\n return {};\n}\nexport function canvasChartData() {\n return {};\n}\n\nexport function matchQuery() {\n return false;\n}\nexport function runQuery() {\n return undefined;\n}\n\nexport function executeAction() {\n return undefined;\n}\nexport function executeSequence() {\n return undefined;\n}\n\nexport function describe() {\n return undefined;\n}\nexport function getRole() {\n return undefined;\n}\nexport function getAccessibleName() {\n return undefined;\n}\nexport function getStates() {\n return {};\n}\nexport function isVisible() {\n return false;\n}\n\nexport class RefRegistry {\n get size() {\n return 0;\n }\n\n markEdited() {}\n\n mintedBeforeLastEdit() {\n return false;\n }\n\n refFor() {\n return undefined;\n }\n\n resolve() {\n return null;\n }\n}\n\nexport const refs = new RefRegistry();\n\nexport class Annotator {\n constructor() {}\n\n get active() {\n return false;\n }\n\n get markCount() {\n return 0;\n }\n\n mount() {}\n unmount() {}\n}\n\nexport function installAnnotator() {}\n\nexport function resolveMarkAnchor() {\n return undefined;\n}\n";
15
+ export declare const RETICLE_DISABLED_STUB_CODE = "\nexport class Reticle {\n connect() {}\n signal() {}\n state() {}\n renderCommit() {}\n describe() {}\n endSession() {}\n observeHotUpdates() {}\n disconnect() {}\n\n get connected() {\n return false;\n }\n}\n\nexport const reticle = new Reticle();\n\nexport const SESSION_AUTO = 'auto';\n\nexport function setIgnoreSelectors() {}\n\nexport function registerAdapter() {}\nexport function identifyComponent() {\n return null;\n}\nexport function readComponentState() {\n return undefined;\n}\nexport function elementHasHoverHandlers() {\n return false;\n}\nexport function adapterNames() {\n return [];\n}\n\nexport function registerStore() {}\nexport function unregisterStore() {}\nexport function storeNames() {\n return [];\n}\nexport function readStores() {\n return {};\n}\nexport function readStoresWithTruncation() {\n return { stores: {} };\n}\nexport function sourceOwner() {\n return undefined;\n}\nexport function markAdapterSource() {}\n\nexport function tanstackQueryStore() {\n return undefined;\n}\nexport function jotaiStore() {\n return undefined;\n}\nexport function xstateStore() {\n return undefined;\n}\nexport function valtioStore() {\n return undefined;\n}\nexport function mobxStore() {\n return undefined;\n}\nexport function recoilStore() {\n return undefined;\n}\nexport function svelteStore() {\n return undefined;\n}\nexport function piniaStore() {\n return undefined;\n}\nexport function pushStore() {\n return undefined;\n}\n\nexport function registerCapabilities() {}\nexport function getCapabilities() {\n return {};\n}\nexport function hasCapabilities() {\n return false;\n}\nexport function setCapabilitiesListener() {}\n\nexport function createReticleEmitter() {\n return {\n signal() {},\n state() {},\n };\n}\n\n// It wraps the APP's write, so the write still runs: Reticle does nothing in production, the app does.\nexport function commitAndSignal(_emitter, mutate) {\n return mutate();\n}\nexport function registerReticleDomain() {}\n\nexport function buildSnapshot() {\n return {};\n}\n\nexport function inspectChart() {\n return {};\n}\nexport function canvasChartData() {\n return {};\n}\n\nexport function matchQuery() {\n return false;\n}\nexport function runQuery() {\n return undefined;\n}\n\nexport function executeAction() {\n return undefined;\n}\nexport function executeSequence() {\n return undefined;\n}\n\nexport function describe() {\n return undefined;\n}\nexport function getRole() {\n return undefined;\n}\nexport function getAccessibleName() {\n return undefined;\n}\nexport function getStates() {\n return {};\n}\nexport function isVisible() {\n return false;\n}\n\nexport class RefRegistry {\n get size() {\n return 0;\n }\n\n markEdited() {}\n\n mintedBeforeLastEdit() {\n return false;\n }\n\n refFor() {\n return undefined;\n }\n\n resolve() {\n return null;\n }\n}\n\nexport const refs = new RefRegistry();\n\nexport class Annotator {\n constructor() {}\n\n get active() {\n return false;\n }\n\n get markCount() {\n return 0;\n }\n\n mount() {}\n unmount() {}\n}\n\nexport function installAnnotator() {}\n\nexport function resolveMarkAnchor() {\n return undefined;\n}\n";
@@ -111,7 +111,10 @@ export function createReticleEmitter() {
111
111
  };
112
112
  }
113
113
 
114
- export function commitAndSignal() {}
114
+ // It wraps the APP's write, so the write still runs: Reticle does nothing in production, the app does.
115
+ export function commitAndSignal(_emitter, mutate) {
116
+ return mutate();
117
+ }
115
118
  export function registerReticleDomain() {}
116
119
 
117
120
  export function buildSnapshot() {
package/dist/index.cjs CHANGED
@@ -4122,126 +4122,7 @@ var coerce = {
4122
4122
  };
4123
4123
  var NEVER = INVALID;
4124
4124
 
4125
- // ../../../core/dist/wire/constants/event-types.js
4126
- var EventType = {
4127
- DOM_ADDED: "dom.added",
4128
- DOM_REMOVED: "dom.removed",
4129
- DOM_ATTR: "dom.attr",
4130
- DOM_TEXT: "dom.text",
4131
- NET_REQUEST: "net.request",
4132
- NET_PENDING: "net.pending",
4133
- /** An SSE (EventSource) or WebSocket frame — a message on a long-lived streaming connection. */
4134
- NET_STREAM: "net.stream",
4135
- /** A web-perf metric a screenshot can't verify: LCP, cumulative layout shift, or a long task. */
4136
- PERF: "perf",
4137
- ROUTE_CHANGE: "route.change",
4138
- CONSOLE_LOG: "console.log",
4139
- CONSOLE_WARN: "console.warn",
4140
- CONSOLE_ERROR: "console.error",
4141
- CONSOLE_INFO: "console.info",
4142
- CONSOLE_DEBUG: "console.debug",
4143
- ERROR_UNCAUGHT: "error.uncaught",
4144
- VISIBLE_SHOWN: "visible.shown",
4145
- ANIM_START: "anim.start",
4146
- ANIM_END: "anim.end",
4147
- SCROLL_POSITION: "scroll.position",
4148
- REVEAL_SHOWN: "reveal.shown",
4149
- SIGNAL: "signal",
4150
- STATE_CHANGE: "state.change",
4151
- /** a write to localStorage/sessionStorage/cookies — `data: { area, key, old?, new? }` (values redacted). */
4152
- STORAGE_CHANGE: "storage.change",
4153
- /** page-level visibility/focus health (distinct from element-level VISIBLE_*). */
4154
- PAGE_HEALTH: "page.health",
4155
- /**
4156
- * synthetic: the page called window.open, so the consequence of what was just clicked may live in
4157
- * another browsing context this one cannot observe (an OAuth popup is the archetype).
4158
- * `data: { href }` — the URL the page asked to open, when it named one.
4159
- */
4160
- CONTEXT_OPENED: "context.opened",
4161
- /**
4162
- * The app opened a native `alert`/`confirm`/`prompt` while Reticle was driving it.
4163
- *
4164
- * Recorded because Reticle ANSWERS these rather than letting them block — a native dialog halts
4165
- * the main thread, and the SDK's own message pump is on that thread, so one `confirm` behind a
4166
- * driven click made the tab permanently unresponsive with no recovery from inside the session.
4167
- * Answering silently would trade a wedge for an invisible one, so the question the app asked, and
4168
- * the answer given, ride out as an event.
4169
- */
4170
- DIALOG_OPENED: "dialog.opened",
4171
- /** aggregated React commits over a throttle window (dev builds) — `data: { commits }`. Commit storms /
4172
- * wasted re-renders show up here without a per-render flood. */
4173
- RENDER_COMMIT: "render.commit",
4174
- /** element focus moved — `data: { to, from, toBody }`. Focus dropping to body after an act is a regression. */
4175
- FOCUS_CHANGE: "focus.change",
4176
- /**
4177
- * a form field's value moved — `data: { field, kind, value?, redacted?, length }`.
4178
- *
4179
- * The DOM observer never saw this: `value` is in its attribute allowlist, but React and every
4180
- * controlled input set the PROPERTY, so `MutationObserver` does not fire. `value` is omitted and
4181
- * `redacted` set for a password, a sensitive name, or a payment autocomplete hint; `length` is
4182
- * always present, because "it was wiped" is assertable without carrying anybody's data.
4183
- */
4184
- FIELD_CHANGE: "field.change",
4185
- /** browser → bridge: a human recording compiled in-page. */
4186
- FLOW_RECORDED: "flow.recorded",
4187
- /** synthetic: browser transport queue overflowed; events were dropped. `data: { dropped: number }`. */
4188
- TRANSPORT_OVERFLOW: "transport.overflow",
4189
- /**
4190
- * synthetic: a per-channel cap truncated a batch (e.g. a DOM mutation flood). `data: { channel, dropped }`.
4191
- * Marks downstream rollups/envelopes as built on incomplete data — a ledger that lies at scale is worse
4192
- * than no ledger, so truncation is never silent.
4193
- */
4194
- TRUNCATED: "truncated",
4195
- /**
4196
- * synthetic: the SDK detected a region it CANNOT observe (a cross-origin iframe, a closed shadow root).
4197
- * `data: { kind: BlindSpotKind, count }`. Surfaced on results as `coverage: partial` so a green never
4198
- * implies it saw everything.
4199
- */
4200
- BLIND_SPOT: "blind-spot",
4201
- /** synthetic: the SDK ITSELF failed (an observer threw). `data: { site, message, errorType }`.
4202
- * Rides the existing bridge — no outbound request. See browser/observers/sdk-failure.ts. */
4203
- SDK_FAILED: "sdk.failed",
4204
- /**
4205
- * synthetic (driven only): CDP/Playwright-authoritative network detail for a response the in-page
4206
- * fetch/XHR wrapper also saw — full response headers + authoritative status/mimeType the page-side
4207
- * wrapper can't reach. `data: { url, method?, status, headers, resourceType? }`. Merged onto the
4208
- * matching in-page NET_REQUEST so the driven view never loses fidelity to an outside-in tool.
4209
- */
4210
- NET_DETAIL: "net.detail",
4211
- /**
4212
- * Live-control: browser → bridge. A human acted on the presenter panel.
4213
- * `data: { kind: HumanControlKind; text?: string }`. Rides the existing EventMessage.
4214
- */
4215
- HUMAN_CONTROL: "human.control",
4216
- /**
4217
- * Human review: browser → bridge. A human pinned a mistake to an element on the running page
4218
- * (the "annotate the bug where you see it" loop). `data` narrows to HumanMarkDataSchema — a note
4219
- * plus a re-resolvable element anchor (and its source file:line when the framework stamped one) so
4220
- * the agent that drains the mark knows exactly which element and which source to fix.
4221
- */
4222
- HUMAN_MARK: "human.mark",
4223
- /**
4224
- * The app produced a FILE — a Blob handed to `URL.createObjectURL`, usually saved by clicking an
4225
- * anchor with `download`. `data: { filename?, mimeType, bytes, lines?, preview? }`. The one artifact
4226
- * class no outside-the-browser tool can inspect: it never crosses the network, so there is no
4227
- * request to intercept. See `observers/download.ts` for the defect that motivated it.
4228
- */
4229
- DOWNLOAD: "download",
4230
- /**
4231
- * Browser -> bridge: a person used the HUD — pressed a control, or the HUD changed how it sits.
4232
- * `data` narrows to HudUseDataSchema: names only. Telemetry, not evidence: the session takes it
4233
- * off the wire before the buffer, so no verdict, settle or agent read ever sees it.
4234
- */
4235
- HUD_USED: "hud.used"
4236
- };
4237
-
4238
- // ../../../core/dist/wire/constants/constants.js
4239
- var RETICLE_DEFAULT_PORT = 4400;
4240
- var RETICLE_WS_PATH = "/reticle";
4241
- var RETICLE_CLIENT_HOST = "localhost";
4242
- function bridgeWsUrl(port = RETICLE_DEFAULT_PORT, host = RETICLE_CLIENT_HOST) {
4243
- return `ws://${host}:${String(port)}${RETICLE_WS_PATH}`;
4244
- }
4125
+ // ../../../core/dist/wire/constants/reticle-env.js
4245
4126
  var ReticleEnv = {
4246
4127
  /** "0"/"false"/"off" turns off telemetry and every optional outbound call; DO_NOT_TRACK too. */
4247
4128
  TELEMETRY: "RETICLE_TELEMETRY",
@@ -4330,6 +4211,8 @@ var ReticleEnv = {
4330
4211
  HARNESS_BASE_URL: "RETICLE_HARNESS_BASE_URL",
4331
4212
  /** Hard ceiling on harness model turns in one drive. Bounds cost, not value. */
4332
4213
  HARNESS_MAX_STEPS: "RETICLE_HARNESS_MAX_STEPS",
4214
+ /** The free drive `reticle try` was granted, sent on every Harness call it bills. */
4215
+ DRIVE_ID: "RETICLE_DRIVE_ID",
4333
4216
  /**
4334
4217
  * Which model backs the harness driver: `anthropic` (the default) or `jev`.
4335
4218
  *
@@ -4367,8 +4250,148 @@ var ReticleEnv = {
4367
4250
  /** The platform host. Read through `cloudUrlFrom()`, which also accepts `URL`. */
4368
4251
  CLOUD_URL: "RETICLE_CLOUD_URL",
4369
4252
  /** The short name for `CLOUD_URL`, which people type first. `CLOUD_URL` wins when both are set. */
4370
- URL: "RETICLE_URL"
4253
+ URL: "RETICLE_URL",
4254
+ /** "0"/"false"/"off" stops `reticle hook prompt` writing prompts, on this whole machine. */
4255
+ PROMPT_CAPTURE: "RETICLE_PROMPT_CAPTURE",
4256
+ /** "0"/"false"/"off" stops the daemon's background sync, on this whole machine. */
4257
+ BACKGROUND_SYNC: "RETICLE_BACKGROUND_SYNC"
4371
4258
  };
4259
+
4260
+ // ../../../core/dist/wire/constants/event-types.js
4261
+ var EventType = {
4262
+ DOM_ADDED: "dom.added",
4263
+ DOM_REMOVED: "dom.removed",
4264
+ DOM_ATTR: "dom.attr",
4265
+ DOM_TEXT: "dom.text",
4266
+ NET_REQUEST: "net.request",
4267
+ NET_PENDING: "net.pending",
4268
+ /** An SSE (EventSource) or WebSocket frame — a message on a long-lived streaming connection. */
4269
+ NET_STREAM: "net.stream",
4270
+ /** A web-perf metric a screenshot can't verify: LCP, cumulative layout shift, or a long task. */
4271
+ PERF: "perf",
4272
+ ROUTE_CHANGE: "route.change",
4273
+ CONSOLE_LOG: "console.log",
4274
+ CONSOLE_WARN: "console.warn",
4275
+ CONSOLE_ERROR: "console.error",
4276
+ CONSOLE_INFO: "console.info",
4277
+ CONSOLE_DEBUG: "console.debug",
4278
+ ERROR_UNCAUGHT: "error.uncaught",
4279
+ VISIBLE_SHOWN: "visible.shown",
4280
+ ANIM_START: "anim.start",
4281
+ ANIM_END: "anim.end",
4282
+ SCROLL_POSITION: "scroll.position",
4283
+ REVEAL_SHOWN: "reveal.shown",
4284
+ SIGNAL: "signal",
4285
+ STATE_CHANGE: "state.change",
4286
+ /** a write to localStorage/sessionStorage/cookies — `data: { area, key, old?, new? }` (values redacted). */
4287
+ STORAGE_CHANGE: "storage.change",
4288
+ /** page-level visibility/focus health (distinct from element-level VISIBLE_*). */
4289
+ PAGE_HEALTH: "page.health",
4290
+ /**
4291
+ * synthetic: the page called window.open, so the consequence of what was just clicked may live in
4292
+ * another browsing context this one cannot observe (an OAuth popup is the archetype).
4293
+ * `data: { href }` — the URL the page asked to open, when it named one.
4294
+ */
4295
+ CONTEXT_OPENED: "context.opened",
4296
+ /**
4297
+ * The app opened a native `alert`/`confirm`/`prompt` while Reticle was driving it.
4298
+ *
4299
+ * Recorded because Reticle ANSWERS these rather than letting them block — a native dialog halts
4300
+ * the main thread, and the SDK's own message pump is on that thread, so one `confirm` behind a
4301
+ * driven click made the tab permanently unresponsive with no recovery from inside the session.
4302
+ * Answering silently would trade a wedge for an invisible one, so the question the app asked, and
4303
+ * the answer given, ride out as an event.
4304
+ */
4305
+ DIALOG_OPENED: "dialog.opened",
4306
+ /** aggregated React commits over a throttle window (dev builds) — `data: { commits }`. Commit storms /
4307
+ * wasted re-renders show up here without a per-render flood. */
4308
+ RENDER_COMMIT: "render.commit",
4309
+ /** element focus moved — `data: { to, from, toBody }`. Focus dropping to body after an act is a regression. */
4310
+ FOCUS_CHANGE: "focus.change",
4311
+ /**
4312
+ * a form field's value moved — `data: { field, kind, value?, checked?, previous?, previousChecked?, redacted?, length }`.
4313
+ *
4314
+ * The DOM observer never saw this: `value` is in its attribute allowlist, but React and every
4315
+ * controlled input set the PROPERTY, so `MutationObserver` does not fire. `value` is omitted and
4316
+ * `redacted` set for a password, a sensitive name, or a payment autocomplete hint; `length` is
4317
+ * always present, because "it was wiped" is assertable without carrying anybody's data.
4318
+ * `checked` is the checkbox/radio property (`value` there is the HTML value, usually `"on"`).
4319
+ * `kind: "settled"` is a read after the action's handlers; `previous` / `previousChecked` are
4320
+ * the pre-action reading, so a restore is distinguishable from a render.
4321
+ */
4322
+ FIELD_CHANGE: "field.change",
4323
+ /** browser → bridge: a human recording compiled in-page. */
4324
+ FLOW_RECORDED: "flow.recorded",
4325
+ /** synthetic: browser transport queue overflowed; events were dropped. `data: { dropped: number }`. */
4326
+ TRANSPORT_OVERFLOW: "transport.overflow",
4327
+ /**
4328
+ * synthetic: a per-channel cap truncated a batch (e.g. a DOM mutation flood). `data: { channel, dropped }`.
4329
+ * Marks downstream rollups/envelopes as built on incomplete data — a ledger that lies at scale is worse
4330
+ * than no ledger, so truncation is never silent.
4331
+ */
4332
+ TRUNCATED: "truncated",
4333
+ /**
4334
+ * synthetic: the SDK detected a region it CANNOT observe (a cross-origin iframe, a closed shadow root).
4335
+ * `data: { kind: BlindSpotKind, count }`. Surfaced on results as `coverage: partial` so a green never
4336
+ * implies it saw everything.
4337
+ */
4338
+ BLIND_SPOT: "blind-spot",
4339
+ /** synthetic: the SDK ITSELF failed (an observer threw). `data: { site, message, errorType }`.
4340
+ * Rides the existing bridge — no outbound request. See browser/observers/sdk-failure.ts. */
4341
+ SDK_FAILED: "sdk.failed",
4342
+ /**
4343
+ * synthetic (driven only): CDP/Playwright-authoritative network detail for a response the in-page
4344
+ * fetch/XHR wrapper also saw — full response headers + authoritative status/mimeType the page-side
4345
+ * wrapper can't reach. `data: { url, method?, status, headers, resourceType? }`. Merged onto the
4346
+ * matching in-page NET_REQUEST so the driven view never loses fidelity to an outside-in tool.
4347
+ */
4348
+ NET_DETAIL: "net.detail",
4349
+ /**
4350
+ * Live-control: browser → bridge. A human acted on the presenter panel.
4351
+ * `data: { kind: HumanControlKind; text?: string }`. Rides the existing EventMessage.
4352
+ */
4353
+ HUMAN_CONTROL: "human.control",
4354
+ /**
4355
+ * Human review: browser → bridge. A human pinned a mistake to an element on the running page
4356
+ * (the "annotate the bug where you see it" loop). `data` narrows to HumanMarkDataSchema — a note
4357
+ * plus a re-resolvable element anchor (and its source file:line when the framework stamped one) so
4358
+ * the agent that drains the mark knows exactly which element and which source to fix.
4359
+ */
4360
+ HUMAN_MARK: "human.mark",
4361
+ /**
4362
+ * The app produced a FILE — a Blob handed to `URL.createObjectURL`, usually saved by clicking an
4363
+ * anchor with `download`. `data: { filename?, mimeType, bytes, lines?, preview? }`. The one artifact
4364
+ * class no outside-the-browser tool can inspect: it never crosses the network, so there is no
4365
+ * request to intercept. See `observers/download.ts` for the defect that motivated it.
4366
+ */
4367
+ DOWNLOAD: "download",
4368
+ /**
4369
+ * Browser -> bridge: a person used the HUD — pressed a control, or the HUD changed how it sits.
4370
+ * `data` narrows to HudUseDataSchema: names only. Telemetry, not evidence: the session takes it
4371
+ * off the wire before the buffer, so no verdict, settle or agent read ever sees it.
4372
+ */
4373
+ HUD_USED: "hud.used",
4374
+ /**
4375
+ * A `<video>` or `<audio>` changed playback state: `data: { event, tag, currentTime, duration?,
4376
+ * readyState, paused }`. The event's own `t` orders it against the network, which is what a media
4377
+ * defect ("Ready before the response arrived") is: an ordering in time no DOM count shows.
4378
+ */
4379
+ MEDIA_STATE: "media.state",
4380
+ /**
4381
+ * A `<canvas>` drew something different: `data: { digest, width, height, mean }`. A hash of a
4382
+ * 16x16 sample of its pixels and their mean colour, emitted only when the digest changes. What a
4383
+ * canvas shows never reaches the DOM, so without this a canvas app's output is invisible.
4384
+ */
4385
+ CANVAS_DIGEST: "canvas.digest"
4386
+ };
4387
+
4388
+ // ../../../core/dist/wire/constants/constants.js
4389
+ var RETICLE_DEFAULT_PORT = 4400;
4390
+ var RETICLE_WS_PATH = "/reticle";
4391
+ var RETICLE_CLIENT_HOST = "localhost";
4392
+ function bridgeWsUrl(port = RETICLE_DEFAULT_PORT, host = RETICLE_CLIENT_HOST) {
4393
+ return `ws://${host}:${String(port)}${RETICLE_WS_PATH}`;
4394
+ }
4372
4395
  var SYNC_BATCH_LIMITS = {
4373
4396
  MAX_RUNS: 50,
4374
4397
  MAX_BYTES: 4 * 1024 * 1024
@@ -4463,6 +4486,8 @@ var ReticleDir = {
4463
4486
  VISUAL_SUBDIR: "visual",
4464
4487
  /** verification-run artifacts —.reticle/runs/<runId>.json (the OEM/CI-consumable verdict). */
4465
4488
  RUNS_SUBDIR: "runs",
4489
+ /** Dev-server logs, only when the state home cannot be written. Machine-local, like `runs/`. */
4490
+ LOGS_SUBDIR: "logs",
4466
4491
  /** fail-to-pass bug capsules —.reticle/capsules/<id>.json (a minimal failing flow + its evidence). */
4467
4492
  CAPSULES_SUBDIR: "capsules",
4468
4493
  /**
@@ -4477,6 +4502,11 @@ var ReticleDir = {
4477
4502
  PLANS_SUBDIR: "plans",
4478
4503
  /** durable causal journal —.reticle/sessions/<id>/{events,actions}.jsonl (the substrate). */
4479
4504
  SESSIONS_SUBDIR: "sessions",
4505
+ /**
4506
+ * Every tool call an agent made, in full, with what it did to the page —
4507
+ * .reticle/episodes/<connection>.jsonl. Local evidence: never committed, never sent.
4508
+ */
4509
+ EPISODES_SUBDIR: "episodes",
4480
4510
  /** append-only event ledger inside a session dir (one ReticleEvent per line). */
4481
4511
  JOURNAL_EVENTS_FILE: "events.jsonl",
4482
4512
  /** append-only action ledger inside a session dir (one JournalAction per line). */
@@ -4492,20 +4522,26 @@ var ReticleDir = {
4492
4522
  JOURNAL_EVENTS_CLOSED_FILE: "events.closed.json",
4493
4523
  /** learned expected-envelopes per route, accumulated across runs (the deviation-report baseline). */
4494
4524
  ENVELOPES_FILE: "envelopes.json",
4495
- /** learned ambient (action-less churn) region map — excluded from settle/summaries/envelopes. */
4496
4525
  AMBIENT_FILE: "ambient.json",
4526
+ // learned action-less churn regions, excluded from settle/summaries
4497
4527
  /** per-flow flake ledger — replay outcomes that decide intermittent-failure quarantine. */
4498
4528
  FLAKE_FILE: "flake.json",
4499
4529
  /** The app-wide coverage ledger — see server features/exhaust/ledger.ts. */
4500
4530
  COVERAGE_FILE: "coverage.json",
4501
4531
  /** Human review marks (HUD notes), pending and resolved — see server portal/session/human. */
4502
4532
  NOTES_FILE: "notes.json",
4503
- /** The user's latest request, as the agent relayed it. Local: it is the user's own words. */
4533
+ /** The user's latest request: the newest prompt the hook captured, or the one the agent relayed. Local. */
4504
4534
  REQUEST_FILE: "request.json",
4535
+ /** Every prompt the agent hook captured, one JSON line each, oldest first. `request.json` holds the newest. */
4536
+ REQUESTS_LOG_FILE: "requests.jsonl",
4537
+ /** The prompt history moved aside when `requests.jsonl` fills. The same words, so the same rule. */
4538
+ REQUESTS_LOG_ROTATED_FILE: "requests.1.jsonl",
4539
+ APP_NOTES_FILE: "app-notes.json",
4540
+ // confirmed targeting hints for the next session; never evidence
4505
4541
  PLATFORM_MOMENTS_FILE: "platform-moments.json",
4506
4542
  // moments already said, so each is said once
4507
4543
  /**
4508
- * the project's cloud binding — .reticle/cloud.json, written by `reticle link`. Git-checked and
4544
+ * the project's cloud binding — .reticle/cloud.json, written by `reticle connect`. Git-checked and
4509
4545
  * non-secret: the project id, the API origin, and where its dashboard lives. The KEY lives in
4510
4546
  * ~/.reticle/credentials.json instead, because that one must never reach a repository.
4511
4547
  */
@@ -4596,6 +4632,111 @@ function projectIdFrom(pkgName, rootPath, hash) {
4596
4632
  return `${base}-${hash(rootPath)}`;
4597
4633
  }
4598
4634
 
4635
+ // ../../../core/dist/identity/checkout.js
4636
+ var MAX_UP_LEVELS = 50;
4637
+ var GITDIR_PREFIX = "gitdir:";
4638
+ var BRANCH_PREFIX = "ref: refs/heads/";
4639
+ var WORKTREES_SEGMENT = "worktrees";
4640
+ var GIT_DIR_NAME = ".git";
4641
+ function checkoutOf(dir, readFile) {
4642
+ const sep2 = dir.includes("\\") ? "\\" : "/";
4643
+ let current = trimEnd(dir, sep2);
4644
+ for (let level = 0; level <= MAX_UP_LEVELS; level += 1) {
4645
+ const head = readFile(`${current}${sep2}${GIT_DIR_NAME}${sep2}HEAD`);
4646
+ if (head !== void 0) {
4647
+ return withBranch({
4648
+ root: current,
4649
+ mainRoot: current,
4650
+ commonDir: `${current}${sep2}${GIT_DIR_NAME}`,
4651
+ linked: false
4652
+ }, head);
4653
+ }
4654
+ const pointer = readFile(`${current}${sep2}${GIT_DIR_NAME}`);
4655
+ if (pointer !== void 0 && pointer.trim().startsWith(GITDIR_PREFIX)) {
4656
+ return linkedCheckout(current, pointer.trim().slice(GITDIR_PREFIX.length).trim(), sep2, readFile);
4657
+ }
4658
+ const parent = parentOf(current, sep2);
4659
+ if (parent === void 0)
4660
+ return void 0;
4661
+ current = parent;
4662
+ }
4663
+ return void 0;
4664
+ }
4665
+ function mainCheckoutTwin(path, readFile) {
4666
+ const checkout = checkoutOf(path, readFile);
4667
+ if (checkout === void 0 || checkout.root === checkout.mainRoot)
4668
+ return void 0;
4669
+ return `${checkout.mainRoot}${path.slice(checkout.root.length)}`;
4670
+ }
4671
+ function linkedCheckout(root, gitdir, sep2, readFile) {
4672
+ const absolute = resolvePath(root, gitdir.split(/[\\/]/).join(sep2), sep2);
4673
+ const parts = absolute.split(sep2);
4674
+ const isWorktree = WORKTREES_SEGMENT === parts[parts.length - 2];
4675
+ const gitDirOfRepo = parts.slice(0, -2);
4676
+ const commonDir = isWorktree ? gitDirOfRepo.join(sep2) : absolute;
4677
+ const mainRoot = isWorktree && GIT_DIR_NAME === gitDirOfRepo[gitDirOfRepo.length - 1] ? gitDirOfRepo.slice(0, -1).join(sep2) : root;
4678
+ const head = readFile(`${absolute}${sep2}HEAD`);
4679
+ const checkout = { root, mainRoot, commonDir, linked: isWorktree };
4680
+ return head === void 0 ? checkout : withBranch(checkout, head);
4681
+ }
4682
+ function withBranch(checkout, head) {
4683
+ const ref = head.trim();
4684
+ return ref.startsWith(BRANCH_PREFIX) ? { ...checkout, branch: ref.slice(BRANCH_PREFIX.length) } : checkout;
4685
+ }
4686
+ function resolvePath(base, rel, sep2) {
4687
+ const absolute = rel.startsWith(sep2) || /^[A-Za-z]:/.test(rel);
4688
+ const parts = (absolute ? rel : `${base}${sep2}${rel}`).split(sep2);
4689
+ const out = [];
4690
+ for (const part of parts) {
4691
+ if ("." === part || "" === part && out.length > 0)
4692
+ continue;
4693
+ if (".." === part) {
4694
+ if (out.length > 1)
4695
+ out.pop();
4696
+ continue;
4697
+ }
4698
+ out.push(part);
4699
+ }
4700
+ return out.join(sep2);
4701
+ }
4702
+ function parentOf(path, sep2) {
4703
+ const at = path.lastIndexOf(sep2);
4704
+ if (at <= 0)
4705
+ return void 0;
4706
+ const parent = path.slice(0, at);
4707
+ return /^[A-Za-z]:$/.test(parent) ? void 0 : parent;
4708
+ }
4709
+ function trimEnd(path, sep2) {
4710
+ let end = path.length;
4711
+ while (end > 1 && sep2 === path[end - 1])
4712
+ end -= 1;
4713
+ return path.slice(0, end);
4714
+ }
4715
+
4716
+ // ../../../core/dist/identity/state-dir.js
4717
+ var stateDirNotAbsolute = (dir) => `The Reticle state folder "${dir}" is not an absolute path, so Reticle did not use it. A relative folder is made inside the folder this process runs in, which is often the project. Set ${ReticleEnv.PAIRING_TOKEN_DIR} and ${ReticleEnv.STATE_DIR} to absolute paths, or unset them.`;
4718
+ var stateDirResolvedFromCwd = (dir, resolved) => `The Reticle state folder "${dir}" is a relative path. Reticle uses "${resolved}", in the folder this process runs in. Set ${ReticleEnv.PAIRING_TOKEN_DIR} to an absolute path to remove this warning.`;
4719
+ var stateDirWindowsPathOnPosix = (dir, fallback) => `The Reticle state folder "${dir}" is a Windows path, and this system does not use Windows paths. Reticle did not use it, and uses "${fallback}". Set ${ReticleEnv.PAIRING_TOKEN_DIR} to a path for this system, or unset it.`;
4720
+ var WINDOWS_DRIVE = /^[A-Za-z]:/;
4721
+ var POSIX_SEPARATOR = "/";
4722
+ function resolveStateDir(override, fallback, rules, warn) {
4723
+ if (override !== void 0 && override.length > 0) {
4724
+ if (rules.isAbsolute(override)) {
4725
+ return override;
4726
+ }
4727
+ if (rules.sep !== POSIX_SEPARATOR || !WINDOWS_DRIVE.test(override)) {
4728
+ const resolved = rules.resolve(override);
4729
+ warn(stateDirResolvedFromCwd(override, resolved));
4730
+ return resolved;
4731
+ }
4732
+ warn(stateDirWindowsPathOnPosix(override, fallback));
4733
+ }
4734
+ if (!rules.isAbsolute(fallback)) {
4735
+ throw new Error(stateDirNotAbsolute(fallback));
4736
+ }
4737
+ return fallback;
4738
+ }
4739
+
4599
4740
  // ../../../core/dist/registry/daemon-registry.js
4600
4741
  var DAEMON_REGISTRY_PREFIX = "daemon-";
4601
4742
  var DAEMON_REGISTRY_SUFFIX = ".json";
@@ -4686,6 +4827,32 @@ function readNearestField(startDir, basename, field, readFile, maxDepth, accept)
4686
4827
  }
4687
4828
  return void 0;
4688
4829
  }
4830
+ function readConfigField(startDir, field, readFile, accept) {
4831
+ const own = readNearestField(
4832
+ startDir,
4833
+ RETICLE_CONFIG_BASENAME,
4834
+ field,
4835
+ readFile,
4836
+ MAX_CONFIG_SEARCH_DEPTH,
4837
+ accept
4838
+ );
4839
+ if (own !== void 0) return own;
4840
+ const twin = mainCheckoutTwin(startDir, (path) => {
4841
+ try {
4842
+ return readFile(path);
4843
+ } catch {
4844
+ return void 0;
4845
+ }
4846
+ });
4847
+ return twin === void 0 ? void 0 : readNearestField(
4848
+ twin,
4849
+ RETICLE_CONFIG_BASENAME,
4850
+ field,
4851
+ readFile,
4852
+ MAX_CONFIG_SEARCH_DEPTH,
4853
+ accept
4854
+ );
4855
+ }
4689
4856
  function readNearestPackageName(startDir, readFile = readFileOrThrow) {
4690
4857
  return readNearestField(
4691
4858
  startDir,
@@ -4697,32 +4864,16 @@ function readNearestPackageName(startDir, readFile = readFileOrThrow) {
4697
4864
  );
4698
4865
  }
4699
4866
  function readConfiguredProjectId(startDir, readFile = readFileOrThrow) {
4700
- return readNearestField(
4701
- startDir,
4702
- RETICLE_CONFIG_BASENAME,
4703
- "projectId",
4704
- readFile,
4705
- MAX_CONFIG_SEARCH_DEPTH,
4706
- nonEmptyString
4707
- );
4867
+ return readConfigField(startDir, "projectId", readFile, nonEmptyString);
4708
4868
  }
4709
4869
  function readConfiguredPort(startDir, readFile = readFileOrThrow) {
4710
- return readNearestField(
4711
- startDir,
4712
- RETICLE_CONFIG_BASENAME,
4713
- "port",
4714
- readFile,
4715
- MAX_CONFIG_SEARCH_DEPTH,
4716
- tcpPort
4717
- );
4870
+ return readConfigField(startDir, "port", readFile, tcpPort);
4718
4871
  }
4719
4872
  function readConfiguredTestIdAttribute(startDir, readFile = readFileOrThrow) {
4720
- return readNearestField(
4873
+ return readConfigField(
4721
4874
  startDir,
4722
- RETICLE_CONFIG_BASENAME,
4723
4875
  "testIdAttribute",
4724
4876
  readFile,
4725
- MAX_CONFIG_SEARCH_DEPTH,
4726
4877
  (value) => "string" === typeof value && TESTID_ATTR_PATTERN.test(value) ? value : void 0
4727
4878
  );
4728
4879
  }
@@ -4831,6 +4982,12 @@ function announceDevServer(entry, home = stateHome(), io = nodeIo) {
4831
4982
  }
4832
4983
  };
4833
4984
  }
4985
+ var DEV_PROXY_URL_ENV = "PORTLESS_URL";
4986
+ function announcedUrl(env, reported, port) {
4987
+ const proxied = env[DEV_PROXY_URL_ENV];
4988
+ if (proxied !== void 0 && proxied.length > 0) return proxied;
4989
+ return reported ?? `http://localhost:${String(port)}/`;
4990
+ }
4834
4991
 
4835
4992
  // src/svelte-source.ts
4836
4993
  var import_node_module = require("node:module");
@@ -5160,7 +5317,10 @@ export function createReticleEmitter() {
5160
5317
  };
5161
5318
  }
5162
5319
 
5163
- export function commitAndSignal() {}
5320
+ // It wraps the APP's write, so the write still runs: Reticle does nothing in production, the app does.
5321
+ export function commitAndSignal(_emitter, mutate) {
5322
+ return mutate();
5323
+ }
5164
5324
  export function registerReticleDomain() {}
5165
5325
 
5166
5326
  export function buildSnapshot() {
@@ -5377,8 +5537,18 @@ function isHtmlEntry(id, specifier, root) {
5377
5537
  return candidate.endsWith(target.startsWith("/") ? target : `/${target}`);
5378
5538
  }
5379
5539
  function readPairingToken() {
5380
- const override = process.env[ReticleEnv.PAIRING_TOKEN_DIR];
5381
- const dir = override !== void 0 && override.length > 0 ? override : (0, import_node_path8.join)((0, import_node_os2.homedir)(), ReticleDir.ROOT);
5540
+ let dir;
5541
+ try {
5542
+ dir = resolveStateDir(
5543
+ process.env[ReticleEnv.PAIRING_TOKEN_DIR],
5544
+ (0, import_node_path8.join)((0, import_node_os2.homedir)(), ReticleDir.ROOT),
5545
+ { isAbsolute: import_node_path8.isAbsolute, resolve: import_node_path8.resolve, sep: import_node_path8.sep },
5546
+ (warning) => console.warn(`[reticle] ${warning}`)
5547
+ );
5548
+ } catch (error) {
5549
+ console.warn(`[reticle] ${error instanceof Error ? error.message : String(error)}`);
5550
+ return void 0;
5551
+ }
5382
5552
  return ensurePairingToken(dir);
5383
5553
  }
5384
5554
  var tokenWarned = false;
@@ -5441,10 +5611,7 @@ function reticle(options = {}) {
5441
5611
  const sourceMapping = options.sourceMapping !== false;
5442
5612
  const inject = options.inject !== false;
5443
5613
  const desktop = true === options.desktop;
5444
- const resolved = {
5445
- ...options,
5446
- projectId: resolveProjectId(options.projectId, process.cwd())
5447
- };
5614
+ const projectIdIn = (dir) => resolveProjectId(options.projectId, dir);
5448
5615
  let htmlEntrySpecifier;
5449
5616
  let root;
5450
5617
  let command;
@@ -5459,7 +5626,9 @@ function reticle(options = {}) {
5459
5626
  let connectDelivered = false;
5460
5627
  let lastPortWarning;
5461
5628
  const resolveLazy = () => {
5462
- const { port, warning } = resolveDaemonPort(resolved.port, resolved.projectId, process.cwd());
5629
+ const appDir = root ?? process.cwd();
5630
+ const resolved = { ...options, projectId: projectIdIn(appDir) };
5631
+ const { port, warning } = resolveDaemonPort(resolved.port, resolved.projectId, appDir);
5463
5632
  if (warning !== void 0 && warning !== lastPortWarning) {
5464
5633
  lastPortWarning = warning;
5465
5634
  warn(warning);
@@ -5655,7 +5824,7 @@ ${code}`;
5655
5824
  port,
5656
5825
  pid: process.pid,
5657
5826
  root: opts.root ?? process.cwd(),
5658
- url: server.resolvedUrls?.local[0] ?? `http://localhost:${String(port)}/`,
5827
+ url: announcedUrl(process.env, server.resolvedUrls?.local[0], port),
5659
5828
  ...opts.sdkVersion === void 0 || 0 === opts.sdkVersion.length ? {} : { sdkVersion: opts.sdkVersion },
5660
5829
  startedAt: Date.now(),
5661
5830
  ...opts.projectId === void 0 ? {} : { projectId: opts.projectId }
package/dist/index.js CHANGED
@@ -2,11 +2,11 @@ import { existsSync, readFileSync } from 'node:fs';
2
2
  import { missingTokenWarning } from './token/missing-token.js';
3
3
  import { ensurePairingToken } from './token/ensure-token.js';
4
4
  import { homedir } from 'node:os';
5
- import { join } from 'node:path';
6
- import { RETICLE_RENDER_PREHOOK, ReticleDir, ReticleEnv, RETICLE_ROOT_GLOBAL, RETICLE_SDK_VERSION_GLOBAL, } from '@reticlehq/core';
5
+ import { isAbsolute, join, resolve, sep } from 'node:path';
6
+ import { RETICLE_RENDER_PREHOOK, ReticleDir, ReticleEnv, RETICLE_ROOT_GLOBAL, RETICLE_SDK_VERSION_GLOBAL, resolveStateDir, } from '@reticlehq/core';
7
7
  import { readConfiguredTestIdAttribute, resolveProjectId } from './project-id.js';
8
8
  import { resolveDaemonPort } from './discover-port.js';
9
- import { announceDevServer } from './announce.js';
9
+ import { announceDevServer, announcedUrl } from './announce.js';
10
10
  import { stampSvelte } from './svelte-source.js';
11
11
  import { NODE_MODULES, ignoredFileNotice, optsOutOfStamping, shouldStamp, shouldStampSvelte, stamp, } from './stamping.js';
12
12
  import { resolvableChain, sdkPackageVersion, sdkBuildFingerprint, viteMajor, optimizerOptionsKey, optimizerOptions, } from './installed.js';
@@ -135,8 +135,17 @@ function isHtmlEntry(id, specifier, root) {
135
135
  * undefined if the daemon hasn't started yet (the page reloads once it has). Exported for testing.
136
136
  */
137
137
  export function readPairingToken() {
138
- const override = process.env[ReticleEnv.PAIRING_TOKEN_DIR];
139
- const dir = override !== undefined && override.length > 0 ? override : join(homedir(), ReticleDir.ROOT);
138
+ let dir;
139
+ try {
140
+ // The daemon's rule: a relative folder is resolved against the working folder, and a Windows
141
+ // path on POSIX is not used. Both warn, so the two sides agree on one token file.
142
+ dir = resolveStateDir(process.env[ReticleEnv.PAIRING_TOKEN_DIR], join(homedir(), ReticleDir.ROOT), { isAbsolute, resolve, sep }, (warning) => console.warn(`[reticle] ${warning}`));
143
+ }
144
+ catch (error) {
145
+ // Only a home folder that is not absolute gets here. The dev server still starts.
146
+ console.warn(`[reticle] ${error instanceof Error ? error.message : String(error)}`);
147
+ return undefined;
148
+ }
140
149
  // Read-or-CREATE, matching the daemon. Reading alone meant a dev server started before the daemon
141
150
  // baked in an empty token and every page it served was refused — with the SDK loading and the
142
151
  // socket opening, so nothing looked broken. See ensure-token for the bisect.
@@ -259,12 +268,10 @@ export function reticle(options = {}) {
259
268
  const sourceMapping = options.sourceMapping !== false;
260
269
  const inject = options.inject !== false;
261
270
  const desktop = true === options.desktop;
262
- // Resolve the stable projectId once (explicit option, else derived from package.json + cwd) so the
263
- // app is identifiable across port changes with zero config.
264
- const resolved = {
265
- ...options,
266
- projectId: resolveProjectId(options.projectId, process.cwd()),
267
- };
271
+ // The stable projectId (explicit option, else `.reticle.json` / package.json in VITE'S root), so the
272
+ // app is identifiable across port changes with zero config. Read from the root, never the cwd:
273
+ // `vite apps/admin` from a monorepo root announced the root's app and saved flows under it.
274
+ const projectIdIn = (dir) => resolveProjectId(options.projectId, dir);
268
275
  /**
269
276
  * The specifier the HTML points at, e.g. `/src/main.tsx`.
270
277
  *
@@ -315,7 +322,9 @@ export function reticle(options = {}) {
315
322
  */
316
323
  const resolveLazy = () => {
317
324
  // Wherever the daemon for this project actually is; the order is argued in discover-port.ts.
318
- const { port, warning } = resolveDaemonPort(resolved.port, resolved.projectId, process.cwd());
325
+ const appDir = root ?? process.cwd();
326
+ const resolved = { ...options, projectId: projectIdIn(appDir) };
327
+ const { port, warning } = resolveDaemonPort(resolved.port, resolved.projectId, appDir);
319
328
  if (warning !== undefined && warning !== lastPortWarning) {
320
329
  lastPortWarning = warning;
321
330
  warn(warning);
@@ -568,7 +577,7 @@ export function reticle(options = {}) {
568
577
  port,
569
578
  pid: process.pid,
570
579
  root: opts.root ?? process.cwd(),
571
- url: server.resolvedUrls?.local[0] ?? `http://localhost:${String(port)}/`,
580
+ url: announcedUrl(process.env, server.resolvedUrls?.local[0], port),
572
581
  ...(opts.sdkVersion === undefined || 0 === opts.sdkVersion.length
573
582
  ? {}
574
583
  : { sdkVersion: opts.sdkVersion }),
@@ -9,7 +9,7 @@
9
9
  import { createHash } from 'node:crypto';
10
10
  import { dirname, join } from 'node:path';
11
11
  import { readFileSync } from 'node:fs';
12
- import { PROJECT_ID_HASH_LENGTH, TESTID_ATTR_PATTERN, projectIdFrom } from '@reticlehq/core';
12
+ import { PROJECT_ID_HASH_LENGTH, TESTID_ATTR_PATTERN, mainCheckoutTwin, projectIdFrom, } from '@reticlehq/core';
13
13
  /** The project config `reticle init` writes, and the id of record it carries. */
14
14
  const RETICLE_CONFIG_BASENAME = '.reticle.json';
15
15
  export { slugifyPackageName } from '@reticlehq/core';
@@ -76,6 +76,27 @@ function readNearestField(startDir, basename, field, readFile, maxDepth, accept)
76
76
  }
77
77
  return undefined;
78
78
  }
79
+ /**
80
+ * A field of the nearest `.reticle.json`, else of the main checkout's when `startDir` is in a git
81
+ * worktree that has none (it is git-ignored, or not committed yet). Every worktree of a project is
82
+ * the same project: one id, one daemon port.
83
+ */
84
+ function readConfigField(startDir, field, readFile, accept) {
85
+ const own = readNearestField(startDir, RETICLE_CONFIG_BASENAME, field, readFile, MAX_CONFIG_SEARCH_DEPTH, accept);
86
+ if (own !== undefined)
87
+ return own;
88
+ const twin = mainCheckoutTwin(startDir, (path) => {
89
+ try {
90
+ return readFile(path);
91
+ }
92
+ catch {
93
+ return undefined;
94
+ }
95
+ });
96
+ return twin === undefined
97
+ ? undefined
98
+ : readNearestField(twin, RETICLE_CONFIG_BASENAME, field, readFile, MAX_CONFIG_SEARCH_DEPTH, accept);
99
+ }
79
100
  /** Read the `name` from the nearest package.json at or above `startDir`, or undefined if none. */
80
101
  function readNearestPackageName(startDir, readFile = readFileOrThrow) {
81
102
  return readNearestField(startDir, 'package.json', 'name', readFile, MAX_PACKAGE_SEARCH_DEPTH, nonEmptyString);
@@ -87,7 +108,7 @@ function readNearestPackageName(startDir, readFile = readFileOrThrow) {
87
108
  * through `init` — it is not a second opinion, and where the two disagree the config wins.
88
109
  */
89
110
  export function readConfiguredProjectId(startDir, readFile = readFileOrThrow) {
90
- return readNearestField(startDir, RETICLE_CONFIG_BASENAME, 'projectId', readFile, MAX_CONFIG_SEARCH_DEPTH, nonEmptyString);
111
+ return readConfigField(startDir, 'projectId', readFile, nonEmptyString);
91
112
  }
92
113
  /**
93
114
  * Read the daemon port `reticle init --port` recorded in the nearest `.reticle.json`, or undefined.
@@ -97,7 +118,7 @@ export function readConfiguredProjectId(startDir, readFile = readFileOrThrow) {
97
118
  * project's daemon — and the bridge refused it.
98
119
  */
99
120
  export function readConfiguredPort(startDir, readFile = readFileOrThrow) {
100
- return readNearestField(startDir, RETICLE_CONFIG_BASENAME, 'port', readFile, MAX_CONFIG_SEARCH_DEPTH, tcpPort);
121
+ return readConfigField(startDir, 'port', readFile, tcpPort);
101
122
  }
102
123
  /**
103
124
  * Read `testIdAttribute` from the nearest `.reticle.json`, or undefined: the attribute this project
@@ -106,7 +127,7 @@ export function readConfiguredPort(startDir, readFile = readFileOrThrow) {
106
127
  * already agree on, so the plugin forwards it to `connect()` rather than asking for it twice.
107
128
  */
108
129
  export function readConfiguredTestIdAttribute(startDir, readFile = readFileOrThrow) {
109
- return readNearestField(startDir, RETICLE_CONFIG_BASENAME, 'testIdAttribute', readFile, MAX_CONFIG_SEARCH_DEPTH, (value) => ('string' === typeof value && TESTID_ATTR_PATTERN.test(value) ? value : undefined));
130
+ return readConfigField(startDir, 'testIdAttribute', readFile, (value) => 'string' === typeof value && TESTID_ATTR_PATTERN.test(value) ? value : undefined);
110
131
  }
111
132
  /**
112
133
  * Resolve the projectId for a plugin instance, most-local statement of intent first: an explicit
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reticlehq/vite-plugin",
3
- "version": "3.6.0",
3
+ "version": "3.8.0",
4
4
  "description": "Vite plugin for Reticle: dev-only source-map stamping plus auto-injected reticle.connect(). apply:'serve' guarantees it never ships to production.",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
@@ -41,15 +41,15 @@
41
41
  ],
42
42
  "dependencies": {
43
43
  "@babel/core": "^7.26.0",
44
- "@reticlehq/babel-plugin": "3.6.0",
45
- "@reticlehq/core": "3.6.0"
44
+ "@reticlehq/core": "3.8.0",
45
+ "@reticlehq/babel-plugin": "3.8.0"
46
46
  },
47
47
  "devDependencies": {
48
48
  "@types/babel__core": "^7.20.5",
49
49
  "esbuild": "^0.28.2",
50
50
  "svelte": "^5.57.1",
51
51
  "vite": "^8",
52
- "@reticlehq/react": "3.6.0"
52
+ "@reticlehq/react": "3.8.0"
53
53
  },
54
54
  "peerDependencies": {
55
55
  "vite": ">=4"