@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
@@ -18,7 +18,7 @@
18
18
  * exact values allowed — anything outside the list is reported as `other`, so a schema change can
19
19
  * never quietly start forwarding free text.
20
20
  */
21
- import { ActionType, FeedbackKind, QueryBy, SnapshotMode } from '@reticlehq/core';
21
+ import { ActionType, FeedbackKind, QueryBy, ReticleTool, SnapshotMode } from '@reticlehq/core';
22
22
  /**
23
23
  * Parameters whose values are enums WE define, and the exact values allowed on the wire. An
24
24
  * unrecognized value reports `other` rather than itself; that fallback is what makes this safe
@@ -35,6 +35,9 @@ const SAFE_ENUM_PARAMS = new Map([
35
35
  ['by', new Set(Object.values(QueryBy))],
36
36
  ['mode', new Set(Object.values(SnapshotMode))],
37
37
  ['kind', new Set(Object.values(FeedbackKind))],
38
+ // `reticle_run { tool }` names the tool it calls. Our own tool names, so the session summary can
39
+ // say which tools agents reach through it. A name that is not a Reticle tool reports `other`.
40
+ ['tool', new Set(Object.values(ReticleTool))],
38
41
  // `level` has no core enum — console levels come from the DOM console API, not from us. Listed
39
42
  // literally BECAUSE there is nothing to derive from, which is a different situation from the four
40
43
  // above and the reason it is called out rather than left looking like an oversight.
@@ -49,10 +52,12 @@ export const OTHER_VALUE = 'other';
49
52
  */
50
53
  export function allowParamValues(name, values) {
51
54
  const allowed = SAFE_ENUM_PARAMS.get(name);
52
- if (allowed === undefined)
55
+ if (allowed === undefined) {
53
56
  return;
54
- for (const value of values)
57
+ }
58
+ for (const value of values) {
55
59
  allowed.add(value);
60
+ }
56
61
  }
57
62
  /**
58
63
  * The reportable form of one parameter: `name` on its own, or `name:value` when the parameter is a
@@ -60,10 +65,12 @@ export function allowParamValues(name, values) {
60
65
  */
61
66
  export function describeParam(name, value) {
62
67
  const allowed = SAFE_ENUM_PARAMS.get(name);
63
- if (allowed === undefined)
68
+ if (allowed === undefined) {
64
69
  return name;
65
- if (typeof value !== 'string')
70
+ }
71
+ if (typeof value !== 'string') {
66
72
  return name;
73
+ }
67
74
  return `${name}:${allowed.has(value) ? value : OTHER_VALUE}`;
68
75
  }
69
76
  /**
@@ -96,10 +103,12 @@ export function describeCliFlags(argv) {
96
103
  const flags = new Set();
97
104
  for (const arg of argv) {
98
105
  // A bare `--` ends the options: everything after it is passed through to something else.
99
- if (END_OF_OPTIONS === arg)
106
+ if (END_OF_OPTIONS === arg) {
100
107
  break;
101
- if (!arg.startsWith('--'))
108
+ }
109
+ if (!arg.startsWith('--')) {
102
110
  continue;
111
+ }
103
112
  // `--port=9000` — split so the NAME is reported and the value is discarded with everything else.
104
113
  const name = arg.split('=')[0] ?? arg;
105
114
  flags.add(KNOWN_CLI_FLAGS.has(name) ? name : OTHER_VALUE);
@@ -4,7 +4,8 @@
4
4
  * Lives in its own module so `cli.ts` stays a dispatcher, and so the `_daemon` exclusion below sits
5
5
  * next to the explanation of why it exists rather than buried in a startup function.
6
6
  */
7
- import { TelemetryActor } from '@reticlehq/core/telemetry';
7
+ import { TelemetryActor, type GateCompleted } from '@reticlehq/core/telemetry';
8
+ import { type Telemetry } from './telemetry.js';
8
9
  /** True when this command represents a person typing something. Exported for the gate. */
9
10
  export declare function isHumanCliCommand(command: string): boolean;
10
11
  /**
@@ -23,3 +24,11 @@ export declare function cliActorOf(env: NodeJS.ProcessEnv): TelemetryActor;
23
24
  */
24
25
  export declare function claimInstallReport(command: string, firstRun: boolean, stateDir: string): boolean;
25
26
  export declare function reportCliRun(argv: readonly string[]): void;
27
+ /**
28
+ * `gate_completed`: how one `reticle gate` run ended.
29
+ *
30
+ * `cli_command_run` says that the gate ran. Only this event says whether it passed. The gate is a
31
+ * one-shot command that exits immediately, so the send is DETACHED, like `init_completed`.
32
+ * Best-effort: a metric never changes what the gate decides.
33
+ */
34
+ export declare function reportGateCompleted(gate: GateCompleted, telemetry?: Pick<Telemetry, 'emit'>): void;
@@ -161,3 +161,18 @@ export function reportCliRun(argv) {
161
161
  flags: describeCliFlags(argv),
162
162
  });
163
163
  }
164
+ /**
165
+ * `gate_completed`: how one `reticle gate` run ended.
166
+ *
167
+ * `cli_command_run` says that the gate ran. Only this event says whether it passed. The gate is a
168
+ * one-shot command that exits immediately, so the send is DETACHED, like `init_completed`.
169
+ * Best-effort: a metric never changes what the gate decides.
170
+ */
171
+ export function reportGateCompleted(gate, telemetry = getTelemetry()) {
172
+ try {
173
+ void telemetry.emit(TelemetryEventKind.GATE_COMPLETED, { gate, detach: true });
174
+ }
175
+ catch {
176
+ /* a metric never fails the gate */
177
+ }
178
+ }
@@ -11,6 +11,7 @@
11
11
  * event loop alive and tax the command by most of a second.
12
12
  */
13
13
  import { type InitOutcome } from '@reticlehq/core/telemetry';
14
- export { InitFailure } from '@reticlehq/init';
14
+ import { InitFailure } from '@reticlehq/init';
15
+ export { InitFailure };
15
16
  /** Report one init outcome. Best-effort; setup must never fail because a metric did. */
16
17
  export declare function reportInitOutcome(init: InitOutcome): void;
@@ -10,12 +10,56 @@
10
10
  * same treatment `cli_command_run` gets, for the same reason: an in-process fetch would keep the
11
11
  * event loop alive and tax the command by most of a second.
12
12
  */
13
- import { TelemetryEventKind } from '@reticlehq/core/telemetry';
13
+ import { OnboardingPhase, OnboardingStop, TelemetryEventKind, } from '@reticlehq/core/telemetry';
14
+ import { InitFailure } from '@reticlehq/init';
14
15
  import { getTelemetry } from './telemetry.js';
15
16
  import { INSTALL_STATE_DIR, resolveInstallSource } from './install-source.js';
17
+ import { reportOnboardingStop } from './onboarding-funnel.js';
16
18
  // The failure vocabulary lives in `@reticlehq/init`, which is what classifies a failed run.
17
19
  // Re-exported so every consumer here is unchanged.
18
- export { InitFailure } from '@reticlehq/init';
20
+ export { InitFailure };
21
+ const PROJECT_DETECTED = 'project_detected';
22
+ const INSTRUMENTED = 'instrumented';
23
+ const APP_CONNECTED = 'app_connected';
24
+ /**
25
+ * Each init failure as one stop, with the first-run step it blocked. A `Record` over the whole
26
+ * enum, so a new `InitFailure` does not compile until it names its stop.
27
+ */
28
+ const INIT_STOP = {
29
+ [InitFailure.NO_PACKAGE_JSON]: { step: PROJECT_DETECTED, stop: OnboardingStop.NO_WEB_APP },
30
+ [InitFailure.MALFORMED_PACKAGE_JSON]: {
31
+ step: PROJECT_DETECTED,
32
+ stop: OnboardingStop.SETUP_FAILED,
33
+ },
34
+ [InitFailure.PREFLIGHT]: { step: PROJECT_DETECTED, stop: OnboardingStop.SETUP_FAILED },
35
+ [InitFailure.DEPENDENCY_INSTALL]: { step: INSTRUMENTED, stop: OnboardingStop.SETUP_FAILED },
36
+ [InitFailure.MCP_REGISTRATION]: { step: INSTRUMENTED, stop: OnboardingStop.MCP_NOT_REGISTERED },
37
+ [InitFailure.OTHER]: { step: INSTRUMENTED, stop: OnboardingStop.UNCLASSIFIED },
38
+ [InitFailure.BRIDGE_OCCUPIED]: { step: APP_CONNECTED, stop: OnboardingStop.SETUP_FAILED },
39
+ [InitFailure.DEV_SERVER]: { step: APP_CONNECTED, stop: OnboardingStop.DEV_SERVER_FAILED },
40
+ [InitFailure.APP_CONNECTION]: { step: APP_CONNECTED, stop: OnboardingStop.APP_NOT_CONNECTED },
41
+ [InitFailure.RUNTIME_ERROR]: { step: APP_CONNECTED, stop: OnboardingStop.UNCLASSIFIED },
42
+ [InitFailure.INTERRUPTED]: { step: APP_CONNECTED, stop: OnboardingStop.INTERRUPTED },
43
+ };
44
+ const INIT_STOP_UNKNOWN = INIT_STOP[InitFailure.OTHER];
45
+ function initStop(reason) {
46
+ if (reason !== undefined && Object.hasOwn(INIT_STOP, reason)) {
47
+ return INIT_STOP[reason];
48
+ }
49
+ return INIT_STOP_UNKNOWN;
50
+ }
51
+ /** The stop row of a failed init, or nothing for a run that worked. */
52
+ function reportInitStop(init) {
53
+ if (init.ok) {
54
+ return;
55
+ }
56
+ reportOnboardingStop({
57
+ phase: OnboardingPhase.FIRST_RUN,
58
+ ...initStop(init.reason),
59
+ ...(init.reason === undefined ? {} : { reason: init.reason }),
60
+ ...(init.stack === undefined ? {} : { stack: init.stack }),
61
+ }, { detach: true });
62
+ }
19
63
  /** Report one init outcome. Best-effort; setup must never fail because a metric did. */
20
64
  export function reportInitOutcome(init) {
21
65
  try {
@@ -27,6 +71,7 @@ export function reportInitOutcome(init) {
27
71
  // marker, same refusal to infer — see install-source.ts.
28
72
  installSource: resolveInstallSource(process.env, INSTALL_STATE_DIR),
29
73
  });
74
+ reportInitStop(init);
30
75
  }
31
76
  catch {
32
77
  /* a metric must never break `reticle init` */
@@ -1,6 +1,6 @@
1
1
  import { existsSync, readFileSync, rmSync } from 'node:fs';
2
2
  import { join } from 'node:path';
3
- import { OnboardingStepSchema } from '@reticlehq/core/telemetry';
3
+ import { OnboardingStepSchema, OnboardingStop } from '@reticlehq/core/telemetry';
4
4
  import { reportOnboardingStep } from './onboarding-funnel.js';
5
5
  /**
6
6
  * What the one-line installer could not report itself.
@@ -27,6 +27,22 @@ const TRACE_FILE = 'install-trace.jsonl';
27
27
  * file must not turn into thousands of events. Far above any real install, which writes three.
28
28
  */
29
29
  const MAX_LINES = 32;
30
+ /**
31
+ * The stop that each installer failure reason names. The keys are the reasons that `install.sh`
32
+ * and `install.ps1` write. A reason not in this table gets no stop.
33
+ */
34
+ const INSTALLER_STOP = {
35
+ no_node: OnboardingStop.NO_NODE,
36
+ node_too_old: OnboardingStop.NODE_TOO_OLD,
37
+ no_npm: OnboardingStop.CLI_INSTALL_FAILED,
38
+ npm_install: OnboardingStop.CLI_INSTALL_FAILED,
39
+ not_on_path: OnboardingStop.CLI_INSTALL_FAILED,
40
+ };
41
+ function installerStop(reason) {
42
+ return reason !== undefined && Object.hasOwn(INSTALLER_STOP, reason)
43
+ ? INSTALLER_STOP[reason]
44
+ : undefined;
45
+ }
30
46
  /**
31
47
  * Drain the installer's breadcrumbs, once.
32
48
  *
@@ -36,8 +52,9 @@ const MAX_LINES = 32;
36
52
  */
37
53
  export function drainInstallTrace(stateDir) {
38
54
  const path = join(stateDir, TRACE_FILE);
39
- if (!existsSync(path))
55
+ if (!existsSync(path)) {
40
56
  return 0;
57
+ }
41
58
  let drained = 0;
42
59
  try {
43
60
  const lines = readFileSync(path, 'utf8')
@@ -55,9 +72,11 @@ export function drainInstallTrace(stateDir) {
55
72
  // deliberate belt-and-braces: this is the one path where the payload is a file a person can
56
73
  // edit, and rule 3 is that unvalidated text never reaches the wire.
57
74
  const step = OnboardingStepSchema.safeParse(parsed);
58
- if (!step.success)
75
+ if (!step.success) {
59
76
  continue;
60
- void reportOnboardingStep(step.data);
77
+ }
78
+ const stop = installerStop(step.data.reason);
79
+ void reportOnboardingStep({ ...step.data, ...(stop === undefined ? {} : { stop }) });
61
80
  drained += 1;
62
81
  }
63
82
  }
@@ -1,4 +1,4 @@
1
- import { OnboardingPhase, type OnboardingStep } from '@reticlehq/core/telemetry';
1
+ import { OnboardingPhase, type OnboardingStep, type OnboardingStop } from '@reticlehq/core/telemetry';
2
2
  /**
3
3
  * The one place an onboarding step is reported.
4
4
  *
@@ -11,7 +11,24 @@ import { OnboardingPhase, type OnboardingStep } from '@reticlehq/core/telemetry'
11
11
  * arriving as a step name would be a rule-3 leak that no reader would ever notice, because a funnel
12
12
  * with one weird row still renders.
13
13
  */
14
- export declare function reportOnboardingStep(step: OnboardingStep): Promise<boolean>;
14
+ export declare function reportOnboardingStep(step: OnboardingStep, opts?: {
15
+ detach?: boolean;
16
+ }): Promise<boolean>;
17
+ /** One stop: the rung it blocked and its name. The status is always `failed`. */
18
+ export type StopRow = Omit<OnboardingStep, 'status' | 'stop'> & {
19
+ stop: OnboardingStop;
20
+ };
21
+ /**
22
+ * Report one stop on the path to a first verdict, once for each process and stop name.
23
+ *
24
+ * A daemon can fail to launch Chromium on every lease. One row says the stop happened here; more
25
+ * rows count retries, not people. Use `detach` from a CLI that exits next, or the row is lost.
26
+ */
27
+ export declare function reportOnboardingStop(row: StopRow, opts?: {
28
+ detach?: boolean;
29
+ }): void;
30
+ /** Tests only: forget the stops this process sent. */
31
+ export declare function resetOnboardingStops(): void;
15
32
  /**
16
33
  * Time a step and report it, whatever happens to it.
17
34
  *
@@ -12,13 +12,35 @@ import { getTelemetry } from './telemetry.js';
12
12
  * arriving as a step name would be a rule-3 leak that no reader would ever notice, because a funnel
13
13
  * with one weird row still renders.
14
14
  */
15
- export async function reportOnboardingStep(step) {
15
+ export async function reportOnboardingStep(step, opts = {}) {
16
16
  const parsed = OnboardingStepSchema.safeParse(step);
17
17
  // A malformed step is DROPPED rather than sent or thrown. Sending it would put unvalidated text on
18
18
  // the wire; throwing would let a metric change what the user's install does, which rule 5 forbids.
19
- if (!parsed.success)
19
+ if (!parsed.success) {
20
20
  return false;
21
- return getTelemetry().emit(TelemetryEventKind.ONBOARDING_STEP, { onboarding: parsed.data });
21
+ }
22
+ return getTelemetry().emit(TelemetryEventKind.ONBOARDING_STEP, {
23
+ onboarding: parsed.data,
24
+ ...(true === opts.detach ? { detach: true } : {}),
25
+ });
26
+ }
27
+ const stopsSent = new Set();
28
+ /**
29
+ * Report one stop on the path to a first verdict, once for each process and stop name.
30
+ *
31
+ * A daemon can fail to launch Chromium on every lease. One row says the stop happened here; more
32
+ * rows count retries, not people. Use `detach` from a CLI that exits next, or the row is lost.
33
+ */
34
+ export function reportOnboardingStop(row, opts = {}) {
35
+ if (stopsSent.has(row.stop)) {
36
+ return;
37
+ }
38
+ stopsSent.add(row.stop);
39
+ void reportOnboardingStep({ ...row, status: OnboardingStepStatus.FAILED }, opts);
40
+ }
41
+ /** Tests only: forget the stops this process sent. */
42
+ export function resetOnboardingStops() {
43
+ stopsSent.clear();
22
44
  }
23
45
  /**
24
46
  * Time a step and report it, whatever happens to it.
@@ -10,6 +10,7 @@
10
10
  * never arrived: the POST was killed every time and nothing threw.
11
11
  */
12
12
  import { type ProxyExitReason, type WakeRefusalPresence } from '@reticlehq/core/telemetry';
13
+ import type { DaemonDeath } from '@reticlehq/core';
13
14
  import { type SessionMetrics } from './session-metrics.js';
14
15
  import { type Telemetry } from './telemetry.js';
15
16
  export declare function flushProxySessionMetrics(telemetry?: Telemetry, metrics?: SessionMetrics): Promise<void>;
@@ -37,7 +38,7 @@ export declare function reportProxyExited(reason: ProxyExitReason, telemetry?: T
37
38
  /** Everything the proxy sends on its way out. Awaited, then the caller exits. */
38
39
  export declare function flushProxyExit(reason: ProxyExitReason): Promise<void>;
39
40
  /** Send `daemon_died_unclean`. Fire-and-forget is fine for callers: the process lives on. */
40
- export declare function reportDaemonDiedUnclean(ageMs: number, telemetry?: Telemetry): Promise<void>;
41
+ export declare function reportDaemonDiedUnclean(ageMs: number, cause?: Omit<DaemonDeath, 'ageMs'>, telemetry?: Telemetry): Promise<void>;
41
42
  /**
42
43
  * Send `wake_refused`, once per process. A dormant proxy refuses again on every client request, and
43
44
  * one event per request would count the client's retries, not the refusal.
@@ -73,9 +73,9 @@ export async function flushProxyExit(reason) {
73
73
  await Promise.allSettled([flushProxySessionMetrics(), reportProxyExited(reason)]);
74
74
  }
75
75
  /** Send `daemon_died_unclean`. Fire-and-forget is fine for callers: the process lives on. */
76
- export async function reportDaemonDiedUnclean(ageMs, telemetry = getTelemetry()) {
76
+ export async function reportDaemonDiedUnclean(ageMs, cause, telemetry = getTelemetry()) {
77
77
  await telemetry.emit(TelemetryEventKind.DAEMON_DIED_UNCLEAN, {
78
- daemonDeath: { ageMs: Math.max(0, Math.round(ageMs)) },
78
+ daemonDeath: { ...cause, ageMs: Math.max(0, Math.round(ageMs)) },
79
79
  });
80
80
  }
81
81
  /**
@@ -15,7 +15,7 @@
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, type AgentNudgeKind, type ConnectFailure, type SessionSummary } from '@reticlehq/core/telemetry';
18
+ import { BrowserLaunchKind, ConnectFailure, type AgentNudgeKind, type SessionSummary } from '@reticlehq/core/telemetry';
19
19
  import type { HudUseData } from '@reticlehq/core';
20
20
  import { type HudSignals } from './hud-metrics.js';
21
21
  /**
@@ -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
  /**