@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
@@ -14,8 +14,8 @@ export declare function renderRunReport(report: RunReport): string;
14
14
  export declare function endsRun(tool: string, args: Record<string, unknown>): boolean;
15
15
  /** What an answer says the run saved or read. An answer that refused counts for nothing. */
16
16
  export declare function noteToolResult(ledger: RunLedger, tool: string, args: Record<string, unknown>, result: unknown): void;
17
- /** The report as an answer field, or no field when the ledger holds no run. Empties the ledger. */
18
- export declare function runReportField(ledger: RunLedger, root: string | undefined, env: Record<string, string | undefined>, now: number): Partial<Record<typeof RUN_REPORT_FIELD, {
17
+ /** The owner's report as an answer field, or no field when the owner has no run. Ends that run. */
18
+ export declare function runReportField(ledger: RunLedger, root: string | undefined, env: Record<string, string | undefined>, now: number, owner?: string): Partial<Record<typeof RUN_REPORT_FIELD, {
19
19
  report: RunReport;
20
20
  markdown: string;
21
21
  instruction: string;
@@ -34,12 +34,15 @@ function headline(report) {
34
34
  const count = (v) => all.filter((each) => v === each).length;
35
35
  const total = all.length;
36
36
  const word = `${HEAD}${WORD[report.verified]}**${SEP}`;
37
- if (0 === total)
37
+ if (0 === total) {
38
38
  return `${word}${Said.NOTHING_CHECKED}`;
39
- if (Verified.NO === report.verified)
39
+ }
40
+ if (Verified.NO === report.verified) {
40
41
  return `${word}${String(count(Verified.NO))} of ${plural(total, 'check')} did not hold`;
41
- if (Verified.YES === report.verified)
42
+ }
43
+ if (Verified.YES === report.verified) {
42
44
  return `${word}${String(total)} of ${plural(total, 'check')} held`;
45
+ }
43
46
  return `${word}${String(total - count(Verified.YES) - count(Verified.NO))} of ${plural(total, 'check')} undecided`;
44
47
  }
45
48
  /** The report as markdown: the verdict, each claim with its evidence, then what the run left. */
@@ -95,39 +98,48 @@ const INTENT_READS = new Set(['list', 'index', 'subject', 'get']);
95
98
  const isRecord = (value) => null !== value && 'object' === typeof value && !Array.isArray(value);
96
99
  /** What an answer says the run saved or read. An answer that refused counts for nothing. */
97
100
  export function noteToolResult(ledger, tool, args, result) {
98
- if (!isRecord(result) || result['error'] !== undefined)
101
+ if (!isRecord(result) || result['error'] !== undefined) {
99
102
  return;
103
+ }
100
104
  if (ReticleTool.FLOW_SAVE === tool || ReticleTool.FLOW_SAVE_RECORDED === tool) {
101
105
  const name = result['name'];
102
- if ('string' === typeof name)
106
+ if ('string' === typeof name) {
103
107
  ledger.flowSaved(name);
108
+ }
104
109
  return;
105
110
  }
106
111
  const action = asString(args['action']);
107
112
  if (ReticleTool.INTENT === tool) {
108
113
  // The intent ledger in `.reticle/intent/` is the memory an unlinked project has.
109
- if (IntentWrite.RECORD === action)
114
+ if (IntentWrite.RECORD === action) {
110
115
  ledger.memoryWritten();
116
+ }
111
117
  const declared = args['intents'];
112
- if (IntentWrite.DECLARE === action && Array.isArray(declared))
118
+ if (IntentWrite.DECLARE === action && Array.isArray(declared)) {
113
119
  ledger.memoryWritten(declared.length);
114
- if (action !== undefined && INTENT_READS.has(action))
120
+ }
121
+ if (action !== undefined && INTENT_READS.has(action)) {
115
122
  ledger.memoryRead([JSON.stringify(result)]);
123
+ }
116
124
  return;
117
125
  }
118
- if (ReticleTool.MEMORY !== tool)
126
+ if (ReticleTool.MEMORY !== tool) {
119
127
  return;
120
- if ('add' === action && result['added'] !== undefined)
128
+ }
129
+ if ('add' === action && result['added'] !== undefined) {
121
130
  ledger.memoryWritten();
131
+ }
122
132
  const known = result['known'];
123
- if (Array.isArray(known))
133
+ if (Array.isArray(known)) {
124
134
  ledger.memoryRead(known.map((entry) => JSON.stringify(entry)));
135
+ }
125
136
  }
126
- /** The report as an answer field, or no field when the ledger holds no run. Empties the ledger. */
127
- export function runReportField(ledger, root, env, now) {
128
- if (ledger.isEmpty())
137
+ /** The owner's report as an answer field, or no field when the owner has no run. Ends that run. */
138
+ export function runReportField(ledger, root, env, now, owner) {
139
+ if (ledger.isEmpty(owner)) {
129
140
  return {};
130
- const taken = ledger.take(now);
141
+ }
142
+ const taken = ledger.take(now, owner);
131
143
  const silenced = env[UPGRADE_HINT_SILENCE_ENV] !== undefined;
132
144
  const linked = root === undefined || linkedToPlatform(root, env);
133
145
  const report = silenced || linked ? taken : { ...taken, cloud: CLOUD_VALUE_LINE };
@@ -96,6 +96,11 @@ export interface ToolDeps<Ext = unknown> {
96
96
  } | undefined>;
97
97
  /** The one dev server running in the caller's project folder, for a lease that names no url. */
98
98
  projectAppUrl?: () => Promise<string | undefined>;
99
+ /**
100
+ * The first-session offer for this attach id's first call, when no app is attached and the
101
+ * project is not wired (see zero-install-first-call.ts). Absent: no offer.
102
+ */
103
+ firstSessionOffer?: (attachId: string) => string | undefined;
99
104
  /** cross-run outcome memory (.reticle/project.json). */
100
105
  project: ProjectStore;
101
106
  /** optional native-input provider. undefined ⇒ everything stays synthetic. */
@@ -303,10 +308,17 @@ export declare const EnvelopeKey: {
303
308
  * what it cannot see, and the command that wires the app for good. See zero-install-first-call.ts.
304
309
  */
305
310
  readonly ZERO_INSTALL: "zero_install";
311
+ /**
312
+ * Once per agent connection, on its first call: this unwired web app can be checked now with the
313
+ * lease, and `init` needs the user's consent. See zero-install-first-call.ts.
314
+ */
315
+ readonly FIRST_SESSION_OFFER: "first_session_offer";
306
316
  /** Once-per-session advisory lines an agent's own call carries. See agent/notices.ts. */
307
317
  readonly NOTICES: "notices";
308
318
  /** The end-of-run report, on the call that ends an agent's run. See run-report.ts. */
309
319
  readonly RUN_REPORT: "runReport";
320
+ /** On the third identical call with the same result: what to try instead. See repeat-hint.ts. */
321
+ readonly REPEAT_HINT: "repeatHint";
310
322
  };
311
323
  export type EnvelopeKey = (typeof EnvelopeKey)[keyof typeof EnvelopeKey];
312
324
  /**
@@ -99,10 +99,17 @@ export const EnvelopeKey = {
99
99
  * what it cannot see, and the command that wires the app for good. See zero-install-first-call.ts.
100
100
  */
101
101
  ZERO_INSTALL: 'zero_install',
102
+ /**
103
+ * Once per agent connection, on its first call: this unwired web app can be checked now with the
104
+ * lease, and `init` needs the user's consent. See zero-install-first-call.ts.
105
+ */
106
+ FIRST_SESSION_OFFER: 'first_session_offer',
102
107
  /** Once-per-session advisory lines an agent's own call carries. See agent/notices.ts. */
103
108
  NOTICES: 'notices',
104
109
  /** The end-of-run report, on the call that ends an agent's run. See run-report.ts. */
105
110
  RUN_REPORT: RUN_REPORT_FIELD,
111
+ /** On the third identical call with the same result: what to try instead. See repeat-hint.ts. */
112
+ REPEAT_HINT: 'repeatHint',
106
113
  };
107
114
  /**
108
115
  * The sentence every session-bound result carries while its leased tab runs on a stubbed shell.
@@ -1,7 +1,7 @@
1
1
  import { TESTID_FOUND_UNDER_SCHEMA } from './query-hint-schema.js';
2
2
  import { z } from 'zod';
3
- import { EMPTY_SESSION_LIST_OUTPUT, emptySessionList, nextStepBesideLiveSessions, } from './empty-session-list.js';
4
- import { AttrNamesSchema, QueryBy, ReticleCommand, SnapshotMode } from '@reticlehq/core';
3
+ import { EMPTY_SESSION_LIST_OUTPUT, emptySessionList, NEXT_STEP_FIELD, nextStepBesideLiveSessions, } from './empty-session-list.js';
4
+ import { AttrNamesSchema, NearestTargetSchema, QueryBy, ReticleCommand, SnapshotMode, } from '@reticlehq/core';
5
5
  import { ReticleTool } from '@reticlehq/core';
6
6
  import { withSizeCost } from '../../portal/session/output-budget.js';
7
7
  import { applySnapshotDelta, SnapshotCache } from './read/snapshot-delta.js';
@@ -145,11 +145,12 @@ export const RAW_TOOLS = withCompactEnvelope([
145
145
  })));
146
146
  const sessions = await list();
147
147
  // An empty list is a dead end on its own; see empty-session-list.ts for what it carries.
148
- if (0 === sessions.length)
148
+ if (0 === sessions.length) {
149
149
  return emptySessionList(deps, sessions);
150
+ }
150
151
  // Another project's tab is up. This caller's project may still never have been wired.
151
152
  const nextStep = nextStepBesideLiveSessions(deps, sessions);
152
- return nextStep === undefined ? { sessions } : { sessions, next_step: nextStep };
153
+ return nextStep === undefined ? { sessions } : { sessions, [NEXT_STEP_FIELD]: nextStep };
153
154
  },
154
155
  },
155
156
  {
@@ -284,7 +285,7 @@ export const RAW_TOOLS = withCompactEnvelope([
284
285
  name: z
285
286
  .string()
286
287
  .optional()
287
- .describe('Accessible name filter — narrows results when `by` is role and the page has many elements of that role.'),
288
+ .describe('Exact accessible-name filter - narrows results when `by` is role and the page has many elements of that role. Matches the whole name; use `text` for a partial match.'),
288
289
  scope: z
289
290
  .string()
290
291
  .optional()
@@ -398,6 +399,10 @@ export const RAW_TOOLS = withCompactEnvelope([
398
399
  .optional()
399
400
  .describe('Present when a ROLE+NAME search missed and that role DOES carry a nearly-matching name — role+name is exact, so "Mesh" does not find "2 Mesh". Retry with one of these spellings; no snapshot needed.'),
400
401
  testidFoundUnder: TESTID_FOUND_UNDER_SCHEMA,
402
+ nearest: z
403
+ .array(NearestTargetSchema)
404
+ .optional()
405
+ .describe('Present when a search missed and the page has visible targets like the one asked for: up to 3, the asked role first, then by name similarity. Each has a ref to retry with. nameTruncated is true when the name was cut. Advice only, never a match.'),
401
406
  })
402
407
  .optional()
403
408
  .describe('Present only on zero matches — tells you what IS on the page so you can diagnose the miss.'),
@@ -422,8 +427,9 @@ export const RAW_TOOLS = withCompactEnvelope([
422
427
  // Accept the predicate's named-field spelling too — see query-shape.ts.
423
428
  const args = normalizeQueryArgs(rawArgs);
424
429
  const empty = emptyQueryRefusal(args);
425
- if (empty !== undefined)
430
+ if (empty !== undefined) {
426
431
  return Promise.reject(empty);
432
+ }
427
433
  const session = deps.sessions.resolve(asString(args['sessionId']));
428
434
  return commandOrThrow(deps, asString(args['sessionId']), ReticleCommand.QUERY, {
429
435
  by: args['by'],
@@ -603,14 +609,17 @@ export const RETIRED_FROM_SURFACE = [
603
609
  * The browser counts what leanness passed over, so the tool can say which of the two it is.
604
610
  */
605
611
  function noteEmptyLeanTree(result, mode) {
606
- if (SnapshotMode.INTERACTIVE !== mode)
612
+ if (SnapshotMode.INTERACTIVE !== mode) {
607
613
  return result;
614
+ }
608
615
  const row = asRecord(result);
609
- if (0 !== asNumber(row['nodes']))
616
+ if (0 !== asNumber(row['nodes'])) {
610
617
  return result;
618
+ }
611
619
  const skipped = asNumber(row['leanSkipped']) ?? 0;
612
- if (0 === skipped)
620
+ if (0 === skipped) {
613
621
  return result;
622
+ }
614
623
  return {
615
624
  ...row,
616
625
  note: `no element on this page carries an interactive ARIA role, so this mode found nothing — ` +
@@ -635,18 +644,22 @@ function noteEmptyLeanTree(result, mode) {
635
644
  * empty tree it replaces.
636
645
  */
637
646
  function noteHiddenPage(result, mode) {
638
- if (SnapshotMode.STATUS === mode)
647
+ if (SnapshotMode.STATUS === mode) {
639
648
  return result;
649
+ }
640
650
  const row = asRecord(result);
641
- if (0 !== asNumber(row['nodes']))
651
+ if (0 !== asNumber(row['nodes'])) {
642
652
  return result;
653
+ }
643
654
  // A note already there is the more specific one (leanness), and two explanations for one empty
644
655
  // tree is worse than the better of them alone.
645
- if (row['note'] !== undefined)
656
+ if (row['note'] !== undefined) {
646
657
  return result;
658
+ }
647
659
  const skipped = asNumber(row['hiddenSkipped']) ?? 0;
648
- if (0 === skipped)
660
+ if (0 === skipped) {
649
661
  return result;
662
+ }
650
663
  return {
651
664
  ...row,
652
665
  note: `this is NOT an empty page: ${String(skipped)} subtree(s) were skipped because their root ` +
@@ -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
  /**