@reticlehq/server 3.8.2 → 3.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (224) hide show
  1. package/LICENSE +201 -104
  2. package/NOTICE +5 -0
  3. package/README.md +1 -1
  4. package/bin/launch.js +13 -0
  5. package/bin/package.json +3 -0
  6. package/bin/reticle.js +66 -8
  7. package/dist/chromium-launch-options.d.ts +25 -0
  8. package/dist/chromium-launch-options.js +21 -1
  9. package/dist/command/cli/adhoc-verdict.js +34 -7
  10. package/dist/command/cli/answers/gate-exit.d.ts +6 -2
  11. package/dist/command/cli/answers/gate-exit.js +12 -2
  12. package/dist/command/cli/ci-hint.d.ts +44 -0
  13. package/dist/command/cli/ci-hint.js +72 -0
  14. package/dist/command/cli/cli-doctor.d.ts +6 -1
  15. package/dist/command/cli/cli-doctor.js +45 -4
  16. package/dist/command/cli/cli-flow-commands.d.ts +7 -1
  17. package/dist/command/cli/cli-flow-commands.js +39 -17
  18. package/dist/command/cli/cli-kill.d.ts +21 -0
  19. package/dist/command/cli/cli-kill.js +46 -3
  20. package/dist/command/cli/cli-parse.d.ts +5 -2
  21. package/dist/command/cli/cli-parse.js +10 -5
  22. package/dist/command/cli/cli-usage.js +3 -2
  23. package/dist/command/cli/cli-verify.d.ts +6 -1
  24. package/dist/command/cli/cli-verify.js +32 -13
  25. package/dist/command/cli/doctor/browser/chromium-hint.d.ts +6 -1
  26. package/dist/command/cli/doctor/browser/chromium-hint.js +11 -5
  27. package/dist/command/cli/doctor/doctor-fix.d.ts +44 -0
  28. package/dist/command/cli/doctor/doctor-fix.js +89 -0
  29. package/dist/command/cli/doctor/doctor-launcher-line.d.ts +23 -0
  30. package/dist/command/cli/doctor/doctor-launcher-line.js +60 -0
  31. package/dist/command/cli/doctor/doctor-rows.d.ts +4 -0
  32. package/dist/command/cli/doctor/doctor-rows.js +4 -0
  33. package/dist/command/cli/doctor-repairs.d.ts +64 -0
  34. package/dist/command/cli/doctor-repairs.js +214 -0
  35. package/dist/command/cli/drive/drive-command.js +3 -7
  36. package/dist/command/cli/first-use-checks.d.ts +41 -0
  37. package/dist/command/cli/first-use-checks.js +127 -0
  38. package/dist/command/cli/first-verdict-node.d.ts +3 -0
  39. package/dist/command/cli/first-verdict-node.js +189 -0
  40. package/dist/command/cli/first-verdict.d.ts +126 -0
  41. package/dist/command/cli/first-verdict.js +267 -0
  42. package/dist/command/cli/launch/sdk-version-match.d.ts +3 -2
  43. package/dist/command/cli/launch/sdk-version-match.js +3 -2
  44. package/dist/command/cli/lifecycle/daemon-lifecycle.js +6 -5
  45. package/dist/command/cli/mcp-command.js +13 -4
  46. package/dist/command/cli/ports/port-holder.d.ts +2 -0
  47. package/dist/command/cli/ports/port-holder.js +11 -3
  48. package/dist/command/cli/setup-mcp-cli.d.ts +27 -1
  49. package/dist/command/cli/setup-mcp-cli.js +98 -23
  50. package/dist/command/cli/status/status-lines.d.ts +1 -0
  51. package/dist/command/cli/status/status-lines.js +50 -17
  52. package/dist/command/cli/status-fields.js +17 -3
  53. package/dist/command/cli/try-command.d.ts +7 -0
  54. package/dist/command/cli/try-command.js +11 -3
  55. package/dist/command/cli.js +70 -33
  56. package/dist/command/daemon/binding/daemon-status-probe.d.ts +3 -1
  57. package/dist/command/daemon/binding/daemon-status-probe.js +4 -2
  58. package/dist/command/daemon/daemon-argv.d.ts +9 -0
  59. package/dist/command/daemon/daemon-argv.js +11 -0
  60. package/dist/command/daemon/daemon.d.ts +10 -3
  61. package/dist/command/daemon/daemon.js +17 -6
  62. package/dist/command/daemon/lifetime/daemon-exit.d.ts +24 -0
  63. package/dist/command/daemon/lifetime/daemon-exit.js +39 -0
  64. package/dist/command/daemon/lifetime/death-cause.d.ts +15 -0
  65. package/dist/command/daemon/lifetime/death-cause.js +130 -0
  66. package/dist/command/daemon/lifetime/heartbeat.d.ts +7 -15
  67. package/dist/command/daemon/lifetime/heartbeat.js +4 -13
  68. package/dist/command/daemon/port-state.d.ts +14 -0
  69. package/dist/command/daemon/port-state.js +31 -0
  70. package/dist/command/setup/agent-configs.d.ts +2 -0
  71. package/dist/command/setup/agent-configs.js +42 -15
  72. package/dist/command/setup/agent-next-steps.d.ts +77 -0
  73. package/dist/command/setup/agent-next-steps.js +284 -0
  74. package/dist/command/setup/agent-writer.js +11 -6
  75. package/dist/command/setup/init/init-host.js +2 -0
  76. package/dist/command/setup/init/init-runtime.d.ts +6 -1
  77. package/dist/command/setup/init/init-runtime.js +57 -21
  78. package/dist/command/setup/mcp-launcher.d.ts +3 -0
  79. package/dist/command/setup/mcp-launcher.js +11 -0
  80. package/dist/command/setup/setup-command.js +30 -12
  81. package/dist/command/setup/setup-mcp.d.ts +8 -0
  82. package/dist/command/setup/setup-mcp.js +47 -20
  83. package/dist/command/setup/start-dev-server.d.ts +8 -0
  84. package/dist/command/setup/start-dev-server.js +43 -0
  85. package/dist/command/version/identity/server-version.d.ts +2 -0
  86. package/dist/command/version/identity/server-version.js +2 -0
  87. package/dist/command/version/version-skew.d.ts +7 -0
  88. package/dist/command/version/version-skew.js +13 -2
  89. package/dist/features/license/license.d.ts +11 -71
  90. package/dist/features/license/license.js +43 -136
  91. package/dist/features/visual/desktop-capture.d.ts +33 -0
  92. package/dist/features/visual/desktop-capture.js +94 -0
  93. package/dist/features/visual/step-shots.js +139 -35
  94. package/dist/features/visual/visual-tools.d.ts +0 -18
  95. package/dist/features/visual/visual-tools.js +2 -81
  96. package/dist/import-graph.js +5 -6
  97. package/dist/index.d.ts +3 -1
  98. package/dist/index.js +14 -5
  99. package/dist/judgement/runs/run-ledger.d.ts +7 -5
  100. package/dist/judgement/runs/run-ledger.js +98 -69
  101. package/dist/judgement/runs/verification-sync.d.ts +6 -3
  102. package/dist/judgement/runs/verification-sync.js +21 -10
  103. package/dist/language/flows/flow-store-for-session.js +7 -5
  104. package/dist/language/flows/recording/tape/recordings.d.ts +2 -13
  105. package/dist/language/flows/recording/tape/recordings.js +41 -44
  106. package/dist/language/flows/replay.d.ts +17 -7
  107. package/dist/language/flows/replay.js +95 -44
  108. package/dist/launch-chromium.d.ts +1 -0
  109. package/dist/launch-chromium.js +49 -12
  110. package/dist/memory/cloud/sync-screenshots.js +6 -1
  111. package/dist/portal/bridge/bridge.d.ts +13 -4
  112. package/dist/portal/bridge/bridge.js +10 -9
  113. package/dist/portal/input/network-mock.js +3 -2
  114. package/dist/portal/input/real-input.d.ts +15 -0
  115. package/dist/portal/input/real-input.js +59 -3
  116. package/dist/portal/input/viewport-tools.js +6 -3
  117. package/dist/portal/pool/browser-pool.d.ts +5 -1
  118. package/dist/portal/pool/browser-pool.js +32 -38
  119. package/dist/portal/pool/context-permissions.d.ts +20 -17
  120. package/dist/portal/pool/context-permissions.js +63 -1
  121. package/dist/portal/pool/csp-widen.d.ts +62 -0
  122. package/dist/portal/pool/csp-widen.js +132 -0
  123. package/dist/portal/pool/lease-page-error.d.ts +29 -0
  124. package/dist/portal/pool/lease-page-error.js +18 -0
  125. package/dist/portal/pool/page-error-journal.d.ts +57 -0
  126. package/dist/portal/pool/page-error-journal.js +181 -0
  127. package/dist/portal/pool/playwright-launcher.js +21 -8
  128. package/dist/portal/pool/pool-contract.d.ts +22 -0
  129. package/dist/portal/pool/pool-lease.d.ts +35 -2
  130. package/dist/portal/pool/pool-lease.js +23 -0
  131. package/dist/portal/pool/zero-install.js +69 -1
  132. package/dist/portal/session/dev-server/dev-command.d.ts +8 -0
  133. package/dist/portal/session/dev-server/dev-command.js +56 -8
  134. package/dist/portal/session/no-session-diagnosis.js +10 -3
  135. package/dist/portal/session/no-session-next-action.d.ts +12 -0
  136. package/dist/portal/session/no-session-next-action.js +44 -4
  137. package/dist/portal/session/no-session-watch.js +3 -1
  138. package/dist/portal/session/page-commands/undeclared-command.d.ts +9 -3
  139. package/dist/portal/session/page-commands/undeclared-command.js +16 -7
  140. package/dist/portal/session/recorded-gaps.d.ts +1 -1
  141. package/dist/portal/session/recorded-gaps.js +5 -2
  142. package/dist/portal/session/session-manager.js +40 -21
  143. package/dist/portal/session/session.js +4 -3
  144. package/dist/portal/session/zero-install-first-call.d.ts +20 -1
  145. package/dist/portal/session/zero-install-first-call.js +34 -2
  146. package/dist/start-wiring.d.ts +29 -2
  147. package/dist/start-wiring.js +73 -9
  148. package/dist/surface/mcp/mcp-proxy.js +10 -2
  149. package/dist/surface/mcp/mcp.js +5 -3
  150. package/dist/surface/mcp/peer/agents-connected.d.ts +6 -0
  151. package/dist/surface/mcp/peer/agents-connected.js +58 -0
  152. package/dist/surface/mcp/proxy/proxy-daemon-probe.js +17 -5
  153. package/dist/surface/tools/act/act-preflight.js +37 -17
  154. package/dist/surface/tools/act/act-target.js +3 -1
  155. package/dist/surface/tools/act/after-match.d.ts +7 -0
  156. package/dist/surface/tools/act/after-match.js +33 -0
  157. package/dist/surface/tools/act/resolve-target.d.ts +2 -0
  158. package/dist/surface/tools/act/resolve-target.js +35 -8
  159. package/dist/surface/tools/act/sequence-grade.d.ts +3 -1
  160. package/dist/surface/tools/act/sequence-grade.js +8 -1
  161. package/dist/surface/tools/act-sequence-tool.js +3 -0
  162. package/dist/surface/tools/act-tools.js +10 -10
  163. package/dist/surface/tools/assert/assert-verdict.js +28 -1
  164. package/dist/surface/tools/browser-tools.js +75 -7
  165. package/dist/surface/tools/dynamic-tools.js +2 -1
  166. package/dist/surface/tools/empty-session-list.d.ts +2 -0
  167. package/dist/surface/tools/empty-session-list.js +4 -2
  168. package/dist/surface/tools/error-recovery.js +43 -22
  169. package/dist/surface/tools/invoke-tool.js +27 -20
  170. package/dist/surface/tools/lease-hint.d.ts +7 -0
  171. package/dist/surface/tools/lease-hint.js +10 -3
  172. package/dist/surface/tools/lease-readiness.d.ts +21 -0
  173. package/dist/surface/tools/lease-readiness.js +47 -0
  174. package/dist/surface/tools/lease-seed.d.ts +2 -0
  175. package/dist/surface/tools/lease-seed.js +2 -1
  176. package/dist/surface/tools/lease-tools.js +43 -15
  177. package/dist/surface/tools/repeat-hint.d.ts +40 -0
  178. package/dist/surface/tools/repeat-hint.js +171 -0
  179. package/dist/surface/tools/run-report.d.ts +2 -2
  180. package/dist/surface/tools/run-report.js +27 -15
  181. package/dist/surface/tools/tool-kit.d.ts +12 -0
  182. package/dist/surface/tools/tool-kit.js +7 -0
  183. package/dist/surface/tools/tools.js +26 -13
  184. package/dist/telemetry/argument-shape.js +16 -7
  185. package/dist/telemetry/cli-telemetry.d.ts +10 -1
  186. package/dist/telemetry/cli-telemetry.js +15 -0
  187. package/dist/telemetry/init-telemetry.d.ts +2 -1
  188. package/dist/telemetry/init-telemetry.js +47 -2
  189. package/dist/telemetry/install-trace.js +23 -4
  190. package/dist/telemetry/onboarding-funnel.d.ts +19 -2
  191. package/dist/telemetry/onboarding-funnel.js +25 -3
  192. package/dist/telemetry/proxy-telemetry.d.ts +2 -1
  193. package/dist/telemetry/proxy-telemetry.js +2 -2
  194. package/dist/telemetry/session-metrics.d.ts +1 -1
  195. package/dist/telemetry/session-metrics.js +67 -27
  196. package/dist/telemetry/telemetry.d.ts +3 -1
  197. package/dist/telemetry/telemetry.js +60 -28
  198. package/dist/telemetry/tool-refused.d.ts +2 -2
  199. package/dist/telemetry/tool-refused.js +9 -2
  200. package/docs/architecture.md +3 -6
  201. package/docs/cli/doctor.mdx +41 -2
  202. package/docs/cli/help.mdx +4 -2
  203. package/docs/cli/license.mdx +4 -5
  204. package/docs/cli/setup.mdx +13 -2
  205. package/docs/cli/status.mdx +4 -0
  206. package/docs/cli.mdx +1 -1
  207. package/docs/deploy-checks.md +25 -0
  208. package/docs/desktop.mdx +1 -1
  209. package/docs/enterprise.md +15 -20
  210. package/docs/environment.mdx +1 -0
  211. package/docs/first-drive.md +1 -1
  212. package/docs/install-manual.mdx +16 -1
  213. package/docs/packages/engine.mdx +1 -1
  214. package/docs/packages/server.mdx +1 -1
  215. package/docs/packages/tauri.mdx +1 -1
  216. package/docs/packages/test.mdx +1 -1
  217. package/docs/packages.mdx +1 -1
  218. package/docs/platform-integration.md +1 -1
  219. package/docs/skill.md +1 -1
  220. package/docs/telemetry.md +4 -2
  221. package/package.json +13 -9
  222. package/LICENSE-ENTERPRISE +0 -37
  223. package/dist/features/ee/audit-log.d.ts +0 -14
  224. package/dist/features/ee/audit-log.js +0 -11
@@ -138,20 +138,24 @@ const Env = {
138
138
  };
139
139
  const isDisabled = (env, cwd = process.cwd()) => {
140
140
  const off = new Set(['0', 'false', 'off', 'no']);
141
- if (off.has((env[Env.DISABLE] ?? '').toLowerCase()))
141
+ if (off.has((env[Env.DISABLE] ?? '').toLowerCase())) {
142
142
  return true;
143
+ }
143
144
  // Recording to a local file is not "phoning home", and the guards below exist to stop us phoning
144
145
  // home. Chief among them is the source-checkout rule — which is exactly where a release sweep is
145
146
  // driven from, so a sink that inherited it would record nothing and look like it had worked.
146
- if ((env[Env.FILE] ?? '') !== '')
147
+ if ((env[Env.FILE] ?? '') !== '') {
147
148
  return false;
148
- if (env[Env.VITEST] !== undefined)
149
- return true; // a test run is not a user
149
+ }
150
+ if (env[Env.VITEST] !== undefined) {
151
+ return true;
152
+ } // a test run is not a user
150
153
  // Developing Reticle is not using Reticle. The `.env` carrying RETICLE_TELEMETRY=0 is gitignored,
151
154
  // so it only exists on the machine that made it — a fresh clone would phone home on a
152
155
  // contributor's first `reticle serve`. The repo marker is committed, so this guarantee travels.
153
- if (isReticleSourceCheckout(cwd))
156
+ if (isReticleSourceCheckout(cwd)) {
154
157
  return true;
158
+ }
155
159
  const dnt = env[Env.DO_NOT_TRACK];
156
160
  return 'string' === typeof dnt && dnt !== '' && dnt !== '0';
157
161
  };
@@ -191,19 +195,22 @@ const readIdFile = (file) => {
191
195
  export const resolveIdentity = (file = ID_FILE) => {
192
196
  try {
193
197
  const existing = readIdFile(file);
194
- if (existing !== undefined)
198
+ if (existing !== undefined) {
195
199
  return { anonymousId: existing, firstRun: false };
200
+ }
196
201
  const id = randomUUID();
197
202
  mkdirSync(dirname(file), { recursive: true });
198
203
  try {
199
204
  writeFileSync(file, id, { encoding: 'utf8', flag: 'wx' });
200
205
  }
201
206
  catch (error) {
202
- if (!isAlreadyThere(error))
207
+ if (!isAlreadyThere(error)) {
203
208
  throw error;
209
+ }
204
210
  const raced = readIdFile(file);
205
- if (raced !== undefined)
211
+ if (raced !== undefined) {
206
212
  return { anonymousId: raced, firstRun: false };
213
+ }
207
214
  // An empty file left by a crash: replace it, as the old code did.
208
215
  writeFileSync(file, id, 'utf8');
209
216
  }
@@ -235,30 +242,40 @@ export const projectFingerprint = (cwd) => {
235
242
  // stable across every directory inside one machine's checkout, a package.json root is the same for
236
243
  // a non-git project — and the raw cwd is stable across nothing, which is why it is last. See
237
244
  // `project-identity.test.ts` for what a wrong order costs.
238
- const { key, source } = origin !== undefined
239
- ? { key: origin, source: ProjectIdSource.GIT_ORIGIN }
240
- : root !== undefined
241
- ? { key: root, source: ProjectIdSource.GIT_ROOT }
242
- : packageRoot(cwd) !== null
243
- ? { key: packageRoot(cwd) ?? cwd, source: ProjectIdSource.PACKAGE_ROOT }
244
- : { key: cwd, source: ProjectIdSource.CWD };
245
+ const { key, source } = projectKey(cwd, origin, root);
245
246
  return {
246
247
  projectId: createHash('sha256').update(key).digest('hex').slice(0, 32),
247
248
  // Reported alongside so the analytics knows whether this id is comparable to another machine's.
248
249
  source,
249
250
  };
250
251
  };
252
+ /** The most stable key this directory has, and what it came from. */
253
+ function projectKey(cwd, origin, root) {
254
+ if (origin !== undefined) {
255
+ return { key: origin, source: ProjectIdSource.GIT_ORIGIN };
256
+ }
257
+ if (root !== undefined) {
258
+ return { key: root, source: ProjectIdSource.GIT_ROOT };
259
+ }
260
+ const pkg = packageRoot(cwd);
261
+ if (pkg !== null) {
262
+ return { key: pkg, source: ProjectIdSource.PACKAGE_ROOT };
263
+ }
264
+ return { key: cwd, source: ProjectIdSource.CWD };
265
+ }
251
266
  /** Nearest ancestor holding a package.json — the project boundary when there is no git. */
252
267
  const packageRoot = (cwd) => {
253
268
  let dir = cwd;
254
269
  // Same bound as the git walk: deep monorepos are well inside 20, and an unbounded loop on a
255
270
  // pathological path would hang the CLI's startup.
256
271
  for (let depth = 0; depth < 20; depth += 1) {
257
- if (existsSync(join(dir, 'package.json')))
272
+ if (existsSync(join(dir, 'package.json'))) {
258
273
  return dir;
274
+ }
259
275
  const parent = dirname(dir);
260
- if (parent === dir)
276
+ if (parent === dir) {
261
277
  return null;
278
+ }
262
279
  dir = parent;
263
280
  }
264
281
  return null;
@@ -329,18 +346,21 @@ export const createTelemetry = (opts) => {
329
346
  const cwd = opts.cwd ?? process.cwd();
330
347
  // A source checkout silences metrics but must not silence FEEDBACK — see onlyBlockedBySourceCheckout.
331
348
  const feedbackOnly = onlyBlockedBySourceCheckout(env, cwd);
332
- if (isDisabled(env, cwd) && !feedbackOnly)
349
+ if (isDisabled(env, cwd) && !feedbackOnly) {
333
350
  return NOOP;
351
+ }
334
352
  try {
335
- if (existsSync(join(reticleDir, OPT_OUT_FILE_NAME)))
336
- return NOOP; // the persistent `reticle telemetry disable` opt-out
353
+ if (existsSync(join(reticleDir, OPT_OUT_FILE_NAME))) {
354
+ return NOOP;
355
+ } // the persistent `reticle telemetry disable` opt-out
337
356
  }
338
357
  catch {
339
358
  /* unreadable home dir — fall through; the env-var opt-outs above still apply */
340
359
  }
341
360
  const apiKey = env[Env.KEY] ?? POSTHOG_KEY;
342
- if ('' === apiKey)
343
- return NOOP; // no key baked in (dev/test build) — nowhere to send, stay silent
361
+ if ('' === apiKey) {
362
+ return NOOP;
363
+ } // no key baked in (dev/test build) — nowhere to send, stay silent
344
364
  const now = opts.now ?? (() => Date.now());
345
365
  const doFetch = opts.fetchImpl ?? fetch;
346
366
  const url = `${(env[Env.URL] ?? DEFAULT_URL).replace(/\/+$/, '')}${TELEMETRY_PATH}`;
@@ -375,8 +395,9 @@ export const createTelemetry = (opts) => {
375
395
  // How this process was launched: a property of the process, so read once. Only the bucket leaves.
376
396
  const launch = launchMethodOf(process.argv[1]);
377
397
  const home = opts.home ?? homedir();
378
- if (!feedbackOnly)
398
+ if (!feedbackOnly) {
379
399
  showNoticeOnce(reticleDir);
400
+ }
380
401
  const spawnDetached = opts.spawnImpl ??
381
402
  ((command, args, input) => {
382
403
  const bodyFile = join(tmpdir(), `reticle-telemetry-${randomUUID()}.json`);
@@ -397,8 +418,9 @@ export const createTelemetry = (opts) => {
397
418
  const emit = async (kind, extra) => {
398
419
  // In a Reticle source checkout the emitter carries FEEDBACK and nothing else: somebody typed the
399
420
  // feedback, and no metric is worth phoning home from a contributor's clone.
400
- if (feedbackOnly && TelemetryEventKind.FEEDBACK_SUBMITTED !== kind)
421
+ if (feedbackOnly && TelemetryEventKind.FEEDBACK_SUBMITTED !== kind) {
401
422
  return false;
423
+ }
402
424
  // One clock read for the whole event: `ts` and the licence check must agree, and two calls to
403
425
  // now() can straddle an expiry.
404
426
  const eventTs = now();
@@ -470,6 +492,7 @@ export const createTelemetry = (opts) => {
470
492
  ...(extra?.proxyExit !== undefined ? { proxyExit: extra.proxyExit } : {}),
471
493
  ...(extra?.daemonDeath !== undefined ? { daemonDeath: extra.daemonDeath } : {}),
472
494
  ...(extra?.wakeRefusal !== undefined ? { wakeRefusal: extra.wakeRefusal } : {}),
495
+ ...(extra?.gate !== undefined ? { gate: extra.gate } : {}),
473
496
  // A SCALAR: the event build and the wire schema, no `blocks` entry.
474
497
  ...(extra?.spawnMethod !== undefined ? { spawnMethod: extra.spawnMethod } : {}),
475
498
  };
@@ -484,7 +507,7 @@ export const createTelemetry = (opts) => {
484
507
  // PostHog filters and breakdowns operate on top-level properties, so a nested `feedback.stack`
485
508
  // would be invisible to the exact "which stacks report the most bugs" question this exists to
486
509
  // answer. One prefix keeps them grouped in the UI without a nested path.
487
- const { anonymousId: distinctId, event: name, ts, sessionId: eventSessionId, feedback, session, project, verification, versionChange, crash, identity, connection, init, onboarding, bug, refusal, outage, instrumentation, hud, link, harness, proxyExit, daemonDeath, wakeRefusal, ...rest } = event;
510
+ const { anonymousId: distinctId, event: name, ts, sessionId: eventSessionId, feedback, session, project, verification, versionChange, crash, identity, connection, init, onboarding, bug, refusal, outage, instrumentation, hud, link, harness, proxyExit, daemonDeath, wakeRefusal, gate, ...rest } = event;
488
511
  // `$session_id` is PostHog's OWN session property, so sending ours under that name lights up
489
512
  // its built-in session views and funnels for free rather than requiring custom HogQL everywhere.
490
513
  // `sessionId` rides only on events that happened inside a daemon run. A one-shot CLI command
@@ -522,6 +545,7 @@ export const createTelemetry = (opts) => {
522
545
  proxyExit,
523
546
  daemonDeath,
524
547
  wakeRefusal,
548
+ gate,
525
549
  };
526
550
  for (const [prefix, block] of Object.entries(blocks)) {
527
551
  for (const [key, value] of Object.entries(block ?? {})) {
@@ -583,8 +607,9 @@ export const createTelemetry = (opts) => {
583
607
  return response.ok !== false;
584
608
  }
585
609
  catch (error) {
586
- if (attempt === attempts - 1)
610
+ if (attempt === attempts - 1) {
587
611
  throw error;
612
+ }
588
613
  }
589
614
  }
590
615
  return false;
@@ -602,10 +627,17 @@ export const createTelemetry = (opts) => {
602
627
  */
603
628
  let invalidDropped = 0;
604
629
  export const droppedInvalidEvents = () => invalidDropped;
605
- /** `RETICLE_INTERNAL=1`, or the machine-wide marker file. Unreadable home → not internal. */
630
+ /** The values of `RETICLE_INTERNAL` that mark a run, after trim and lower case. Anything else is not. */
631
+ const INTERNAL_ON = new Set(['1', 'true', 'yes', 'on']);
632
+ /**
633
+ * The one decision for the `internal` field on every event: `RETICLE_INTERNAL` set to an "on" value,
634
+ * or the machine-wide marker file. An explicit signal only, never read from a name, email or path.
635
+ * Unreadable home → not internal.
636
+ */
606
637
  const isInternal = (env, dir) => {
607
- if ('1' === env[Env.INTERNAL] || 'true' === env[Env.INTERNAL])
638
+ if (INTERNAL_ON.has((env[Env.INTERNAL] ?? '').trim().toLowerCase())) {
608
639
  return true;
640
+ }
609
641
  try {
610
642
  return existsSync(join(dir, INTERNAL_MARKER_FILE));
611
643
  }
@@ -15,7 +15,7 @@ import { RefusalReason, type NoSessionReason } from '@reticlehq/core/telemetry';
15
15
  export declare function notePendingNoSessionReason(reason: NoSessionReason | undefined): void;
16
16
  /** A call that was served. Breaks the retry chain: what follows it is not a retry of anything. */
17
17
  export declare function noteToolServed(): void;
18
- /** One refused call, reported with its cause and whether it was itself a retry. */
19
- export declare function reportToolRefused(tool: string, reason: RefusalReason): void;
18
+ /** One refused call, reported with its cause, whether it was a retry, and the names of its arguments. */
19
+ export declare function reportToolRefused(tool: string, reason: RefusalReason, args?: Record<string, unknown>): void;
20
20
  /** Tests only — the counters are process-lifetime, so each case has to start from zero. */
21
21
  export declare function resetToolRefusals(): void;
@@ -11,6 +11,7 @@
11
11
  * covered without anybody remembering to cover it.
12
12
  */
13
13
  import { RefusalReason, TelemetryActor, TelemetryEventKind, } from '@reticlehq/core/telemetry';
14
+ import { describeToolParams } from './argument-shape.js';
14
15
  import { getTelemetry } from './telemetry.js';
15
16
  /**
16
17
  * Refusal events one daemon run will send.
@@ -54,8 +55,10 @@ export function notePendingNoSessionReason(reason) {
54
55
  export function noteToolServed() {
55
56
  lastRefusedTool = undefined;
56
57
  }
57
- /** One refused call, reported with its cause and whether it was itself a retry. */
58
- export function reportToolRefused(tool, reason) {
58
+ /** How many argument names one refusal carries. A call with more is malformed in its own way. */
59
+ const MAX_REFUSAL_PARAMS = 16;
60
+ /** One refused call, reported with its cause, whether it was a retry, and the names of its arguments. */
61
+ export function reportToolRefused(tool, reason, args = {}) {
59
62
  const retried = lastRefusedTool === tool;
60
63
  lastRefusedTool = tool;
61
64
  // Read and clear whatever the throw left, whether or not this refusal can use it: a reason left
@@ -71,6 +74,10 @@ export function reportToolRefused(tool, reason) {
71
74
  tool,
72
75
  reason,
73
76
  retried,
77
+ ...(() => {
78
+ const params = describeToolParams(args).slice(0, MAX_REFUSAL_PARAMS);
79
+ return 0 === params.length ? {} : { params };
80
+ })(),
74
81
  // Only where it means something. On any other refusal reason this field would be answering
75
82
  // a question nobody asked of it.
76
83
  ...(RefusalReason.NO_SESSION === reason && noSessionReason !== undefined
@@ -115,14 +115,11 @@ This is plain, reviewable, version-controllable data, not a black box.
115
115
 
116
116
  ---
117
117
 
118
- ## Open-core licensing (what's free, what's protected)
118
+ ## Licensing
119
119
 
120
- - The **protocol and the rules** (`open-verification`, `@reticlehq/core`, `@reticlehq/engine`) are **Apache-2.0**. The part that decides a verdict is the part a sceptical reader most wants to audit, so it is the part with no licence in the way.
121
- - The embeddable **SDK** (`@reticlehq/browser`, `@reticlehq/react` and the build adapters) is **Apache-2.0**, safe to ship inside your own app.
122
- - The **server / CLI** is under the **Functional Source License (FSL-1.1, Apache-2.0 future)**: source-available, converts to Apache-2.0 over time.
123
- - Enterprise-only features live behind a license gate and are clearly separated.
120
+ Every npm package is **Apache-2.0**: the protocol and the rules (`open-verification`, `@reticlehq/core`, `@reticlehq/engine`), the embeddable SDK and its adapters, and the server, CLI, `init` and spec runner. The `reticle-tauri` crate is MIT.
124
121
 
125
- See [LICENSE](https://github.com/reticlehq/reticle/blob/main/LICENSE) and each package's own `LICENSE` file. The licensing _mechanism_ is open and inspectable; activation is offline (no phone-home).
122
+ See [LICENSE](https://github.com/reticlehq/reticle/blob/main/LICENSE) and each package's own `LICENSE` file.
126
123
 
127
124
  ---
128
125
 
@@ -7,13 +7,14 @@ icon: stethoscope
7
7
  `reticle doctor` collapses the setup failure modes into one command. It is the first thing to run when something is wrong.
8
8
 
9
9
  ```bash
10
- npx @reticlehq/server doctor [--port N]
10
+ npx @reticlehq/server doctor [--fix] [--port N]
11
11
  ```
12
12
 
13
13
  ## Flags
14
14
 
15
15
  | Flag | Type | Default | What it does |
16
16
  | --- | --- | --- | --- |
17
+ | `--fix` | boolean | off | Repair what Reticle can repair safely, then run the checks again. See [Repair with --fix](#repair-with---fix) |
17
18
  | `--port N` | number | `RETICLE_PORT`, else the port in `.reticle.json`, else the daemon already serving this project, else `4400` | Which bridge port to diagnose |
18
19
 
19
20
  ## What it prints
@@ -50,10 +51,12 @@ When nothing has connected, the `sessions` line fails and the daemon's own diagn
50
51
  | Line | What it checks |
51
52
  | --- | --- |
52
53
  | `node` | The Node version running the CLI |
54
+ | `mcp` | Which coding agents found on this machine have Reticle's MCP server registered. Fails when an agent is found without it, and names the agent and `reticle setup mcp` |
53
55
  | `chromium` | Whether the exact Chromium revision the bundled Playwright wants is on disk. The most common silent failure |
54
56
  | `daemon` | Whether the port holds a Reticle daemon, a foreign process (named where possible), or nothing. Adds a `version` line when the daemon's version or contract fingerprint disagrees with the CLI's |
55
57
  | `sessions` | Whether any page has actually connected. Zero is a FAILING check rather than a neutral note, because it is the state the setup most often stalls in: wired correctly, daemon up, nothing dialled in. When it is zero the daemon's own no-session diagnosis prints below the checklist |
56
58
  | `agent link` | Whether an MCP client has listed Reticle's tools on this port **and actually called one**. Every other row here checks a component; this one checks the hop between your agent and the daemon. It is possible for the SDK to be injected, a session to be live, and your client to show every tool enabled, while no request has ever crossed from the agent. That state passes every other check and is the one people lose hours to |
59
+ | `launcher` | Only when an agent config starts Reticle from an installed copy (`node <path> mcp`) and that file does not exist, for example after `npm uninstall -g`. Names the agent, the path, and the command that writes a working entry |
57
60
  | `loopback` | Which of `localhost`'s two answers reaches the bridge. Printed **only when they disagree**. The daemon binds `127.0.0.1`, and a best-effort alias forwards `[::1]` to it so the name works everywhere. When that alias cannot open (IPv6 disabled, or something already holding `[::1]` on the port) the daemon serves IPv4 perfectly and a page told to dial `localhost` still cannot reach it, because Windows Chrome tries the IPv6 answer first. Every other row here goes green in that state |
58
61
  | `bridge port` | The port your app must dial, spelled out because mixing it up with the dev server port is the most common setup failure |
59
62
  | `port check` | Only when your project's configured port disagrees with the one the daemon is on. Prints the mismatch rather than leaving you to spot it across two lines |
@@ -113,9 +116,45 @@ The wanted revision and the root are both read back off the path Playwright itse
113
116
  before. That loop is why the line names the version to pin to.
114
117
  </Warning>
115
118
 
119
+ ## Repair with --fix
120
+
121
+ When a row has a safe repair, a plain `doctor` run ends with one more line:
122
+
123
+ ```
124
+ Run `reticle doctor --fix` to repair 2 of these.
125
+ ```
126
+
127
+ `reticle doctor --fix` prints the checks, applies the safe repairs in a fixed order, runs the checks again, and prints one line for each repair and one for each row that still fails. A repair changes only Reticle's own state:
128
+
129
+ | Order | Row | Repair | Done only when |
130
+ | --- | --- | --- | --- |
131
+ | 1 | `daemon` | Delete the pid file of a daemon that is not running | The process that the file names does not exist |
132
+ | 2 | `daemon` | Stop a Reticle daemon that holds the port and does not answer, as `reticle restart` does | The holder's command line is a Reticle daemon, and it is still silent after a grace period. A different program and the agent's own `reticle mcp` proxy are never stopped |
133
+ | 3 | `chromium` | Install the pinned Chromium of the bundled Playwright (a download) | No Chrome or Edge can stand in, and `RETICLE_CHROMIUM_PATH` is not set |
134
+ | 4 | `launcher` | Run `reticle setup mcp`, which replaces Reticle's own entry | An agent config starts Reticle from an installed copy that does not exist. `setup mcp` keeps an existing Claude Code entry, so for Claude Code the step stays `claude mcp remove reticle -s user`, then `setup mcp` |
135
+ | 5 | `mcp` | Run `reticle setup mcp` | An agent on this machine does not have Reticle registered |
136
+
137
+ Each repair checks again before it acts, so a second `--fix` finds nothing to do and changes nothing.
138
+
139
+ `--fix` never repairs a row that needs a change to your app, a process that Reticle cannot prove is its own daemon, or an action only you can do. For those rows it prints the step that the row names: for example `reticle init` for `project`, starting your agent for a stopped `daemon`, `reticle hook install` for `prompts`, a CSP or desktop change, or a version skew (restart the daemon when no other agent uses it).
140
+
141
+ ```
142
+ reticle doctor --fix
143
+ daemon pid file of a stopped daemon: fixed
144
+ daemon a Reticle daemon that does not answer: nothing to do
145
+ chromium Playwright's own Chromium: nothing to do
146
+ launcher a registered launcher that does not exist: nothing to do
147
+ mcp agent registrations: fixed
148
+ still needs: sync ✗ off: this project is not linked: run `reticle connect`; no push yet
149
+ ```
150
+
151
+ A registration takes effect when the agent reads its config again, so restart your agent after `mcp` is fixed.
152
+
116
153
  ## Exit codes
117
154
 
118
- Always `0`. `doctor` reports; it does not gate.
155
+ A plain `doctor` always exits `0`. It reports; it does not gate.
156
+
157
+ `doctor --fix` exits `0` only when no row fails after the repairs, and `1` otherwise.
119
158
 
120
159
  ## Worked example
121
160
 
package/docs/cli/help.mdx CHANGED
@@ -33,7 +33,7 @@ Get started
33
33
  try Watch Reticle drive a URL once and say which journeys work
34
34
 
35
35
  Check your app
36
- verify Check the saved flows against the running app; exit 0 means pass
36
+ verify Check saved flows, or one claim with --expect, against the running app; exit 0 = pass
37
37
  gate Fail unless passing checks cover the flows your changes touch
38
38
 
39
39
  Reticle on this machine
@@ -85,5 +85,7 @@ Two audiences, two channels: the structured line for logs and scripts, the plain
85
85
  | `1` | Help was printed because something else was wrong |
86
86
 
87
87
  <Note>
88
- A bare `reticle` with no arguments is **not** help. It is [`reticle serve`](/cli/serve).
88
+ A bare `reticle` with no arguments is **not** help. In a wired project it is [`reticle
89
+ serve`](/cli/serve). In a folder that Reticle has not wired, it gives a first verdict on the app:
90
+ see [the CLI reference](/cli).
89
91
  </Note>
@@ -20,17 +20,16 @@ One `reticle_license` JSON line, on stderr. On a checkout with no issuer key con
20
20
  {
21
21
  "t": "2026-08-15T18:42:26.758Z",
22
22
  "event": "reticle_license",
23
- "gated": ["audit-log"],
24
23
  "contact": "hey@reticle.sh",
25
24
  "status": "eval",
26
- "detail": "evaluation mode: enterprise features run free (no issuer key configured)"
25
+ "detail": "evaluation mode (no issuer key configured)"
27
26
  }
28
27
  ```
29
28
 
30
29
  | `status` | Meaning |
31
30
  | --------- | --------------------------------------------------- |
32
31
  | `active` | A valid license is present |
33
- | `eval` | Evaluation mode. Enterprise features run free |
32
+ | `eval` | Evaluation mode. No issuer key is configured |
34
33
  | `missing` | No license, and the build expects one |
35
34
  | `expired` | The key verified, but its expiry has passed |
36
35
  | `invalid` | The key was rejected, or the build is misconfigured |
@@ -47,6 +46,6 @@ Always `0`.
47
46
  npx @reticlehq/server license 2>&1 | jq -r .status
48
47
  ```
49
48
 
50
- <Card title="What enterprise covers" icon="building" href="/enterprise">
51
- The licensed surface, and what stays Apache 2.0.
49
+ <Card title="Enterprise" icon="building" href="/enterprise">
50
+ License keys, the roadmap, and the licensing model.
52
51
  </Card>
@@ -14,10 +14,21 @@ No flags, and nothing is asked. It works for your user account, not for one proj
14
14
 
15
15
  ## What it prints
16
16
 
17
- One line per agent: `✓ <agent>` for a new registration, `✓ <agent> (already set up)` when Reticle was there already, `⚠ <agent>: Reticle registration failed…` when a registration failed (the command then exits `1`), and `⚠ <agent>: add the reticle entry to <path> by hand` for a config it will not rewrite. If it finds no agent, it names every agent it looked for.
17
+ A `Your agents:` block, with one line for each agent and the one next step for that agent:
18
+
19
+ ```text
20
+ Your agents:
21
+ Claude Code plugin active: type /reload-plugins if it is open
22
+ Cursor added: restart Cursor, then turn Reticle on in Settings › MCP
23
+ Codex added: restart Codex, then /mcp shows reticle
24
+ ```
25
+
26
+ `already set up` means that Reticle was there already. `registration failed` gives the command to retry, and the command then exits `1`. `not added: add the reticle entry to <path> by hand` is for a config that it does not rewrite. If it finds no agent, it names every agent it looked for.
27
+
28
+ In a terminal, and not in CI, it then waits up to 60 seconds for each agent to connect. Then it prints `Connected: <agent> ✓`, or the step that is still missing for that agent. Press Ctrl+C to stop the wait. [`reticle status`](/cli/status) shows the same lines later.
18
29
 
19
30
  ## How you know it worked
20
31
 
21
- Restart your coding agent. An agent reads its server list only when it starts. After the restart, the `reticle_*` tools are listed.
32
+ Do the next step for your agent. Most agents read their server list only when they start. The Claude Code plugin loads with `/reload-plugins`. Then the `reticle_*` tools are listed, and `reticle status` shows `Connected: <agent> ✓`.
22
33
 
23
34
  Codex CLI keeps a TOML config. When the `codex` binary is on your PATH, Reticle registers through `codex mcp add`. Otherwise it prints the hand-edit lines above. Reticle then adds one line, `default_tools_approval_mode = "approve"`, under `[mcp_servers.reticle]` in `~/.codex/config.toml`. This line approves Reticle's own tools, so Codex does not ask before each call. Reticle changes no other line, and it keeps a mode that you set. To undo it, change that line to `default_tools_approval_mode = "prompt"`. Reticle does not change a mode that is already there.
@@ -35,8 +35,12 @@ reticle status
35
35
  http://localhost:5173/ (throttled, hidden)
36
36
  http://localhost:7699/ (throttled, hidden)
37
37
  agent link ✓ your agent has read and used Reticle's tools
38
+ agents Connected: Claude Code ✓
39
+ Cursor not connected yet: restart Cursor, then turn Reticle on in Settings › MCP
38
40
  ```
39
41
 
42
+ The `agents` row has one line for each coding agent on this machine. A line shows `✓` when that agent connected to Reticle at least one time. Otherwise it shows the step that is still missing. `reticle setup mcp` prints the same lines.
43
+
40
44
  And with no daemon on the port:
41
45
 
42
46
  ```text
package/docs/cli.mdx CHANGED
@@ -197,7 +197,7 @@ policy https://github.com/reticlehq/reticle/blob/main/docs/telemetry.md
197
197
  | `reticle drive <url>` | Foreground mode, for debugging |
198
198
  | `reticle mcp` | The MCP stdio proxy. Auto-starts the daemon if needed. This is what your agent runs |
199
199
 
200
- A bare `reticle` with no arguments is not help. It is `reticle serve`.
200
+ A bare `reticle` with no arguments is not help. In a project that `init` wired, it is `reticle serve`. In a folder that Reticle has not wired, it gives a first verdict with no agent and no `init`. It finds the folder's web app (it starts the dev script when the app does not run), or the one dev server that runs on this computer. It opens the page in a Reticle browser and checks four things: the page loads, no console error, no failed request, and each same-origin link opens (8 at most). It prints `verified: yes` or `verified: no` with each fault, and exits 0 only for `yes`. A page that goes to a sign-in page is verified up to there. It stops the dev server it started.
201
201
 
202
202
  <Note>
203
203
  When the Reticle SDK installed in the project is a different **major** version from the CLI you ran,
@@ -12,6 +12,8 @@ That makes the non-developer story complete: the SDK is auto-injected by the bui
12
12
 
13
13
  > **Recipe A works today; Recipe B does not exist yet.** `reticle verify` exits 0/1 and persists a run artifact right now. What is _not_ built is a hosted Reticle app that registers itself as a Vercel or Netlify check provider. Until it exists, use the CI recipe.
14
14
 
15
+ Using Reticle in CI or in a pipeline? Talk to a founder: [book a call](https://calendar.app.google/h9NRDbBBQetyTzWM6) or write to [hey@reticle.sh](mailto:hey@reticle.sh).
16
+
15
17
  ## The shape
16
18
 
17
19
  ```
@@ -54,6 +56,29 @@ The job's own pass/fail becomes the PR check. Nothing else is required.
54
56
 
55
57
  **Non-loopback previews need pairing.** For a real preview URL (not `localhost`), Reticle injects `reticle.connect()` with a one-time token and allow-lists the preview origin, so the app does not need to be rebuilt per environment. Confirm the SDK actually runs on the deployed build: a production build that tree-shakes the dev-only SDK will connect to nothing, and `verify` will (correctly) fail with _"no app connected."_
56
58
 
59
+ ## The Reticle GitHub Action
60
+
61
+ The Action replays your saved flows on each preview and posts one check with a verdict for each flow. On `deployment_status`, it reads the preview URL from the event, so you type no URL.
62
+
63
+ ```yaml
64
+ # .github/workflows/reticle.yml
65
+ name: reticle
66
+ on: [deployment_status]
67
+ jobs:
68
+ verify:
69
+ if: github.event.deployment_status.state == 'success'
70
+ runs-on: ubuntu-latest
71
+ steps:
72
+ - uses: actions/checkout@v4
73
+ - uses: reticlehq/reticle/action@v3.9.0
74
+ with:
75
+ api-key: ${{ secrets.RETICLE_API_KEY }}
76
+ # Only for a Vercel preview behind Deployment Protection.
77
+ vercel-bypass-secret: ${{ secrets.VERCEL_AUTOMATION_BYPASS_SECRET }}
78
+ ```
79
+
80
+ A Vercel preview behind Deployment Protection needs a [Protection Bypass for Automation](https://vercel.com/docs/deployment-protection/methods-to-bypass-deployment-protection/protection-bypass-automation) secret. Netlify has no bypass secret for its password protection, so turn off protection for deploy previews. The [Action README](https://github.com/reticlehq/reticle/tree/main/action) gives each input.
81
+
57
82
  ## Recipe B: the Checks API pattern (what a hosted Reticle would do)
58
83
 
59
84
  Both providers expose the same shape, which is why this is one integration rather than two:
package/docs/desktop.mdx CHANGED
@@ -224,7 +224,7 @@ Alternatively, since an Electron renderer _is_ Chromium, `--remote-debugging-por
224
224
 
225
225
  ```toml
226
226
  [dependencies]
227
- reticle-tauri = "3.8"
227
+ reticle-tauri = "3.9"
228
228
  ```
229
229
 
230
230
  ```rust
@@ -1,22 +1,22 @@
1
1
  ---
2
2
  title: Enterprise
3
- description: 'Premium access and activation, what is gated, the security and data-handling posture, and the licensing model.'
3
+ description: 'License keys and activation, the enterprise roadmap, the security and data-handling posture, and the licensing model.'
4
4
  icon: building
5
5
  ---
6
6
 
7
- Enterprise features ship inside the open package and are unlocked by a signed license key you set as `RETICLE_LICENSE_KEY`. Activation is verified locally with Ed25519 and makes **no network call**. Usage metrics are separate: they are on by default, and on a licensed build they carry your licence id. Turn them off with `RETICLE_TELEMETRY=0`. See [telemetry](/telemetry). Run `npx @reticlehq/server license` to see whether you are `active`, `eval`, `missing`, `expired` or `invalid`.
7
+ Every Reticle package is Apache-2.0. An enterprise customer gets a signed license key, which you set as `RETICLE_LICENSE_KEY`. Activation is verified locally with Ed25519 and makes **no network call**. Usage metrics are separate: they are on by default, and on a licensed build they carry your licence id. Turn them off with `RETICLE_TELEMETRY=0`. See [telemetry](/telemetry). Run `npx @reticlehq/server license` to see whether you are `active`, `eval`, `missing`, `expired` or `invalid`.
8
8
 
9
9
  > For how to connect a platform to Reticle, see [Platform integration](/platform-integration).
10
10
 
11
11
  > **Already have a key and just need to switch it on?** [Activating a license key](./license-activation) is the step-by-step version: where to set it on each platform, the editor and CI cases, how to read every status, and what to do when it does not work.
12
12
 
13
- ## How premium access works (offline, no phone-home)
13
+ ## How a license key works (offline, no phone-home)
14
14
 
15
- Enterprise (`ee/`) features ship **inside the open package**; they're source-available (free for development, testing, and evaluation). A **license key activates them in production**. Activation is verified locally with Ed25519 and sends nothing. Usage metrics, which you can turn off, carry your licence id on a licensed build.
15
+ No feature in the open packages needs a key. A key identifies your organisation to Reticle. Activation is verified locally with Ed25519 and sends nothing. Usage metrics, which you can turn off, carry your licence id on a licensed build.
16
16
 
17
17
  **The flow, end to end:**
18
18
 
19
- 1. **Buy.** Contact **[hey@reticle.sh](mailto:hey@reticle.sh)**; we issue you a signed license key (license id, org, plan, expiry, feature set). The **license id** is the stable handle on your subscription: it survives a rename, and it is what a renewal continues.
19
+ 1. **Buy.** Contact **[hey@reticle.sh](mailto:hey@reticle.sh)**; we issue you a signed license key (license id, org, plan, expiry). The **license id** is the stable handle on your subscription: it survives a rename, and it is what a renewal continues.
20
20
  2. **Install.** Set it on the machine running the Reticle server:
21
21
  ```bash
22
22
  export RETICLE_LICENSE_KEY="<your key>"
@@ -26,25 +26,21 @@ Enterprise (`ee/`) features ship **inside the open package**; they're source-ava
26
26
 
27
27
  ```
28
28
  active licensed to Acme Corp (enterprise), expires 2027-06-20T00:00:00.000Z
29
- eval evaluation mode: enterprise features run free (no issuer key configured)
30
- missing set RETICLE_LICENSE_KEY to activate enterprise features in production. Request a key from hey@reticle.sh
31
- expired license expired. Renew with hey@reticle.sh to keep using enterprise features
29
+ eval evaluation mode (no issuer key configured)
30
+ missing set RETICLE_LICENSE_KEY to activate a license. Request a key from hey@reticle.sh
31
+ expired license expired. Renew with hey@reticle.sh
32
32
  invalid license key rejected (<reason>). hey@reticle.sh can re-issue it
33
33
  ```
34
34
 
35
- Every status reports `contact`, so a reader who is stuck always has somebody to write to. An unlicensed status also reports `gated`, what a licence unlocks in the build you are running, so the command answers "what would this buy me, and how do I get one" without anybody having to find this page first. It is derived from the features the gate actually enforces, so it names what your build will really refuse rather than a roadmap. An `active` licence does not report it: that reader has already bought, and listing feature names at them answers nothing they asked.
35
+ Every status reports `contact`, so a reader who is stuck always has somebody to write to. A published release has the issuer key baked in. Only a build from source, with no issuer key, reports `eval`.
36
36
 
37
- 4. **Unlock.** Enterprise features now run; without a valid key they refuse to run (a clear error, never a silent half-feature). A published release has the issuer key baked in, so it enforces the key everywhere. Only a build without one (from source) runs in `eval` mode, where the features run free outside `NODE_ENV=production`.
38
- 5. **Renew.** Keys carry an expiry, shown as `expiresAt`; once it passes, the status is `expired`.
37
+ 4. **Renew.** Keys carry an expiry, shown as `expiresAt`; once it passes, the status is `expired`.
39
38
 
40
39
  > Procuring a license: contact **[hey@reticle.sh](mailto:hey@reticle.sh)**. Keys are issued offline and signed with Ed25519; the activation you run (`reticle license`) verifies them locally with no network call.
41
40
 
42
- ## What's gated (and the roadmap)
41
+ ## Roadmap
43
42
 
44
- The licensing **mechanism** is open core (inspectable, FSL). Only the **features** under `ee/` are gated:
45
-
46
- - **Today:** the activation gate + one gated feature (audit event recording, `audit-log`).
47
- - **Roadmap** (the reliably enterprise-only set): **SSO/SAML, SCIM, RBAC / team permissions, audit logs, multi-org management, verify-before-merge policy gates, and the hosted control-plane connectors.** These are the things a security/compliance org pays for; the core verification engine stays free forever.
43
+ **SSO/SAML, SCIM, RBAC / team permissions, audit logs, multi-org management, verify-before-merge policy gates, and the hosted control-plane connectors** are planned for the hosted Reticle platform, not for the open packages. The core verification engine stays free.
48
44
 
49
45
  > For pricing, write to [hey@reticle.sh](mailto:hey@reticle.sh). This page describes the mechanism, not the price list.
50
46
 
@@ -62,15 +58,14 @@ The honest one-pager a security review needs. Reticle is built so the answer to
62
58
  | Leak risk downstream? | The **`prod-preview` profile** redacts source `file:line`, raw bodies, and app-state values. |
63
59
  | Path safety | Run/flow ids are validated as single path segments on read **and** write (no traversal). |
64
60
 
65
- **Verify it yourself:** the SDK + server are source-available, so read the tree-shaking, the `127.0.0.1` bind, and the offline license verify. `SECURITY.md` has the disclosure process; an SBOM is available on request. SOC 2 is not in place; the posture above is the honest current state.
61
+ **Verify it yourself:** the SDK and server are open source, so read the tree-shaking, the `127.0.0.1` bind, and the offline license verify. `SECURITY.md` has the disclosure process; an SBOM is available on request. SOC 2 is not in place; the posture above is the honest current state.
66
62
 
67
- ## Licensing model (per package)
63
+ ## Licensing model
68
64
 
69
65
  | Scope | License | Why |
70
66
  | --- | --- | --- |
71
67
  | The protocol and the rules (`open-verification`, `core`, `engine`) | **Apache-2.0** | the part that DECIDES a verdict, auditable and reimplementable by anyone |
72
68
  | Embeddable SDK (`browser`, `react`, `babel-plugin`, `next`, `vite-plugin`, `electron`, `eslint-plugin`) | **Apache-2.0** | safe to ship inside your customers' apps; explicit patent grant |
73
- | Server / CLI / MCP (`server`, `init`, `test`) | **FSL-1.1-ALv2** | free for any use except reselling Reticle itself; converts to Apache after 2 years |
74
- | Enterprise features (`server/src/features/ee/`) | **Reticle Enterprise License** | source-available; free for dev/eval, license key required in production |
69
+ | Server / CLI / MCP (`server`, `init`, `test`) | **Apache-2.0** | use, change and redistribute it anywhere |
75
70
 
76
71
  Embedding / OEM / enterprise: **[hey@reticle.sh](mailto:hey@reticle.sh)**.
@@ -53,6 +53,7 @@ Reticle works with none of these set. Each one changes a single behaviour, and m
53
53
  | Variable | Read by | What it does |
54
54
  | --- | --- | --- |
55
55
  | `RETICLE_SKIP_GATE` | `reticle gate --hook` | Lets an agent's stop hook proceed past a failing gate. The skip is printed and recorded, never silent. It does not affect `reticle gate` without `--hook`, which is what CI runs. |
56
+ | `RETICLE_NO_CI_HINT` | `reticle gate`, `reticle verify`, `reticle affected` | `1` stops the one line these commands write to stderr at the end of a run in a CI runner or a container. The line names the pull request check and `https://reticle.sh/ci`. It never prints in an interactive terminal or for `gate --hook`, and it makes no network call. |
56
57
  | `RETICLE_VERIFY_TOKEN` | `reticle serve --http` | The bearer token the optional HTTP verify endpoint requires. |
57
58
  | `RETICLE_SECRET_<FIELD>` | replay, verify, crawl | The value typed into a password or secret field at replay. A recorded flow never stores it. See [what is recorded](/what-is-recorded). |
58
59
 
@@ -12,7 +12,7 @@ Everything here is written from a run of these exact steps, including the places
12
12
  ## Before you start
13
13
 
14
14
  ```bash
15
- node --version # 22.12 or newer
15
+ node --version # 22.15 or newer
16
16
  pnpm --version # this is a pnpm workspace; npm will not resolve it
17
17
  ```
18
18