@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.
- package/LICENSE +201 -104
- package/NOTICE +5 -0
- package/README.md +1 -1
- package/bin/launch.js +13 -0
- package/bin/package.json +3 -0
- package/bin/reticle.js +66 -8
- package/dist/chromium-launch-options.d.ts +25 -0
- package/dist/chromium-launch-options.js +21 -1
- package/dist/command/cli/adhoc-verdict.js +34 -7
- package/dist/command/cli/answers/gate-exit.d.ts +6 -2
- package/dist/command/cli/answers/gate-exit.js +12 -2
- package/dist/command/cli/ci-hint.d.ts +44 -0
- package/dist/command/cli/ci-hint.js +72 -0
- package/dist/command/cli/cli-doctor.d.ts +6 -1
- package/dist/command/cli/cli-doctor.js +45 -4
- package/dist/command/cli/cli-flow-commands.d.ts +7 -1
- package/dist/command/cli/cli-flow-commands.js +39 -17
- package/dist/command/cli/cli-kill.d.ts +21 -0
- package/dist/command/cli/cli-kill.js +46 -3
- package/dist/command/cli/cli-parse.d.ts +5 -2
- package/dist/command/cli/cli-parse.js +10 -5
- package/dist/command/cli/cli-usage.js +3 -2
- package/dist/command/cli/cli-verify.d.ts +6 -1
- package/dist/command/cli/cli-verify.js +32 -13
- package/dist/command/cli/doctor/browser/chromium-hint.d.ts +6 -1
- package/dist/command/cli/doctor/browser/chromium-hint.js +11 -5
- package/dist/command/cli/doctor/doctor-fix.d.ts +44 -0
- package/dist/command/cli/doctor/doctor-fix.js +89 -0
- package/dist/command/cli/doctor/doctor-launcher-line.d.ts +23 -0
- package/dist/command/cli/doctor/doctor-launcher-line.js +60 -0
- package/dist/command/cli/doctor/doctor-rows.d.ts +4 -0
- package/dist/command/cli/doctor/doctor-rows.js +4 -0
- package/dist/command/cli/doctor-repairs.d.ts +64 -0
- package/dist/command/cli/doctor-repairs.js +214 -0
- package/dist/command/cli/drive/drive-command.js +3 -7
- package/dist/command/cli/first-use-checks.d.ts +41 -0
- package/dist/command/cli/first-use-checks.js +127 -0
- package/dist/command/cli/first-verdict-node.d.ts +3 -0
- package/dist/command/cli/first-verdict-node.js +189 -0
- package/dist/command/cli/first-verdict.d.ts +126 -0
- package/dist/command/cli/first-verdict.js +267 -0
- package/dist/command/cli/launch/sdk-version-match.d.ts +3 -2
- package/dist/command/cli/launch/sdk-version-match.js +3 -2
- package/dist/command/cli/lifecycle/daemon-lifecycle.js +6 -5
- package/dist/command/cli/mcp-command.js +13 -4
- package/dist/command/cli/ports/port-holder.d.ts +2 -0
- package/dist/command/cli/ports/port-holder.js +11 -3
- package/dist/command/cli/setup-mcp-cli.d.ts +27 -1
- package/dist/command/cli/setup-mcp-cli.js +98 -23
- package/dist/command/cli/status/status-lines.d.ts +1 -0
- package/dist/command/cli/status/status-lines.js +50 -17
- package/dist/command/cli/status-fields.js +17 -3
- package/dist/command/cli/try-command.d.ts +7 -0
- package/dist/command/cli/try-command.js +11 -3
- package/dist/command/cli.js +70 -33
- package/dist/command/daemon/binding/daemon-status-probe.d.ts +3 -1
- package/dist/command/daemon/binding/daemon-status-probe.js +4 -2
- package/dist/command/daemon/daemon-argv.d.ts +9 -0
- package/dist/command/daemon/daemon-argv.js +11 -0
- package/dist/command/daemon/daemon.d.ts +10 -3
- package/dist/command/daemon/daemon.js +17 -6
- package/dist/command/daemon/lifetime/daemon-exit.d.ts +24 -0
- package/dist/command/daemon/lifetime/daemon-exit.js +39 -0
- package/dist/command/daemon/lifetime/death-cause.d.ts +15 -0
- package/dist/command/daemon/lifetime/death-cause.js +130 -0
- package/dist/command/daemon/lifetime/heartbeat.d.ts +7 -15
- package/dist/command/daemon/lifetime/heartbeat.js +4 -13
- package/dist/command/daemon/port-state.d.ts +14 -0
- package/dist/command/daemon/port-state.js +31 -0
- package/dist/command/setup/agent-configs.d.ts +2 -0
- package/dist/command/setup/agent-configs.js +42 -15
- package/dist/command/setup/agent-next-steps.d.ts +77 -0
- package/dist/command/setup/agent-next-steps.js +284 -0
- package/dist/command/setup/agent-writer.js +11 -6
- package/dist/command/setup/init/init-host.js +2 -0
- package/dist/command/setup/init/init-runtime.d.ts +6 -1
- package/dist/command/setup/init/init-runtime.js +57 -21
- package/dist/command/setup/mcp-launcher.d.ts +3 -0
- package/dist/command/setup/mcp-launcher.js +11 -0
- package/dist/command/setup/setup-command.js +30 -12
- package/dist/command/setup/setup-mcp.d.ts +8 -0
- package/dist/command/setup/setup-mcp.js +47 -20
- package/dist/command/setup/start-dev-server.d.ts +8 -0
- package/dist/command/setup/start-dev-server.js +43 -0
- package/dist/command/version/identity/server-version.d.ts +2 -0
- package/dist/command/version/identity/server-version.js +2 -0
- package/dist/command/version/version-skew.d.ts +7 -0
- package/dist/command/version/version-skew.js +13 -2
- package/dist/features/license/license.d.ts +11 -71
- package/dist/features/license/license.js +43 -136
- package/dist/features/visual/desktop-capture.d.ts +33 -0
- package/dist/features/visual/desktop-capture.js +94 -0
- package/dist/features/visual/step-shots.js +139 -35
- package/dist/features/visual/visual-tools.d.ts +0 -18
- package/dist/features/visual/visual-tools.js +2 -81
- package/dist/import-graph.js +5 -6
- package/dist/index.d.ts +3 -1
- package/dist/index.js +14 -5
- package/dist/judgement/runs/run-ledger.d.ts +7 -5
- package/dist/judgement/runs/run-ledger.js +98 -69
- package/dist/judgement/runs/verification-sync.d.ts +6 -3
- package/dist/judgement/runs/verification-sync.js +21 -10
- package/dist/language/flows/flow-store-for-session.js +7 -5
- package/dist/language/flows/recording/tape/recordings.d.ts +2 -13
- package/dist/language/flows/recording/tape/recordings.js +41 -44
- package/dist/language/flows/replay.d.ts +17 -7
- package/dist/language/flows/replay.js +95 -44
- package/dist/launch-chromium.d.ts +1 -0
- package/dist/launch-chromium.js +49 -12
- package/dist/memory/cloud/sync-screenshots.js +6 -1
- package/dist/portal/bridge/bridge.d.ts +13 -4
- package/dist/portal/bridge/bridge.js +10 -9
- package/dist/portal/input/drive-error.d.ts +6 -0
- package/dist/portal/input/drive-error.js +9 -0
- package/dist/portal/input/network-mock.js +3 -2
- package/dist/portal/input/real-input-press.d.ts +34 -0
- package/dist/portal/input/real-input-press.js +150 -0
- package/dist/portal/input/real-input.d.ts +21 -23
- package/dist/portal/input/real-input.js +153 -194
- package/dist/portal/input/viewport-tools.js +6 -3
- package/dist/portal/pool/browser-pool.d.ts +5 -1
- package/dist/portal/pool/browser-pool.js +32 -38
- package/dist/portal/pool/context-permissions.d.ts +20 -17
- package/dist/portal/pool/context-permissions.js +63 -1
- package/dist/portal/pool/csp-widen.d.ts +62 -0
- package/dist/portal/pool/csp-widen.js +132 -0
- package/dist/portal/pool/lease-page-error.d.ts +29 -0
- package/dist/portal/pool/lease-page-error.js +18 -0
- package/dist/portal/pool/page-error-journal.d.ts +57 -0
- package/dist/portal/pool/page-error-journal.js +181 -0
- package/dist/portal/pool/playwright-launcher.js +21 -8
- package/dist/portal/pool/pool-contract.d.ts +22 -0
- package/dist/portal/pool/pool-lease.d.ts +35 -2
- package/dist/portal/pool/pool-lease.js +23 -0
- package/dist/portal/pool/zero-install.js +69 -1
- package/dist/portal/session/dev-server/dev-command.d.ts +8 -0
- package/dist/portal/session/dev-server/dev-command.js +56 -8
- package/dist/portal/session/no-session-diagnosis.js +10 -3
- package/dist/portal/session/no-session-next-action.d.ts +12 -0
- package/dist/portal/session/no-session-next-action.js +44 -4
- package/dist/portal/session/no-session-watch.js +3 -1
- package/dist/portal/session/page-commands/undeclared-command.d.ts +9 -3
- package/dist/portal/session/page-commands/undeclared-command.js +16 -7
- package/dist/portal/session/recorded-gaps.d.ts +1 -1
- package/dist/portal/session/recorded-gaps.js +5 -2
- package/dist/portal/session/session-manager.js +40 -21
- package/dist/portal/session/session.js +4 -3
- package/dist/portal/session/zero-install-first-call.d.ts +20 -1
- package/dist/portal/session/zero-install-first-call.js +34 -2
- package/dist/start-wiring.d.ts +29 -2
- package/dist/start-wiring.js +73 -9
- package/dist/surface/mcp/mcp-proxy.js +10 -2
- package/dist/surface/mcp/mcp.js +5 -3
- package/dist/surface/mcp/peer/agents-connected.d.ts +6 -0
- package/dist/surface/mcp/peer/agents-connected.js +58 -0
- package/dist/surface/mcp/proxy/proxy-daemon-probe.js +17 -5
- package/dist/surface/tools/act/act-preflight.js +37 -17
- package/dist/surface/tools/act/act-target.js +3 -1
- package/dist/surface/tools/act/after-match.d.ts +7 -0
- package/dist/surface/tools/act/after-match.js +33 -0
- package/dist/surface/tools/act/resolve-target.d.ts +2 -0
- package/dist/surface/tools/act/resolve-target.js +35 -8
- package/dist/surface/tools/act/sequence-grade.d.ts +3 -1
- package/dist/surface/tools/act/sequence-grade.js +8 -1
- package/dist/surface/tools/act-sequence-tool.js +3 -0
- package/dist/surface/tools/act-tools.js +12 -12
- package/dist/surface/tools/assert/assert-verdict.js +28 -1
- package/dist/surface/tools/browser-tools.js +75 -7
- package/dist/surface/tools/dynamic-tools.js +2 -1
- package/dist/surface/tools/empty-session-list.d.ts +2 -0
- package/dist/surface/tools/empty-session-list.js +4 -2
- package/dist/surface/tools/error-recovery.js +43 -22
- package/dist/surface/tools/invoke-tool.js +27 -20
- package/dist/surface/tools/lease-hint.d.ts +7 -0
- package/dist/surface/tools/lease-hint.js +10 -3
- package/dist/surface/tools/lease-readiness.d.ts +21 -0
- package/dist/surface/tools/lease-readiness.js +47 -0
- package/dist/surface/tools/lease-seed.d.ts +2 -0
- package/dist/surface/tools/lease-seed.js +2 -1
- package/dist/surface/tools/lease-tools.js +43 -15
- package/dist/surface/tools/real-input-attempt.js +70 -31
- package/dist/surface/tools/repeat-hint.d.ts +40 -0
- package/dist/surface/tools/repeat-hint.js +171 -0
- package/dist/surface/tools/run-report.d.ts +2 -2
- package/dist/surface/tools/run-report.js +27 -15
- package/dist/surface/tools/tool-kit.d.ts +12 -0
- package/dist/surface/tools/tool-kit.js +7 -0
- package/dist/surface/tools/tools.js +26 -13
- package/dist/telemetry/argument-shape.js +16 -7
- package/dist/telemetry/cli-telemetry.d.ts +10 -1
- package/dist/telemetry/cli-telemetry.js +15 -0
- package/dist/telemetry/init-telemetry.d.ts +2 -1
- package/dist/telemetry/init-telemetry.js +47 -2
- package/dist/telemetry/install-trace.js +23 -4
- package/dist/telemetry/onboarding-funnel.d.ts +19 -2
- package/dist/telemetry/onboarding-funnel.js +25 -3
- package/dist/telemetry/proxy-telemetry.d.ts +2 -1
- package/dist/telemetry/proxy-telemetry.js +2 -2
- package/dist/telemetry/session-metrics.d.ts +1 -1
- package/dist/telemetry/session-metrics.js +67 -27
- package/dist/telemetry/telemetry.d.ts +3 -1
- package/dist/telemetry/telemetry.js +60 -28
- package/dist/telemetry/tool-refused.d.ts +2 -2
- package/dist/telemetry/tool-refused.js +9 -2
- package/docs/architecture.md +3 -6
- package/docs/cli/doctor.mdx +41 -2
- package/docs/cli/help.mdx +4 -2
- package/docs/cli/license.mdx +4 -5
- package/docs/cli/setup.mdx +13 -2
- package/docs/cli/status.mdx +4 -0
- package/docs/cli.mdx +1 -1
- package/docs/deploy-checks.md +25 -0
- package/docs/desktop.mdx +1 -1
- package/docs/enterprise.md +15 -20
- package/docs/environment.mdx +1 -0
- package/docs/first-drive.md +1 -1
- package/docs/install-manual.mdx +16 -1
- package/docs/packages/engine.mdx +1 -1
- package/docs/packages/react.mdx +14 -2
- package/docs/packages/server.mdx +1 -1
- package/docs/packages/tauri.mdx +1 -1
- package/docs/packages/test.mdx +1 -1
- package/docs/packages/vite-plugin.mdx +1 -0
- package/docs/packages.mdx +1 -1
- package/docs/platform-integration.md +1 -1
- package/docs/skill.md +1 -1
- package/docs/telemetry.md +4 -2
- package/package.json +14 -10
- package/LICENSE-ENTERPRISE +0 -37
- package/dist/features/ee/audit-log.d.ts +0 -14
- 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
|
-
|
|
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;
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
666
|
+
}
|
|
667
|
+
if (this.#clientLeft) {
|
|
631
668
|
return 'client_left';
|
|
632
|
-
|
|
669
|
+
}
|
|
670
|
+
if (this.#unsettledActions > 0) {
|
|
633
671
|
return 'abandoned';
|
|
634
|
-
|
|
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
|
-
|
|
149
|
-
|
|
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
|
|
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;
|
|
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;
|
|
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
|
|
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 (
|
|
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
|
|
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
|
-
/**
|
|
58
|
-
|
|
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
|
package/docs/architecture.md
CHANGED
|
@@ -115,14 +115,11 @@ This is plain, reviewable, version-controllable data, not a black box.
|
|
|
115
115
|
|
|
116
116
|
---
|
|
117
117
|
|
|
118
|
-
##
|
|
118
|
+
## Licensing
|
|
119
119
|
|
|
120
|
-
-
|
|
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.
|
|
122
|
+
See [LICENSE](https://github.com/reticlehq/reticle/blob/main/LICENSE) and each package's own `LICENSE` file.
|
|
126
123
|
|
|
127
124
|
---
|
|
128
125
|
|
package/docs/cli/doctor.mdx
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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.
|
|
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>
|