@reticlehq/server 3.8.1 → 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 (231) 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/drive-error.d.ts +6 -0
  114. package/dist/portal/input/drive-error.js +9 -0
  115. package/dist/portal/input/network-mock.js +3 -2
  116. package/dist/portal/input/real-input-press.d.ts +34 -0
  117. package/dist/portal/input/real-input-press.js +150 -0
  118. package/dist/portal/input/real-input.d.ts +21 -23
  119. package/dist/portal/input/real-input.js +153 -194
  120. package/dist/portal/input/viewport-tools.js +6 -3
  121. package/dist/portal/pool/browser-pool.d.ts +5 -1
  122. package/dist/portal/pool/browser-pool.js +32 -38
  123. package/dist/portal/pool/context-permissions.d.ts +20 -17
  124. package/dist/portal/pool/context-permissions.js +63 -1
  125. package/dist/portal/pool/csp-widen.d.ts +62 -0
  126. package/dist/portal/pool/csp-widen.js +132 -0
  127. package/dist/portal/pool/lease-page-error.d.ts +29 -0
  128. package/dist/portal/pool/lease-page-error.js +18 -0
  129. package/dist/portal/pool/page-error-journal.d.ts +57 -0
  130. package/dist/portal/pool/page-error-journal.js +181 -0
  131. package/dist/portal/pool/playwright-launcher.js +21 -8
  132. package/dist/portal/pool/pool-contract.d.ts +22 -0
  133. package/dist/portal/pool/pool-lease.d.ts +35 -2
  134. package/dist/portal/pool/pool-lease.js +23 -0
  135. package/dist/portal/pool/zero-install.js +69 -1
  136. package/dist/portal/session/dev-server/dev-command.d.ts +8 -0
  137. package/dist/portal/session/dev-server/dev-command.js +56 -8
  138. package/dist/portal/session/no-session-diagnosis.js +10 -3
  139. package/dist/portal/session/no-session-next-action.d.ts +12 -0
  140. package/dist/portal/session/no-session-next-action.js +44 -4
  141. package/dist/portal/session/no-session-watch.js +3 -1
  142. package/dist/portal/session/page-commands/undeclared-command.d.ts +9 -3
  143. package/dist/portal/session/page-commands/undeclared-command.js +16 -7
  144. package/dist/portal/session/recorded-gaps.d.ts +1 -1
  145. package/dist/portal/session/recorded-gaps.js +5 -2
  146. package/dist/portal/session/session-manager.js +40 -21
  147. package/dist/portal/session/session.js +4 -3
  148. package/dist/portal/session/zero-install-first-call.d.ts +20 -1
  149. package/dist/portal/session/zero-install-first-call.js +34 -2
  150. package/dist/start-wiring.d.ts +29 -2
  151. package/dist/start-wiring.js +73 -9
  152. package/dist/surface/mcp/mcp-proxy.js +10 -2
  153. package/dist/surface/mcp/mcp.js +5 -3
  154. package/dist/surface/mcp/peer/agents-connected.d.ts +6 -0
  155. package/dist/surface/mcp/peer/agents-connected.js +58 -0
  156. package/dist/surface/mcp/proxy/proxy-daemon-probe.js +17 -5
  157. package/dist/surface/tools/act/act-preflight.js +37 -17
  158. package/dist/surface/tools/act/act-target.js +3 -1
  159. package/dist/surface/tools/act/after-match.d.ts +7 -0
  160. package/dist/surface/tools/act/after-match.js +33 -0
  161. package/dist/surface/tools/act/resolve-target.d.ts +2 -0
  162. package/dist/surface/tools/act/resolve-target.js +35 -8
  163. package/dist/surface/tools/act/sequence-grade.d.ts +3 -1
  164. package/dist/surface/tools/act/sequence-grade.js +8 -1
  165. package/dist/surface/tools/act-sequence-tool.js +3 -0
  166. package/dist/surface/tools/act-tools.js +12 -12
  167. package/dist/surface/tools/assert/assert-verdict.js +28 -1
  168. package/dist/surface/tools/browser-tools.js +75 -7
  169. package/dist/surface/tools/dynamic-tools.js +2 -1
  170. package/dist/surface/tools/empty-session-list.d.ts +2 -0
  171. package/dist/surface/tools/empty-session-list.js +4 -2
  172. package/dist/surface/tools/error-recovery.js +43 -22
  173. package/dist/surface/tools/invoke-tool.js +27 -20
  174. package/dist/surface/tools/lease-hint.d.ts +7 -0
  175. package/dist/surface/tools/lease-hint.js +10 -3
  176. package/dist/surface/tools/lease-readiness.d.ts +21 -0
  177. package/dist/surface/tools/lease-readiness.js +47 -0
  178. package/dist/surface/tools/lease-seed.d.ts +2 -0
  179. package/dist/surface/tools/lease-seed.js +2 -1
  180. package/dist/surface/tools/lease-tools.js +43 -15
  181. package/dist/surface/tools/real-input-attempt.js +70 -31
  182. package/dist/surface/tools/repeat-hint.d.ts +40 -0
  183. package/dist/surface/tools/repeat-hint.js +171 -0
  184. package/dist/surface/tools/run-report.d.ts +2 -2
  185. package/dist/surface/tools/run-report.js +27 -15
  186. package/dist/surface/tools/tool-kit.d.ts +12 -0
  187. package/dist/surface/tools/tool-kit.js +7 -0
  188. package/dist/surface/tools/tools.js +26 -13
  189. package/dist/telemetry/argument-shape.js +16 -7
  190. package/dist/telemetry/cli-telemetry.d.ts +10 -1
  191. package/dist/telemetry/cli-telemetry.js +15 -0
  192. package/dist/telemetry/init-telemetry.d.ts +2 -1
  193. package/dist/telemetry/init-telemetry.js +47 -2
  194. package/dist/telemetry/install-trace.js +23 -4
  195. package/dist/telemetry/onboarding-funnel.d.ts +19 -2
  196. package/dist/telemetry/onboarding-funnel.js +25 -3
  197. package/dist/telemetry/proxy-telemetry.d.ts +2 -1
  198. package/dist/telemetry/proxy-telemetry.js +2 -2
  199. package/dist/telemetry/session-metrics.d.ts +1 -1
  200. package/dist/telemetry/session-metrics.js +67 -27
  201. package/dist/telemetry/telemetry.d.ts +3 -1
  202. package/dist/telemetry/telemetry.js +60 -28
  203. package/dist/telemetry/tool-refused.d.ts +2 -2
  204. package/dist/telemetry/tool-refused.js +9 -2
  205. package/docs/architecture.md +3 -6
  206. package/docs/cli/doctor.mdx +41 -2
  207. package/docs/cli/help.mdx +4 -2
  208. package/docs/cli/license.mdx +4 -5
  209. package/docs/cli/setup.mdx +13 -2
  210. package/docs/cli/status.mdx +4 -0
  211. package/docs/cli.mdx +1 -1
  212. package/docs/deploy-checks.md +25 -0
  213. package/docs/desktop.mdx +1 -1
  214. package/docs/enterprise.md +15 -20
  215. package/docs/environment.mdx +1 -0
  216. package/docs/first-drive.md +1 -1
  217. package/docs/install-manual.mdx +16 -1
  218. package/docs/packages/engine.mdx +1 -1
  219. package/docs/packages/react.mdx +14 -2
  220. package/docs/packages/server.mdx +1 -1
  221. package/docs/packages/tauri.mdx +1 -1
  222. package/docs/packages/test.mdx +1 -1
  223. package/docs/packages/vite-plugin.mdx +1 -0
  224. package/docs/packages.mdx +1 -1
  225. package/docs/platform-integration.md +1 -1
  226. package/docs/skill.md +1 -1
  227. package/docs/telemetry.md +4 -2
  228. package/package.json +14 -10
  229. package/LICENSE-ENTERPRISE +0 -37
  230. package/dist/features/ee/audit-log.d.ts +0 -14
  231. package/dist/features/ee/audit-log.js +0 -11
@@ -15,12 +15,13 @@
15
15
  * URLs, no error messages — the accumulator has no way to hold them, which is a stronger guarantee
16
16
  * than a rule saying it shouldn't.
17
17
  */
18
- import { BrowserLaunchKind, } from '@reticlehq/core/telemetry';
18
+ import { BrowserLaunchKind, ConnectFailure, OnboardingPhase, OnboardingStop, } from '@reticlehq/core/telemetry';
19
19
  import { errorSkeleton, fingerprintError } from './error-fingerprint.js';
20
20
  import { describeToolParams } from './argument-shape.js';
21
21
  import { machineSnapshot } from './machine-snapshot.js';
22
22
  import { classifyError } from './error-class.js';
23
23
  import { HudMetrics } from './hud-metrics.js';
24
+ import { reportOnboardingStop } from './onboarding-funnel.js';
24
25
  import { releaseVersionOf } from '../command/update/version-seen.js';
25
26
  /** Cap the distinct error shapes held in memory — a pathological loop must not grow unbounded. */
26
27
  const MAX_ERROR_KINDS = 40;
@@ -58,6 +59,12 @@ const BREADCRUMB_LENGTH = 12;
58
59
  * never fires for the sessions that need it most.
59
60
  */
60
61
  const UNVERIFIED_ACTION_NUDGE_AT = 3;
62
+ /** The launch failures that leave no browser to drive: a stop on the path to a first verdict. */
63
+ const CHROMIUM_UNAVAILABLE = new Set([
64
+ ConnectFailure.CHROMIUM_MISSING,
65
+ ConnectFailure.PLAYWRIGHT_MISSING,
66
+ ]);
67
+ const DRIVEN_STEP = 'driven';
61
68
  /**
62
69
  * Mutable counters for one daemon lifetime. A plain class, not a store: it is process-local, it never
63
70
  * persists, and it dies with the daemon.
@@ -187,10 +194,12 @@ export class SessionMetrics {
187
194
  }
188
195
  /** A tab went into the background (true) or came back (false): its HUD time pauses meanwhile. */
189
196
  setHudTabHidden(tab, hidden) {
190
- if (hidden)
197
+ if (hidden) {
191
198
  this.#hud.hide(tab);
192
- else
199
+ }
200
+ else {
193
201
  this.#hud.show(tab);
202
+ }
194
203
  }
195
204
  /** A tab went away: its HUD stops accruing time. */
196
205
  closeHudTab(tab) {
@@ -204,12 +213,14 @@ export class SessionMetrics {
204
213
  startToolCall(tool, args) {
205
214
  this.recordToolCall(tool, args);
206
215
  this.#concurrent += 1;
207
- if (this.#concurrent > this.#peakConcurrent)
216
+ if (this.#concurrent > this.#peakConcurrent) {
208
217
  this.#peakConcurrent = this.#concurrent;
218
+ }
209
219
  let settled = false;
210
220
  return (durationMs) => {
211
- if (settled)
212
- return; // a double-settle would corrupt the concurrency count
221
+ if (settled) {
222
+ return;
223
+ } // a double-settle would corrupt the concurrency count
213
224
  settled = true;
214
225
  this.#concurrent = Math.max(0, this.#concurrent - 1);
215
226
  this.#busyMs += durationMs;
@@ -243,8 +254,9 @@ export class SessionMetrics {
243
254
  */
244
255
  recordUnknownTool(name) {
245
256
  this.#unknownToolCalls += 1;
246
- if (name === undefined || 0 === name.length)
257
+ if (name === undefined || 0 === name.length) {
247
258
  return;
259
+ }
248
260
  const key = name.slice(0, 64);
249
261
  const seen = this.#unknownTools.get(key);
250
262
  if (seen !== undefined) {
@@ -253,8 +265,9 @@ export class SessionMetrics {
253
265
  }
254
266
  // Stop growing once full: a pathological loop must not balloon the payload. The head is what
255
267
  // gets built; the tail is noise.
256
- if (this.#unknownTools.size >= MAX_ERROR_KINDS)
268
+ if (this.#unknownTools.size >= MAX_ERROR_KINDS) {
257
269
  return;
270
+ }
258
271
  this.#unknownTools.set(key, 1);
259
272
  }
260
273
  recordToolCall(tool, args) {
@@ -269,8 +282,9 @@ export class SessionMetrics {
269
282
  if (this.#currentRun > 1) {
270
283
  const best = this.#repeatRuns.get(tool) ?? 0;
271
284
  if (this.#currentRun > best &&
272
- (this.#repeatRuns.has(tool) || this.#repeatRuns.size < MAX_REPEAT_TOOLS))
285
+ (this.#repeatRuns.has(tool) || this.#repeatRuns.size < MAX_REPEAT_TOOLS)) {
273
286
  this.#repeatRuns.set(tool, this.#currentRun);
287
+ }
274
288
  }
275
289
  this.#toolCalls += 1;
276
290
  this.#lifetimeToolCalls += 1;
@@ -280,23 +294,27 @@ export class SessionMetrics {
280
294
  bump(this.#toolCounts, tool);
281
295
  this.#inFlight = tool;
282
296
  this.#breadcrumb.push(tool);
283
- if (this.#breadcrumb.length > BREADCRUMB_LENGTH)
297
+ if (this.#breadcrumb.length > BREADCRUMB_LENGTH) {
284
298
  this.#breadcrumb.shift();
285
- if (args === undefined)
299
+ }
300
+ if (args === undefined) {
286
301
  return;
302
+ }
287
303
  // Which PARAMETERS get used, per tool — names only (plus a short allowlist of our own enums).
288
304
  // Bounded by our own schemas: a tool has a fixed handful of declared parameters, so this map
289
305
  // cannot grow with traffic the way a value-carrying one would.
290
306
  let perTool = this.#toolParams.get(tool);
291
307
  if (perTool === undefined) {
292
- if (this.#toolParams.size >= MAX_PARAM_TOOLS)
308
+ if (this.#toolParams.size >= MAX_PARAM_TOOLS) {
293
309
  return;
310
+ }
294
311
  perTool = new Map();
295
312
  this.#toolParams.set(tool, perTool);
296
313
  }
297
314
  for (const param of describeToolParams(args)) {
298
- if (perTool.size < MAX_PARAMS_PER_TOOL || perTool.has(param))
315
+ if (perTool.size < MAX_PARAMS_PER_TOOL || perTool.has(param)) {
299
316
  bump(perTool, param);
317
+ }
300
318
  }
301
319
  }
302
320
  /**
@@ -310,8 +328,9 @@ export class SessionMetrics {
310
328
  // indistinguishable from the rest inside one `toolErrors` count.
311
329
  bump(this.#errorClasses, classifyError(message));
312
330
  this.#currentCallErrored = true;
313
- if (NO_SESSION_ERROR.test(message))
331
+ if (NO_SESSION_ERROR.test(message)) {
314
332
  this.#noSessionErrors += 1;
333
+ }
315
334
  const fingerprint = fingerprintError(message);
316
335
  const existing = this.#errorKinds.get(fingerprint);
317
336
  if (existing !== undefined) {
@@ -319,8 +338,9 @@ export class SessionMetrics {
319
338
  return;
320
339
  }
321
340
  // Stop growing once the cap is hit; the tail is noise and the head is what gets fixed.
322
- if (this.#errorKinds.size >= MAX_ERROR_KINDS)
341
+ if (this.#errorKinds.size >= MAX_ERROR_KINDS) {
323
342
  return;
343
+ }
324
344
  this.#errorKinds.set(fingerprint, {
325
345
  fingerprint,
326
346
  count: 1,
@@ -343,8 +363,9 @@ export class SessionMetrics {
343
363
  existing.count += 1;
344
364
  return;
345
365
  }
346
- if (this.#sdkFailureKinds.size >= MAX_ERROR_KINDS)
366
+ if (this.#sdkFailureKinds.size >= MAX_ERROR_KINDS) {
347
367
  return;
368
+ }
348
369
  this.#sdkFailureKinds.set(fingerprint, {
349
370
  fingerprint,
350
371
  count: 1,
@@ -428,8 +449,9 @@ export class SessionMetrics {
428
449
  /** The 1-based occurrence of this finding in the session, or undefined past the cap. */
429
450
  recordBugOccurrence(fingerprint) {
430
451
  const seen = this.#bugOccurrences.get(fingerprint);
431
- if (seen === undefined && this.#bugOccurrences.size >= MAX_BUG_FINGERPRINTS)
452
+ if (seen === undefined && this.#bugOccurrences.size >= MAX_BUG_FINGERPRINTS) {
432
453
  return undefined;
454
+ }
433
455
  const occurrence = (seen ?? 0) + 1;
434
456
  this.#bugOccurrences.set(fingerprint, occurrence);
435
457
  return occurrence;
@@ -437,8 +459,9 @@ export class SessionMetrics {
437
459
  recordBug(kind) {
438
460
  this.#bugsFound += 1;
439
461
  this.#lifetimeBugsFound += 1;
440
- if (this.#bugKinds.size < MAX_ERROR_KINDS || this.#bugKinds.has(kind))
462
+ if (this.#bugKinds.size < MAX_ERROR_KINDS || this.#bugKinds.has(kind)) {
441
463
  bump(this.#bugKinds, kind);
464
+ }
442
465
  const first = !this.#seenBugKinds.has(kind);
443
466
  this.#seenBugKinds.add(kind);
444
467
  return first;
@@ -457,8 +480,9 @@ export class SessionMetrics {
457
480
  this.#connections.set(kind, stats);
458
481
  let settled = false;
459
482
  return (failure) => {
460
- if (settled)
483
+ if (settled) {
461
484
  return;
485
+ }
462
486
  settled = true;
463
487
  if (failure === undefined) {
464
488
  stats.successes += 1;
@@ -466,12 +490,21 @@ export class SessionMetrics {
466
490
  }
467
491
  stats.failures ??= {};
468
492
  stats.failures[failure] = (stats.failures[failure] ?? 0) + 1;
493
+ if (CHROMIUM_UNAVAILABLE.has(failure)) {
494
+ reportOnboardingStop({
495
+ phase: OnboardingPhase.FIRST_RUN,
496
+ step: DRIVEN_STEP,
497
+ reason: failure,
498
+ stop: OnboardingStop.CHROMIUM_UNAVAILABLE,
499
+ });
500
+ }
469
501
  };
470
502
  }
471
503
  recordClient(name, version) {
472
504
  const key = name.slice(0, 64);
473
- if (this.#clients.size < MAX_CLIENTS)
505
+ if (this.#clients.size < MAX_CLIENTS) {
474
506
  this.#clients.add(key);
507
+ }
475
508
  // The version was already in the handshake and was being dropped. A regression in agent
476
509
  // behaviour is usually a client version, and without this it is unattributable.
477
510
  if (version !== undefined && '' !== version && this.#clientVersions.size < MAX_CLIENTS) {
@@ -511,8 +544,9 @@ export class SessionMetrics {
511
544
  summarize(final, exit) {
512
545
  // The last call has no successor to resolve it, so resolve it here. Only on a FINAL summary:
513
546
  // a periodic flush is not the end of anything and would resolve a call still in flight.
514
- if (final)
547
+ if (final) {
515
548
  this.#resolveRecovery();
549
+ }
516
550
  const machine = machineSnapshot();
517
551
  // EVERY event reports its own window, the final one included: summing a session's
518
552
  // `session_progress` + `daemon_stopped` rows is then its total. The final event used to swap in
@@ -612,10 +646,12 @@ export class SessionMetrics {
612
646
  */
613
647
  #resolveRecovery() {
614
648
  if (this.#priorCallErrored) {
615
- if (this.#currentCallErrored)
649
+ if (this.#currentCallErrored) {
616
650
  this.#errorsRepeated += 1;
617
- else
651
+ }
652
+ else {
618
653
  this.#errorsRecovered += 1;
654
+ }
619
655
  }
620
656
  this.#priorCallErrored = this.#currentCallErrored;
621
657
  this.#currentCallErrored = false;
@@ -625,14 +661,18 @@ export class SessionMetrics {
625
661
  * that left explains everything after it, and a verdict settles whatever preceded it.
626
662
  */
627
663
  #endReason() {
628
- if (0 === this.#lifetimeToolCalls)
664
+ if (0 === this.#lifetimeToolCalls) {
629
665
  return 'never_used';
630
- if (this.#clientLeft)
666
+ }
667
+ if (this.#clientLeft) {
631
668
  return 'client_left';
632
- if (this.#unsettledActions > 0)
669
+ }
670
+ if (this.#unsettledActions > 0) {
633
671
  return 'abandoned';
634
- if (this.#lifetimeVerifications > 0)
672
+ }
673
+ if (this.#lifetimeVerifications > 0) {
635
674
  return 'verified';
675
+ }
636
676
  return 'explored';
637
677
  }
638
678
  /**
@@ -1,4 +1,4 @@
1
- import { TelemetryEventKind, ProjectIdSource, type Crash, type Feedback, type Identity, type BugFound, type InitOutcome, type OnboardingStep, type InstallSource, type McpConnection, type McpOutage, type DaemonDeath, type ProxyExit, type WakeRefusal, type DaemonSpawnMethod, type ToolRefusal, type AppInstrumentation, type HarnessRun, type HudEvent, type PlatformLink, type ProjectProfile, type SessionSummary, type TelemetryActor, type Verification, type VersionChange } from '@reticlehq/core/telemetry';
1
+ import { TelemetryEventKind, ProjectIdSource, type Crash, type Feedback, type Identity, type BugFound, type InitOutcome, type OnboardingStep, type InstallSource, type McpConnection, type McpOutage, type DaemonDeath, type GateCompleted, type ProxyExit, type WakeRefusal, type DaemonSpawnMethod, type ToolRefusal, type AppInstrumentation, type HarnessRun, type HudEvent, type PlatformLink, type ProjectProfile, type SessionSummary, type TelemetryActor, type Verification, type VersionChange } from '@reticlehq/core/telemetry';
2
2
  /**
3
3
  * Presence of this file marks the MACHINE as the Reticle team's own: every event carries
4
4
  * `internal: true`. The persistent twin of `RETICLE_INTERNAL=1`, for a dev machine where setting a
@@ -113,6 +113,8 @@ export interface TelemetryExtra {
113
113
  daemonDeath?: DaemonDeath;
114
114
  /** `wake_refused`: what held the port. */
115
115
  wakeRefusal?: WakeRefusal;
116
+ /** `gate_completed`: how `reticle gate` ended. */
117
+ gate?: GateCompleted;
116
118
  /** `daemon_started`: how the parent started the daemon. */
117
119
  spawnMethod?: DaemonSpawnMethod;
118
120
  /**
@@ -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>