@reticlehq/server 2.4.0 → 2.5.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/SKILL.md +702 -0
- package/dist/bridge/auth-failure-reason.d.ts +19 -0
- package/dist/bridge/auth-failure-reason.js +29 -0
- package/dist/{bridge-security.d.ts → bridge/bridge-security.d.ts} +1 -1
- package/dist/{bridge-security.js → bridge/bridge-security.js} +1 -1
- package/dist/{bridge.d.ts → bridge/bridge.d.ts} +1 -1
- package/dist/{bridge.js → bridge/bridge.js} +63 -13
- package/dist/{bridge.test-harness.d.ts → bridge/bridge.test-harness.d.ts} +1 -11
- package/dist/{bridge.test-harness.js → bridge/bridge.test-harness.js} +9 -9
- package/dist/{pairing-token.js → bridge/pairing-token.js} +1 -1
- package/dist/capsule/capsule-store.d.ts +8 -3
- package/dist/capsule/capsule-store.js +7 -2
- package/dist/capsule/capsule.js +3 -3
- package/dist/capsule/causal-summary.d.ts +8 -0
- package/dist/capsule/causal-summary.js +22 -12
- package/dist/capsule/divergence.d.ts +9 -4
- package/dist/capsule/divergence.js +8 -8
- package/dist/capsule/predicate-to-links.js +11 -8
- package/dist/{cli-doctor.js → cli/cli-doctor.js} +9 -2
- package/dist/{cli-flow-commands.d.ts → cli/cli-flow-commands.d.ts} +2 -2
- package/dist/{cli-flow-commands.js → cli/cli-flow-commands.js} +15 -15
- package/dist/{cli-launch.d.ts → cli/cli-launch.d.ts} +32 -3
- package/dist/cli/cli-launch.js +171 -0
- package/dist/{cli-parse.d.ts → cli/cli-parse.d.ts} +2 -11
- package/dist/{cli-parse.js → cli/cli-parse.js} +113 -60
- package/dist/{cli-port.d.ts → cli/cli-port.d.ts} +12 -0
- package/dist/{cli-port.js → cli/cli-port.js} +9 -9
- package/dist/cli/cli-update-commands.d.ts +4 -0
- package/dist/cli/cli-update-commands.js +93 -0
- package/dist/{cli-verify.js → cli/cli-verify.js} +14 -14
- package/dist/{cloud-cli.js → cli/cloud-cli.js} +29 -24
- package/dist/cli.d.ts +2 -2
- package/dist/cli.js +174 -92
- package/dist/cloud/cloud-config.d.ts +0 -1
- package/dist/cloud/cloud-config.js +9 -9
- package/dist/cloud/cloud-sync.d.ts +1 -7
- package/dist/cloud/cloud-sync.js +12 -12
- package/dist/crawl/crawl-empty.d.ts +16 -0
- package/dist/crawl/crawl-empty.js +24 -0
- package/dist/crawl/crawl-tools.js +4 -1
- package/dist/crawl/crawl.js +26 -7
- package/dist/daemon/daemon-resilience.d.ts +49 -0
- package/dist/daemon/daemon-resilience.js +191 -0
- package/dist/daemon/daemon-usefulness.d.ts +61 -0
- package/dist/daemon/daemon-usefulness.js +62 -0
- package/dist/{daemon.d.ts → daemon/daemon.d.ts} +33 -8
- package/dist/{daemon.js → daemon/daemon.js} +80 -48
- package/dist/daemon/idle-grace.d.ts +32 -0
- package/dist/daemon/idle-grace.js +41 -0
- package/dist/{idle-shutdown.d.ts → daemon/idle-shutdown.d.ts} +13 -0
- package/dist/{idle-shutdown.js → daemon/idle-shutdown.js} +27 -4
- package/dist/domain/domain-model.js +2 -2
- package/dist/events/body-failures.js +8 -8
- package/dist/events/contradictions.d.ts +1 -1
- package/dist/events/contradictions.js +60 -10
- package/dist/events/echo-mismatch.js +10 -10
- package/dist/events/event-filters.js +6 -6
- package/dist/events/json-salvage.d.ts +2 -1
- package/dist/events/json-salvage.js +6 -6
- package/dist/events/observed-in-window.d.ts +12 -0
- package/dist/events/observed-in-window.js +21 -0
- package/dist/events/predicate-eval.d.ts +43 -20
- package/dist/events/predicate-eval.js +159 -55
- package/dist/events/predicate.d.ts +2 -2
- package/dist/events/predicate.js +112 -35
- package/dist/events/reconcile.js +5 -5
- package/dist/events/ring-buffer.js +1 -1
- package/dist/events/stale-response.js +6 -6
- package/dist/events/testid-near-miss.d.ts +22 -0
- package/dist/events/testid-near-miss.js +33 -0
- package/dist/events/unit-mismatch.js +8 -8
- package/dist/flows/affected-tools.js +19 -4
- package/dist/flows/affected.js +1 -1
- package/dist/flows/annotate-target.d.ts +1 -0
- package/dist/flows/annotate-target.js +20 -0
- package/dist/flows/annotate-tools.js +25 -3
- package/dist/flows/annotate.js +17 -4
- package/dist/flows/assertion-integrity.d.ts +2 -1
- package/dist/flows/assertion-tiers-store.d.ts +3 -66
- package/dist/flows/assertion-tiers-store.js +2 -2
- package/dist/flows/attributed-failure.d.ts +17 -0
- package/dist/flows/attributed-failure.js +20 -0
- package/dist/flows/buddy-status.d.ts +2 -1
- package/dist/flows/buddy-status.js +2 -2
- package/dist/flows/coverage.d.ts +3 -2
- package/dist/flows/coverage.js +2 -2
- package/dist/flows/decision.js +24 -9
- package/dist/flows/empty-flow.d.ts +6 -0
- package/dist/flows/empty-flow.js +24 -0
- package/dist/flows/flake-store.d.ts +0 -1
- package/dist/flows/flake-store.js +1 -1
- package/dist/flows/flake.d.ts +2 -1
- package/dist/flows/flake.js +2 -2
- package/dist/flows/flow-classify.js +12 -0
- package/dist/flows/flow-replay-run.js +4 -4
- package/dist/flows/flow-replay.d.ts +5 -7
- package/dist/flows/flow-replay.js +114 -47
- package/dist/flows/flow-report.js +1 -1
- package/dist/flows/flow-scope.d.ts +1 -6
- package/dist/flows/flow-step-runners.d.ts +26 -6
- package/dist/flows/flow-step-runners.js +157 -6
- package/dist/flows/flow-success.js +20 -15
- package/dist/flows/flow-tools.js +53 -7
- package/dist/flows/flows.js +15 -11
- package/dist/flows/gate.d.ts +4 -3
- package/dist/flows/gate.js +1 -1
- package/dist/flows/heal-run.d.ts +0 -8
- package/dist/flows/heal-run.js +5 -5
- package/dist/flows/heal.js +3 -3
- package/dist/flows/predicate-to-expect.d.ts +38 -0
- package/dist/flows/predicate-to-expect.js +95 -0
- package/dist/flows/replay.d.ts +16 -1
- package/dist/flows/replay.js +58 -22
- package/dist/flows/role-anchor-nearest.d.ts +6 -0
- package/dist/flows/role-anchor-nearest.js +45 -0
- package/dist/flows/server-verify.d.ts +1 -1
- package/dist/flows/server-verify.js +5 -5
- package/dist/flows/verify-change-tools.js +32 -10
- package/dist/flows/verify-change-verdict.d.ts +15 -0
- package/dist/flows/verify-change-verdict.js +25 -0
- package/dist/flows/watch-batcher.d.ts +3 -2
- package/dist/honesty/already-true.d.ts +2 -0
- package/dist/honesty/already-true.js +31 -0
- package/dist/honesty/blind-spots.d.ts +16 -3
- package/dist/honesty/blind-spots.js +24 -12
- package/dist/honesty/honesty.d.ts +3 -2
- package/dist/honesty/honesty.js +4 -4
- package/dist/honesty/uncaptured-bodies.js +1 -1
- package/dist/honesty/unread-outcome.js +2 -2
- package/dist/honesty/verified.d.ts +22 -3
- package/dist/honesty/verified.js +48 -8
- package/dist/http-server.js +14 -7
- package/dist/hunt/hunt-report.d.ts +2 -1
- package/dist/hunt/hunt-report.js +2 -2
- package/dist/index.d.ts +8 -6
- package/dist/index.js +32 -25
- package/dist/init/agent-rules.d.ts +2 -1
- package/dist/init/agent-rules.js +45 -7
- package/dist/init/app-choice.d.ts +20 -0
- package/dist/init/app-choice.js +25 -0
- package/dist/init/astro-host.d.ts +21 -0
- package/dist/init/astro-host.js +27 -0
- package/dist/init/astro-patch.d.ts +16 -0
- package/dist/init/astro-patch.js +240 -0
- package/dist/init/capabilities.js +4 -1
- package/dist/init/cra.d.ts +37 -0
- package/dist/init/cra.js +90 -0
- package/dist/init/cursor.js +39 -4
- package/dist/init/desktop-doctor.d.ts +3 -2
- package/dist/init/desktop-doctor.js +4 -4
- package/dist/init/detect.d.ts +3 -2
- package/dist/init/detect.js +16 -5
- package/dist/init/mcp-registered.d.ts +12 -0
- package/dist/init/mcp-registered.js +14 -0
- package/dist/init/mcp.d.ts +27 -2
- package/dist/init/mcp.js +40 -4
- package/dist/init/next-patch.d.ts +2 -4
- package/dist/init/next-patch.js +109 -19
- package/dist/init/node-io.js +23 -6
- package/dist/init/plan-framework.d.ts +16 -1
- package/dist/init/plan-framework.js +132 -8
- package/dist/init/plan.d.ts +73 -8
- package/dist/init/plan.js +133 -34
- package/dist/init/project-id.d.ts +5 -9
- package/dist/init/project-id.js +10 -22
- package/dist/init/run.d.ts +5 -0
- package/dist/init/run.js +210 -36
- package/dist/init/slash-command.d.ts +9 -0
- package/dist/init/slash-command.js +10 -1
- package/dist/init/snippets.d.ts +7 -10
- package/dist/init/snippets.js +44 -15
- package/dist/init/vite-config.js +1 -1
- package/dist/init/workspace-apps.d.ts +23 -0
- package/dist/init/workspace-apps.js +68 -0
- package/dist/input/driven-redaction.js +1 -1
- package/dist/input/network-detail.js +8 -8
- package/dist/input/network-mock-tools.js +2 -2
- package/dist/input/network-mock.js +4 -4
- package/dist/input/real-input.js +8 -3
- package/dist/input/scroll-find.js +2 -2
- package/dist/input/scroll-tools.d.ts +0 -5
- package/dist/input/scroll-tools.js +11 -4
- package/dist/input/viewport-tools.js +2 -2
- package/dist/journal/ambient-store.d.ts +0 -2
- package/dist/journal/ambient-store.js +1 -1
- package/dist/journal/ambient.js +1 -1
- package/dist/journal/attach-journal.d.ts +2 -1
- package/dist/journal/deviation-report.js +6 -6
- package/dist/journal/deviation-service.js +1 -1
- package/dist/journal/envelope-store.d.ts +0 -2
- package/dist/journal/envelope-store.js +1 -1
- package/dist/journal/envelope.d.ts +2 -3
- package/dist/journal/envelope.js +3 -3
- package/dist/journal/journal-query.js +1 -1
- package/dist/journal/journal-recorder.d.ts +2 -1
- package/dist/journal/journal-recorder.js +2 -2
- package/dist/journal/rollups.js +9 -9
- package/dist/journal/session-end.d.ts +2 -1
- package/dist/journal/session-journal.js +4 -4
- package/dist/license/license.d.ts +0 -14
- package/dist/license/license.js +8 -8
- package/dist/log.d.ts +10 -0
- package/dist/log.js +11 -1
- package/dist/mcp/mcp-is-error.d.ts +13 -0
- package/dist/mcp/mcp-is-error.js +18 -0
- package/dist/mcp/mcp-outage.d.ts +31 -0
- package/dist/mcp/mcp-outage.js +48 -0
- package/dist/mcp/mcp-proxy.d.ts +124 -0
- package/dist/mcp/mcp-proxy.js +697 -0
- package/dist/{mcp.d.ts → mcp/mcp.d.ts} +13 -10
- package/dist/{mcp.js → mcp/mcp.js} +110 -35
- package/dist/mcp/proxy-handshake.d.ts +3 -0
- package/dist/mcp/proxy-handshake.js +54 -0
- package/dist/mcp/proxy-lifecycle.d.ts +64 -0
- package/dist/mcp/proxy-lifecycle.js +70 -0
- package/dist/mcp/tool-catalog-cache.d.ts +29 -0
- package/dist/mcp/tool-catalog-cache.js +55 -0
- package/dist/oracles/propose-consequences.d.ts +2 -1
- package/dist/oracles/propose-consequences.js +16 -11
- package/dist/oracles/self-instrument.d.ts +2 -1
- package/dist/oracles/self-instrument.js +1 -1
- package/dist/phenomena/phenomena.js +3 -3
- package/dist/platform.d.ts +16 -0
- package/dist/platform.js +16 -0
- package/dist/pool/playwright-launcher.d.ts +15 -2
- package/dist/pool/playwright-launcher.js +21 -2
- package/dist/project/fs-port.js +1 -1
- package/dist/project/project-tools.js +2 -2
- package/dist/runs/build-verification-run.js +1 -1
- package/dist/runs/repair-prompt.d.ts +0 -6
- package/dist/runs/repair-prompt.js +3 -3
- package/dist/runs/risk-classify.js +1 -1
- package/dist/runs/run-diff.d.ts +0 -2
- package/dist/runs/run-diff.js +3 -3
- package/dist/runs/run-tools.js +2 -2
- package/dist/runs/verification-sync.js +2 -2
- package/dist/runs/verify-http.js +1 -1
- package/dist/runs/verify-server.js +1 -1
- package/dist/session/command-timeout.d.ts +11 -3
- package/dist/session/command-timeout.js +2 -2
- package/dist/session/control-envelope.d.ts +6 -0
- package/dist/session/control-envelope.js +7 -1
- package/dist/session/dev-server-probe.d.ts +13 -0
- package/dist/session/dev-server-probe.js +52 -0
- package/dist/session/last-act.d.ts +17 -3
- package/dist/session/last-act.js +16 -4
- package/dist/session/live-control-tools.js +7 -2
- package/dist/session/live-control.js +1 -1
- package/dist/session/looks-like-dev-server.d.ts +14 -0
- package/dist/session/looks-like-dev-server.js +25 -0
- package/dist/session/no-session-diagnosis.d.ts +38 -0
- package/dist/session/no-session-diagnosis.js +70 -0
- package/dist/session/no-session-watch.d.ts +19 -0
- package/dist/session/no-session-watch.js +61 -0
- package/dist/session/observed-state.d.ts +4 -0
- package/dist/session/observed-state.js +19 -1
- package/dist/session/output-budget.js +1 -1
- package/dist/session/session-health.d.ts +8 -1
- package/dist/session/session-health.js +8 -7
- package/dist/session/session-info.d.ts +26 -7
- package/dist/session/session-info.js +42 -1
- package/dist/session/session-lease.d.ts +0 -11
- package/dist/session/session-lease.js +1 -1
- package/dist/session/session-manager.d.ts +3 -0
- package/dist/session/session-manager.js +52 -9
- package/dist/session/session-readiness.d.ts +1 -1
- package/dist/session/session-readiness.js +6 -1
- package/dist/session/session-reaper.js +1 -1
- package/dist/session/session-reconnect.d.ts +32 -0
- package/dist/session/session-reconnect.js +42 -0
- package/dist/session/session.d.ts +14 -2
- package/dist/session/session.js +69 -26
- package/dist/session/state-select.d.ts +1 -1
- package/dist/session/state-select.js +1 -1
- package/dist/status-payload.d.ts +19 -0
- package/dist/status-payload.js +11 -0
- package/dist/telemetry/argument-shape.js +10 -1
- package/dist/telemetry/browser-mode.d.ts +27 -0
- package/dist/telemetry/browser-mode.js +33 -0
- package/dist/telemetry/bug-found.d.ts +17 -21
- package/dist/telemetry/bug-found.js +120 -8
- package/dist/telemetry/cli-telemetry.d.ts +2 -12
- package/dist/telemetry/cli-telemetry.js +27 -4
- package/dist/telemetry/daemon-telemetry.d.ts +19 -0
- package/dist/telemetry/daemon-telemetry.js +12 -2
- package/dist/telemetry/dev-repo.js +4 -4
- package/dist/telemetry/error-fingerprint.js +21 -2
- package/dist/telemetry/feedback-cli.d.ts +1 -1
- package/dist/telemetry/feedback-cli.js +4 -4
- package/dist/telemetry/feedback-context.d.ts +22 -0
- package/dist/telemetry/feedback-context.js +41 -1
- package/dist/telemetry/feedback-delivery.d.ts +20 -0
- package/dist/telemetry/feedback-delivery.js +30 -0
- package/dist/telemetry/feedback-outbox.d.ts +22 -0
- package/dist/telemetry/feedback-outbox.js +87 -0
- package/dist/telemetry/feedback.d.ts +22 -1
- package/dist/telemetry/feedback.js +67 -8
- package/dist/telemetry/git-facts.js +1 -1
- package/dist/telemetry/session-metrics.d.ts +2 -0
- package/dist/telemetry/session-metrics.js +68 -6
- package/dist/telemetry/telemetry.d.ts +27 -2
- package/dist/telemetry/telemetry.js +119 -29
- package/dist/telemetry/verification-of.d.ts +22 -0
- package/dist/telemetry/verification-of.js +84 -0
- package/dist/tools/act-capsule.js +2 -2
- package/dist/tools/act-danger.d.ts +12 -0
- package/dist/tools/act-danger.js +20 -3
- package/dist/tools/act-helpers.js +4 -4
- package/dist/tools/act-tools.d.ts +0 -8
- package/dist/tools/act-tools.js +84 -115
- package/dist/tools/act-view.js +7 -7
- package/dist/tools/alias-args.d.ts +17 -0
- package/dist/tools/alias-args.js +25 -0
- package/dist/tools/assert-grade.js +9 -9
- package/dist/tools/assert-verdict.d.ts +3 -1
- package/dist/tools/assert-verdict.js +8 -5
- package/dist/tools/browser-tools.js +30 -11
- package/dist/tools/contract-tools.js +1 -1
- package/dist/tools/coverage-identity.d.ts +34 -0
- package/dist/tools/coverage-identity.js +46 -0
- package/dist/tools/coverage-tools.js +10 -14
- package/dist/tools/dynamic-tools.d.ts +2 -1
- package/dist/tools/dynamic-tools.js +90 -18
- package/dist/tools/error-recovery.d.ts +20 -2
- package/dist/tools/error-recovery.js +179 -4
- package/dist/tools/feedback-tools.d.ts +12 -1
- package/dist/tools/feedback-tools.js +28 -5
- package/dist/tools/invoke-tool.d.ts +0 -6
- package/dist/tools/invoke-tool.js +57 -32
- package/dist/tools/lease-hint.d.ts +17 -0
- package/dist/tools/lease-hint.js +24 -0
- package/dist/tools/lease-tools.js +6 -5
- package/dist/tools/merge-tools.d.ts +10 -1
- package/dist/tools/merge-tools.js +9 -3
- package/dist/tools/merged-name-redirect.d.ts +13 -0
- package/dist/tools/merged-name-redirect.js +55 -0
- package/dist/tools/navigate-result.d.ts +17 -0
- package/dist/tools/navigate-result.js +31 -0
- package/dist/tools/observe-tools.js +54 -20
- package/dist/tools/observed-nothing.d.ts +25 -0
- package/dist/tools/observed-nothing.js +33 -0
- package/dist/tools/query-paginate.js +2 -2
- package/dist/tools/query-shape.d.ts +18 -0
- package/dist/tools/query-shape.js +39 -0
- package/dist/tools/read-tools.js +81 -20
- package/dist/tools/real-input-attempt.d.ts +28 -0
- package/dist/tools/real-input-attempt.js +88 -0
- package/dist/tools/reconcile-tools.js +7 -7
- package/dist/tools/reload-result.d.ts +14 -0
- package/dist/tools/reload-result.js +29 -0
- package/dist/tools/snapshot-delta.d.ts +4 -5
- package/dist/tools/snapshot-delta.js +0 -0
- package/dist/tools/tool-kit.d.ts +8 -0
- package/dist/tools/tool-surface.d.ts +83 -0
- package/dist/tools/tool-surface.js +204 -0
- package/dist/tools/tools-helpers.js +6 -6
- package/dist/tools/tools.d.ts +15 -0
- package/dist/tools/tools.js +53 -17
- package/dist/tools/unadvertised-help.d.ts +5 -0
- package/dist/tools/unadvertised-help.js +39 -0
- package/dist/trace.d.ts +69 -0
- package/dist/trace.js +150 -0
- package/dist/update/nudge-credit.d.ts +4 -0
- package/dist/update/nudge-credit.js +53 -0
- package/dist/update/sdk-sync.d.ts +21 -0
- package/dist/update/sdk-sync.js +42 -0
- package/dist/update/update-checker.js +3 -3
- package/dist/update/update-nudge.d.ts +63 -6
- package/dist/update/update-nudge.js +128 -15
- package/dist/update/updater.d.ts +1 -9
- package/dist/update/updater.js +13 -8
- package/dist/version/peer-announce.d.ts +4 -0
- package/dist/version/peer-announce.js +30 -0
- package/dist/{server-version.js → version/server-version.js} +5 -1
- package/dist/version/version-nudge.d.ts +28 -0
- package/dist/version/version-nudge.js +40 -0
- package/dist/version/version-skew.d.ts +66 -0
- package/dist/version/version-skew.js +71 -0
- package/dist/visual/visual-diff.js +2 -2
- package/dist/visual/visual-tools.js +4 -4
- package/docs/agent-cheatsheet.md +85 -0
- package/docs/architecture.md +113 -0
- package/docs/benchmarks.md +255 -0
- package/docs/debugging.md +102 -0
- package/docs/deploy-checks.md +69 -0
- package/docs/desktop-apps.md +256 -0
- package/docs/enterprise.md +62 -0
- package/docs/fixtures.md +28 -0
- package/docs/flows.md +166 -0
- package/docs/getting-started.md +469 -0
- package/docs/human-control.md +63 -0
- package/docs/integration-patterns.md +171 -0
- package/docs/local-registry.md +126 -0
- package/docs/mint.json +61 -0
- package/docs/multi-agent-testing.md +55 -0
- package/docs/platform-integration.md +122 -0
- package/docs/telemetry-contract.md +214 -0
- package/docs/telemetry.md +184 -0
- package/docs/testing.md +82 -0
- package/docs/token-efficiency.md +80 -0
- package/docs/usage.md +1046 -0
- package/package.json +8 -5
- package/dist/cli-launch.js +0 -112
- package/dist/daemon-resilience.d.ts +0 -22
- package/dist/daemon-resilience.js +0 -85
- package/dist/mcp-proxy.d.ts +0 -67
- package/dist/mcp-proxy.js +0 -337
- package/dist/tools/profiles.d.ts +0 -37
- package/dist/tools/profiles.js +0 -139
- /package/dist/{pairing-token.d.ts → bridge/pairing-token.d.ts} +0 -0
- /package/dist/{token-auth.d.ts → bridge/token-auth.d.ts} +0 -0
- /package/dist/{token-auth.js → bridge/token-auth.js} +0 -0
- /package/dist/{cli-doctor.d.ts → cli/cli-doctor.d.ts} +0 -0
- /package/dist/{cli-verify.d.ts → cli/cli-verify.d.ts} +0 -0
- /package/dist/{cloud-cli.d.ts → cli/cloud-cli.d.ts} +0 -0
- /package/dist/{server-version.d.ts → version/server-version.d.ts} +0 -0
|
@@ -24,8 +24,11 @@
|
|
|
24
24
|
import { FeedbackSchema, FEEDBACK_TEXT_MAX, FEEDBACK_TRACE_MAX, FEEDBACK_FIELD_MAX, TelemetryEventKind, } from '@reticlehq/core';
|
|
25
25
|
import { platform } from 'node:os';
|
|
26
26
|
import { getTelemetry } from './telemetry.js';
|
|
27
|
-
import {
|
|
27
|
+
import { noteFeedbackUndelivered } from './feedback-delivery.js';
|
|
28
|
+
import { isReticleSourceCheckout } from './dev-repo.js';
|
|
29
|
+
import { SERVER_VERSION } from '../version/server-version.js';
|
|
28
30
|
import { feedbackContext } from './feedback-context.js';
|
|
31
|
+
import { markDelivered, outboxPath, queueFeedback } from './feedback-outbox.js';
|
|
29
32
|
/** Kills the feedback channel ONLY — adoption counters keep flowing. */
|
|
30
33
|
export const FEEDBACK_ENV = 'RETICLE_FEEDBACK';
|
|
31
34
|
/** What replaces a redacted match. Visible on purpose: a reader must see that something was removed. */
|
|
@@ -67,7 +70,7 @@ export function redactFeedbackText(input, max) {
|
|
|
67
70
|
removed.add(label);
|
|
68
71
|
// A rule with a leading capture keeps it (the scheme, the `api_key=` key) so the reader still
|
|
69
72
|
// sees WHAT was removed, not just that something was.
|
|
70
|
-
const prefix = typeof groups[0]
|
|
73
|
+
const prefix = 'string' === typeof groups[0] ? groups[0] : '';
|
|
71
74
|
return `${prefix}${REDACTED}`;
|
|
72
75
|
});
|
|
73
76
|
}
|
|
@@ -76,7 +79,7 @@ export function redactFeedbackText(input, max) {
|
|
|
76
79
|
/** True when the feedback channel is switched off on its own (the emitter's switches apply as well). */
|
|
77
80
|
export function feedbackDisabled(env = process.env) {
|
|
78
81
|
const value = (env[FEEDBACK_ENV] ?? '').toLowerCase();
|
|
79
|
-
return
|
|
82
|
+
return '0' === value || 'false' === value || 'off' === value || 'no' === value;
|
|
80
83
|
}
|
|
81
84
|
/**
|
|
82
85
|
* The fields an author writes, and therefore the fields that must be redacted. ONE list, used both to
|
|
@@ -118,7 +121,7 @@ export async function submitFeedback(input, opts = {}) {
|
|
|
118
121
|
// addition. Deriving the set means a sixth field is covered the moment it exists.
|
|
119
122
|
const redactions = new Map();
|
|
120
123
|
for (const [key, value] of Object.entries(input)) {
|
|
121
|
-
if (typeof value !== 'string' ||
|
|
124
|
+
if (typeof value !== 'string' || '' === value)
|
|
122
125
|
continue;
|
|
123
126
|
if (!AUTHOR_WRITTEN_FIELDS.has(key))
|
|
124
127
|
continue;
|
|
@@ -127,13 +130,25 @@ export async function submitFeedback(input, opts = {}) {
|
|
|
127
130
|
const body = redactions.get('text') ?? { text: '', removed: [] };
|
|
128
131
|
const redacted = [...new Set([...redactions.values()].flatMap((r) => r.removed))];
|
|
129
132
|
if (feedbackDisabled(env)) {
|
|
130
|
-
return {
|
|
133
|
+
return {
|
|
134
|
+
sent: false,
|
|
135
|
+
accepted: false,
|
|
136
|
+
reason: `feedback is disabled by ${FEEDBACK_ENV}`,
|
|
137
|
+
redacted,
|
|
138
|
+
context,
|
|
139
|
+
};
|
|
131
140
|
}
|
|
132
141
|
const telemetry = getTelemetry();
|
|
133
142
|
if (!telemetry.enabled) {
|
|
134
143
|
return {
|
|
135
144
|
sent: false,
|
|
136
|
-
|
|
145
|
+
accepted: false,
|
|
146
|
+
// Named precisely, because the commonest cause is not what the old wording suggested: a
|
|
147
|
+
// Reticle SOURCE CHECKOUT disables telemetry by cwd, and that is exactly where release runs and
|
|
148
|
+
// contributor sessions happen — so the reports most worth having were the ones silently lost.
|
|
149
|
+
reason: isReticleSourceCheckout(opts.cwd ?? process.cwd())
|
|
150
|
+
? 'this is a Reticle source checkout, where telemetry is disabled by design — so feedback has nowhere to go. Open an issue at https://github.com/reticlehq/reticle/issues instead, or run from the app you are verifying.'
|
|
151
|
+
: `telemetry is disabled on this machine, so feedback has nowhere to go. Re-enable with \`reticle telemetry enable\`, or open an issue at https://github.com/reticlehq/reticle/issues.`,
|
|
137
152
|
redacted,
|
|
138
153
|
context,
|
|
139
154
|
};
|
|
@@ -151,13 +166,57 @@ export async function submitFeedback(input, opts = {}) {
|
|
|
151
166
|
if (!parsed.success) {
|
|
152
167
|
return {
|
|
153
168
|
sent: false,
|
|
169
|
+
accepted: false,
|
|
154
170
|
reason: parsed.error.issues[0]?.message ?? 'invalid feedback',
|
|
155
171
|
redacted,
|
|
156
172
|
context,
|
|
157
173
|
};
|
|
158
174
|
}
|
|
159
|
-
|
|
160
|
-
|
|
175
|
+
// BACKGROUND: return now, deliver after. The agent calls this mid-task and a ~340ms POST is the
|
|
176
|
+
// product blocking the user's work to talk about itself. The honesty that the awaited version was
|
|
177
|
+
// written for is kept elsewhere: `sent` stays false (nothing is confirmed yet), `accepted` says
|
|
178
|
+
// the report is well-formed and queued, and a send that then fails reaches the reporter on its
|
|
179
|
+
// next tool result instead of vanishing.
|
|
180
|
+
// Written down BEFORE the network is touched, on both paths. A failed send is then a queued report
|
|
181
|
+
// rather than a lost one — see feedback-outbox. Reported from the field: a 1.3s hiccup destroyed a
|
|
182
|
+
// root-cause analysis that took an hour of driving to produce, and it survived only because the
|
|
183
|
+
// reporter happened to have written the markdown by hand first.
|
|
184
|
+
const queued = queueFeedback(parsed.data);
|
|
185
|
+
if (true === opts.background) {
|
|
186
|
+
void telemetry
|
|
187
|
+
.emit(TelemetryEventKind.FEEDBACK_SUBMITTED, { feedback: parsed.data })
|
|
188
|
+
.then((ok) => {
|
|
189
|
+
if (ok)
|
|
190
|
+
markDelivered(queued);
|
|
191
|
+
else
|
|
192
|
+
noteFeedbackUndelivered('the telemetry endpoint did not accept it');
|
|
193
|
+
})
|
|
194
|
+
.catch((error) => {
|
|
195
|
+
noteFeedbackUndelivered(error instanceof Error ? error.message : String(error));
|
|
196
|
+
});
|
|
197
|
+
return { sent: false, accepted: true, redacted, context };
|
|
198
|
+
}
|
|
199
|
+
// `sent` reflects DELIVERY, not handing the payload to the emitter. It was unconditional, so a DNS
|
|
200
|
+
// miss or a 4xx both reported "filed" — and this is the only qualitative channel the product has,
|
|
201
|
+
// so a silent failure here loses the report AND tells the reporter it worked.
|
|
202
|
+
const delivered = await telemetry.emit(TelemetryEventKind.FEEDBACK_SUBMITTED, {
|
|
203
|
+
feedback: parsed.data,
|
|
204
|
+
});
|
|
205
|
+
if (delivered) {
|
|
206
|
+
markDelivered(queued);
|
|
207
|
+
return { sent: true, accepted: true, redacted, context };
|
|
208
|
+
}
|
|
209
|
+
// Undelivered, but no longer lost. `accepted` is true when it reached the outbox: the report is
|
|
210
|
+
// well-formed and on disk, which is a materially different situation from "it is gone".
|
|
211
|
+
return {
|
|
212
|
+
sent: false,
|
|
213
|
+
accepted: queued !== null,
|
|
214
|
+
reason: queued !== null
|
|
215
|
+
? `the report could not be delivered (the network call failed or was rejected), but it is SAVED at ${outboxPath()} and nothing was lost. It will be retried; you can also open an issue at https://github.com/reticlehq/reticle/issues with the same detail.`
|
|
216
|
+
: 'the report could not be delivered and could not be saved locally either — it was NOT filed. Retry, or open an issue at https://github.com/reticlehq/reticle/issues with the same detail.',
|
|
217
|
+
redacted,
|
|
218
|
+
context,
|
|
219
|
+
};
|
|
161
220
|
}
|
|
162
221
|
/**
|
|
163
222
|
* The receipt's context PLUS the fields the emitter stamps on every event regardless of kind — the
|
|
@@ -96,7 +96,7 @@ export function gitFacts(cwd, read = defaultRead, exists = defaultExists) {
|
|
|
96
96
|
if (url === undefined)
|
|
97
97
|
return { state: GitState.LOCAL_ONLY };
|
|
98
98
|
const origin = normalizeGitOrigin(url);
|
|
99
|
-
if (
|
|
99
|
+
if ('' === origin)
|
|
100
100
|
return { state: GitState.LOCAL_ONLY };
|
|
101
101
|
return { state: GitState.REMOTE, origin, forge: forgeOf(origin) };
|
|
102
102
|
}
|
|
@@ -62,6 +62,8 @@ export declare class SessionMetrics {
|
|
|
62
62
|
breadcrumb: string[];
|
|
63
63
|
inFlight: string | undefined;
|
|
64
64
|
};
|
|
65
|
+
/** One action driven at the page. Settled by the next verdict; see #unsettledActions. */
|
|
66
|
+
recordAction(): void;
|
|
65
67
|
recordVerification(): void;
|
|
66
68
|
/**
|
|
67
69
|
* One defect found in the app under test — the outcome number, kept broken down by kind.
|
|
@@ -21,6 +21,17 @@ import { describeToolParams } from './argument-shape.js';
|
|
|
21
21
|
import { machineSnapshot } from './machine-snapshot.js';
|
|
22
22
|
/** Cap the distinct error shapes held in memory — a pathological loop must not grow unbounded. */
|
|
23
23
|
const MAX_ERROR_KINDS = 40;
|
|
24
|
+
/** Distinct tool names whose repeat-runs are tracked. Its own cap: the tool surface, not error shapes. */
|
|
25
|
+
const MAX_REPEAT_TOOLS = 64;
|
|
26
|
+
/**
|
|
27
|
+
* Tool errors that all mean the same thing to a user: the agent asked, and there was no app it could
|
|
28
|
+
* reach. Three different messages, one experience — "nothing is connected" — and it is the error
|
|
29
|
+
* that ends most sessions before a single verification happens.
|
|
30
|
+
*
|
|
31
|
+
* `multiple sessions connected` belongs here too: the agent could not reach AN app because it could
|
|
32
|
+
* not tell which one, and the outcome is identical.
|
|
33
|
+
*/
|
|
34
|
+
const NO_SESSION_ERROR = /no browser session connected|no connected session with id|multiple sessions connected/i;
|
|
24
35
|
/** Cap distinct MCP clients recorded; more than a handful on one daemon is already the story. */
|
|
25
36
|
const MAX_CLIENTS = 8;
|
|
26
37
|
/** Bounds on the parameter map. Our own schemas are already finite; these are a belt-and-braces cap. */
|
|
@@ -71,6 +82,25 @@ export class SessionMetrics {
|
|
|
71
82
|
* capped: the kind vocabulary is a bounded enum plus oracle names, not user data.
|
|
72
83
|
*/
|
|
73
84
|
#seenBugKinds = new Set();
|
|
85
|
+
/**
|
|
86
|
+
* Tool errors meaning "the agent could not reach an app at all" — the single biggest drop-off in
|
|
87
|
+
* the funnel, and until now reachable only by unpacking `errors[]` in HogQL. 74% of daemons never
|
|
88
|
+
* call a tool, and of the sessions that made exactly one call, most bounced on this.
|
|
89
|
+
*/
|
|
90
|
+
#noSessionErrors = 0;
|
|
91
|
+
/** Longest back-to-back run per tool — a retry loop, which `toolCounts` cannot distinguish. */
|
|
92
|
+
#repeatRuns = new Map();
|
|
93
|
+
#lastTool;
|
|
94
|
+
#currentRun = 0;
|
|
95
|
+
/**
|
|
96
|
+
* Actions driven since the last verdict — the run that is left hanging if the loop stops here.
|
|
97
|
+
*
|
|
98
|
+
* NOT `actions - verifications`. That difference ignores ORDER, so a verdict that drove nothing
|
|
99
|
+
* (a `flow_verify` over saved flows) silently paid for a genuinely abandoned action elsewhere, and
|
|
100
|
+
* a session could verify first and act after and look settled. What is abandoned is the trailing
|
|
101
|
+
* run, which is exactly what the field claims to mean.
|
|
102
|
+
*/
|
|
103
|
+
#unsettledActions = 0;
|
|
74
104
|
#clients = new Set();
|
|
75
105
|
#startedAt;
|
|
76
106
|
#now;
|
|
@@ -120,6 +150,16 @@ export class SessionMetrics {
|
|
|
120
150
|
this.#unknownToolCalls += 1;
|
|
121
151
|
}
|
|
122
152
|
recordToolCall(tool, args) {
|
|
153
|
+
// A run of the same tool back to back. Five calls to reticle_act is engagement; five in a row
|
|
154
|
+
// after four failures is an agent stuck, and toolCounts reports both as "5".
|
|
155
|
+
this.#currentRun = tool === this.#lastTool ? this.#currentRun + 1 : 1;
|
|
156
|
+
this.#lastTool = tool;
|
|
157
|
+
if (this.#currentRun > 1) {
|
|
158
|
+
const best = this.#repeatRuns.get(tool) ?? 0;
|
|
159
|
+
if (this.#currentRun > best &&
|
|
160
|
+
(this.#repeatRuns.has(tool) || this.#repeatRuns.size < MAX_REPEAT_TOOLS))
|
|
161
|
+
this.#repeatRuns.set(tool, this.#currentRun);
|
|
162
|
+
}
|
|
123
163
|
this.#toolCalls += 1;
|
|
124
164
|
bump(this.#toolCounts, tool);
|
|
125
165
|
this.#inFlight = tool;
|
|
@@ -150,6 +190,8 @@ export class SessionMetrics {
|
|
|
150
190
|
*/
|
|
151
191
|
recordToolError(message, tool) {
|
|
152
192
|
this.#toolErrors += 1;
|
|
193
|
+
if (NO_SESSION_ERROR.test(message))
|
|
194
|
+
this.#noSessionErrors += 1;
|
|
153
195
|
const fingerprint = fingerprintError(message);
|
|
154
196
|
const existing = this.#errorKinds.get(fingerprint);
|
|
155
197
|
if (existing !== undefined) {
|
|
@@ -194,8 +236,14 @@ export class SessionMetrics {
|
|
|
194
236
|
get trail() {
|
|
195
237
|
return { breadcrumb: [...this.#breadcrumb], inFlight: this.#inFlight };
|
|
196
238
|
}
|
|
239
|
+
/** One action driven at the page. Settled by the next verdict; see #unsettledActions. */
|
|
240
|
+
recordAction() {
|
|
241
|
+
this.#unsettledActions += 1;
|
|
242
|
+
}
|
|
197
243
|
recordVerification() {
|
|
198
244
|
this.#verifications += 1;
|
|
245
|
+
// A verdict settles whatever was outstanding, however many actions it took to get there.
|
|
246
|
+
this.#unsettledActions = 0;
|
|
199
247
|
}
|
|
200
248
|
/**
|
|
201
249
|
* One defect found in the app under test — the outcome number, kept broken down by kind.
|
|
@@ -258,6 +306,12 @@ export class SessionMetrics {
|
|
|
258
306
|
verifications: this.#verifications,
|
|
259
307
|
bugsFound: this.#bugsFound,
|
|
260
308
|
...(this.#bugKinds.size > 0 ? { bugKinds: Object.fromEntries(this.#bugKinds) } : {}),
|
|
309
|
+
...(this.#noSessionErrors > 0 ? { noSessionErrors: this.#noSessionErrors } : {}),
|
|
310
|
+
...(this.#repeatRuns.size > 0
|
|
311
|
+
? { consecutiveRepeats: Object.fromEntries(this.#repeatRuns) }
|
|
312
|
+
: {}),
|
|
313
|
+
// Absent rather than zero, so the PRESENCE of the field is the signal on a dashboard.
|
|
314
|
+
...(this.#unsettledActions > 0 ? { abandonedActions: this.#unsettledActions } : {}),
|
|
261
315
|
...(this.#toolParams.size > 0
|
|
262
316
|
? {
|
|
263
317
|
toolParams: Object.fromEntries([...this.#toolParams].map(([tool, params]) => [tool, Object.fromEntries(params)])),
|
|
@@ -277,17 +331,22 @@ export class SessionMetrics {
|
|
|
277
331
|
}
|
|
278
332
|
/** True when nothing at all happened — a flush of an idle daemon is not worth an event. */
|
|
279
333
|
get empty() {
|
|
280
|
-
return (this.#toolCalls
|
|
281
|
-
this.#verifications
|
|
282
|
-
this.#toolErrors
|
|
283
|
-
this.#bugsFound
|
|
284
|
-
this.#sdkFailures
|
|
334
|
+
return (0 === this.#toolCalls &&
|
|
335
|
+
0 === this.#verifications &&
|
|
336
|
+
0 === this.#toolErrors &&
|
|
337
|
+
0 === this.#bugsFound &&
|
|
338
|
+
0 === this.#sdkFailures);
|
|
285
339
|
}
|
|
286
340
|
/** Zero the counters after a non-final flush so the next flush reports the NEXT window, not a total. */
|
|
287
341
|
reset() {
|
|
288
342
|
this.#toolCalls = 0;
|
|
289
343
|
this.#toolErrors = 0;
|
|
290
344
|
this.#verifications = 0;
|
|
345
|
+
this.#noSessionErrors = 0;
|
|
346
|
+
this.#repeatRuns.clear();
|
|
347
|
+
this.#lastTool = undefined;
|
|
348
|
+
this.#currentRun = 0;
|
|
349
|
+
this.#unsettledActions = 0;
|
|
291
350
|
this.#toolCounts.clear();
|
|
292
351
|
this.#toolParams.clear();
|
|
293
352
|
this.#errorKinds.clear();
|
|
@@ -302,7 +361,10 @@ export class SessionMetrics {
|
|
|
302
361
|
this.#unknownToolCalls = 0;
|
|
303
362
|
this.#bugsFound = 0;
|
|
304
363
|
this.#bugKinds.clear();
|
|
305
|
-
|
|
364
|
+
// #seenBugKinds is deliberately NOT cleared. It is not a window counter — it is the
|
|
365
|
+
// session-lifetime memory behind `repeat` on bug_found, and zeroing it made the same defect,
|
|
366
|
+
// found again after a flush, report as a newly distinct one. Sessions run to 11.5 hours in the
|
|
367
|
+
// data, so that was up to 23 chances to count one defect many times.
|
|
306
368
|
}
|
|
307
369
|
}
|
|
308
370
|
function bump(map, key) {
|
|
@@ -1,4 +1,20 @@
|
|
|
1
|
-
import { TelemetryEventKind, type Crash, type Feedback, type Identity, type BugFound, type InitOutcome, type McpConnection, type ProjectProfile, type SessionSummary, type TelemetryActor, type Verification, type VersionChange } from '@reticlehq/core';
|
|
1
|
+
import { TelemetryEventKind, type Crash, type Feedback, type Identity, type BugFound, type InitOutcome, type McpConnection, type McpOutage, type ProjectProfile, type SessionSummary, type TelemetryActor, type Verification, type VersionChange } from '@reticlehq/core';
|
|
2
|
+
/**
|
|
3
|
+
* Feedback gets its own, much larger budget — and a retry.
|
|
4
|
+
*
|
|
5
|
+
* 2s + drop is right for a usage counter: the metric is worthless if it costs the user latency, and
|
|
6
|
+
* losing one changes nothing. Feedback is the opposite on both counts. It is the only qualitative
|
|
7
|
+
* channel the product has, it carries an agent's entire root-cause analysis, and losing one loses
|
|
8
|
+
* work that cannot be reconstructed.
|
|
9
|
+
*
|
|
10
|
+
* Measured to the collector with a WARM DNS cache: dns 0.003s, connect 0.233s, tls 0.467s, total
|
|
11
|
+
* 0.694s — a third of the old budget gone before any payload moves, on the good path. A fresh
|
|
12
|
+
* short-lived CLI process pays cold DNS and a cold route on top, which is exactly the case that
|
|
13
|
+
* failed in the field and then succeeded on retry from a warmer process.
|
|
14
|
+
*/
|
|
15
|
+
export declare const FEEDBACK_TIMEOUT_MS = 15000;
|
|
16
|
+
export declare const FEEDBACK_RETRIES = 1;
|
|
17
|
+
export declare const FEEDBACK_RETRY_BACKOFF_MS = 750;
|
|
2
18
|
/** The current telemetry state and which control set it — what `reticle telemetry status` prints. */
|
|
3
19
|
export declare const describeTelemetry: (env?: NodeJS.ProcessEnv, dir?: string, cwd?: string) => {
|
|
4
20
|
enabled: boolean;
|
|
@@ -38,6 +54,8 @@ export interface TelemetryExtra {
|
|
|
38
54
|
init?: InitOutcome;
|
|
39
55
|
/** `bug_found`: one defect Reticle found in the app under test. */
|
|
40
56
|
bug?: BugFound;
|
|
57
|
+
/** `mcp_connection_lost`: the agent lost its tools — which stage, and why. */
|
|
58
|
+
outage?: McpOutage;
|
|
41
59
|
}
|
|
42
60
|
export interface Telemetry {
|
|
43
61
|
/**
|
|
@@ -45,7 +63,14 @@ export interface Telemetry {
|
|
|
45
63
|
* CLI process can exit immediately instead of waiting out the POST (use it from command entry
|
|
46
64
|
* points; daemon-side events send in-process).
|
|
47
65
|
*/
|
|
48
|
-
|
|
66
|
+
/**
|
|
67
|
+
* Send one event. Resolves to whether it was DELIVERED.
|
|
68
|
+
*
|
|
69
|
+
* Almost every caller ignores the result and should — a lost metric must never surface to a user.
|
|
70
|
+
* `reticle_feedback` is the exception: it hands a receipt to an agent, and reporting "filed" for a
|
|
71
|
+
* send that failed loses the report and lies about it in the same breath.
|
|
72
|
+
*/
|
|
73
|
+
emit(kind: TelemetryEventKind, extra?: TelemetryExtra): Promise<boolean>;
|
|
49
74
|
readonly enabled: boolean;
|
|
50
75
|
/** True the very first run on this machine — the CLI emits INSTALL alongside the first INVOKE. */
|
|
51
76
|
readonly firstRun: boolean;
|
|
@@ -16,11 +16,11 @@
|
|
|
16
16
|
*/
|
|
17
17
|
import { spawn } from 'node:child_process';
|
|
18
18
|
import { randomUUID, createHash } from 'node:crypto';
|
|
19
|
-
import { readFileSync, writeFileSync, mkdirSync, existsSync, rmSync } from 'node:fs';
|
|
19
|
+
import { appendFileSync, readFileSync, writeFileSync, mkdirSync, existsSync, rmSync, } from 'node:fs';
|
|
20
20
|
import { homedir, platform } from 'node:os';
|
|
21
21
|
import { join } from 'node:path';
|
|
22
|
-
import { TelemetryEventKind, TELEMETRY_EVENT_VERSION, ProjectIdSource, } from '@reticlehq/core';
|
|
23
|
-
import { SERVER_VERSION } from '../server-version.js';
|
|
22
|
+
import { TelemetryEventKind, TELEMETRY_EVENT_VERSION, ProjectIdSource, isSessionScoped, } from '@reticlehq/core';
|
|
23
|
+
import { SERVER_VERSION } from '../version/server-version.js';
|
|
24
24
|
const RETICLE_DIR = join(homedir(), '.reticle');
|
|
25
25
|
const ID_FILE = join(RETICLE_DIR, 'telemetry-id');
|
|
26
26
|
const NOTICE_FILE = join(RETICLE_DIR, 'telemetry-notice-shown');
|
|
@@ -43,6 +43,22 @@ const POSTHOG_KEY = 'phc_q4yCxKMXHfWQ43VAz4HxJXZFLqFi4nBbg5v3avemggYo';
|
|
|
43
43
|
*/
|
|
44
44
|
const POSTHOG_PERSONLESS = { $process_person_profile: false };
|
|
45
45
|
const SEND_TIMEOUT_MS = 2000;
|
|
46
|
+
/**
|
|
47
|
+
* Feedback gets its own, much larger budget — and a retry.
|
|
48
|
+
*
|
|
49
|
+
* 2s + drop is right for a usage counter: the metric is worthless if it costs the user latency, and
|
|
50
|
+
* losing one changes nothing. Feedback is the opposite on both counts. It is the only qualitative
|
|
51
|
+
* channel the product has, it carries an agent's entire root-cause analysis, and losing one loses
|
|
52
|
+
* work that cannot be reconstructed.
|
|
53
|
+
*
|
|
54
|
+
* Measured to the collector with a WARM DNS cache: dns 0.003s, connect 0.233s, tls 0.467s, total
|
|
55
|
+
* 0.694s — a third of the old budget gone before any payload moves, on the good path. A fresh
|
|
56
|
+
* short-lived CLI process pays cold DNS and a cold route on top, which is exactly the case that
|
|
57
|
+
* failed in the field and then succeeded on retry from a warmer process.
|
|
58
|
+
*/
|
|
59
|
+
export const FEEDBACK_TIMEOUT_MS = 15_000;
|
|
60
|
+
export const FEEDBACK_RETRIES = 1;
|
|
61
|
+
export const FEEDBACK_RETRY_BACKOFF_MS = 750;
|
|
46
62
|
/**
|
|
47
63
|
* The sender a `detach: true` emit runs in a disowned child (argv: [url, body]) — an in-process fetch
|
|
48
64
|
* keeps Node's event loop alive until the POST finishes, which taxed every short-lived CLI command
|
|
@@ -59,6 +75,7 @@ const Env = {
|
|
|
59
75
|
DISABLE: 'RETICLE_TELEMETRY', // "0" / "false" / "off" → disabled
|
|
60
76
|
DO_NOT_TRACK: 'DO_NOT_TRACK', // the cross-tool opt-out convention (any truthy value)
|
|
61
77
|
URL: 'RETICLE_TELEMETRY_URL', // override the PostHog host (EU cloud / self-hosted)
|
|
78
|
+
FILE: 'RETICLE_TELEMETRY_FILE', // record to a local JSONL file and send NOTHING — see the sink note
|
|
62
79
|
KEY: 'RETICLE_TELEMETRY_KEY', // override the PostHog project key
|
|
63
80
|
CI: 'CI', // presence marks a CI environment
|
|
64
81
|
VITEST: 'VITEST', // set by vitest — unit tests must never phone home
|
|
@@ -67,6 +84,11 @@ const isDisabled = (env, cwd = process.cwd()) => {
|
|
|
67
84
|
const off = new Set(['0', 'false', 'off', 'no']);
|
|
68
85
|
if (off.has((env[Env.DISABLE] ?? '').toLowerCase()))
|
|
69
86
|
return true;
|
|
87
|
+
// Recording to a local file is not "phoning home", and the guards below exist to stop us phoning
|
|
88
|
+
// home. Chief among them is the source-checkout rule — which is exactly where a release sweep is
|
|
89
|
+
// driven from, so a sink that inherited it would record nothing and look like it had worked.
|
|
90
|
+
if ((env[Env.FILE] ?? '') !== '')
|
|
91
|
+
return false;
|
|
70
92
|
if (env[Env.VITEST] !== undefined)
|
|
71
93
|
return true; // a test run is not a user
|
|
72
94
|
// Developing Reticle is not using Reticle. The `.env` carrying RETICLE_TELEMETRY=0 is gitignored,
|
|
@@ -75,8 +97,21 @@ const isDisabled = (env, cwd = process.cwd()) => {
|
|
|
75
97
|
if (isReticleSourceCheckout(cwd))
|
|
76
98
|
return true;
|
|
77
99
|
const dnt = env[Env.DO_NOT_TRACK];
|
|
78
|
-
return typeof dnt
|
|
100
|
+
return 'string' === typeof dnt && dnt !== '' && dnt !== '0';
|
|
79
101
|
};
|
|
102
|
+
/**
|
|
103
|
+
* Would telemetry be on here if we ignored the source-checkout guard alone?
|
|
104
|
+
*
|
|
105
|
+
* That guard exists so a fresh clone does not phone home on a contributor's first `reticle serve` —
|
|
106
|
+
* a rule about PASSIVE collection. Feedback is never passive: somebody typed it. Applying the guard
|
|
107
|
+
* to it means anyone dogfooding from their own checkout files nothing and is told "not sent, unknown
|
|
108
|
+
* reason", which silently loses every report from exactly the people most likely to write good ones.
|
|
109
|
+
* Reported from the field, and the reporter only escaped it by happening to run from another repo.
|
|
110
|
+
*
|
|
111
|
+
* Every explicit opt-out still applies — RETICLE_TELEMETRY=0, DO_NOT_TRACK, the persisted opt-out
|
|
112
|
+
* file, and a test run. This lifts one implicit guard for one user-initiated event kind.
|
|
113
|
+
*/
|
|
114
|
+
const onlyBlockedBySourceCheckout = (env, cwd) => isDisabled(env, cwd) && !isDisabled(env, '/');
|
|
80
115
|
/** Read (or mint-and-persist) the anonymous machine id. `firstRun` is true the run that created it. */
|
|
81
116
|
const resolveIdentity = () => {
|
|
82
117
|
try {
|
|
@@ -172,7 +207,8 @@ export const setTelemetryEnabled = (enabled, dir = RETICLE_DIR) => {
|
|
|
172
207
|
mkdirSync(dir, { recursive: true });
|
|
173
208
|
writeFileSync(optOutFile, '1', 'utf8');
|
|
174
209
|
};
|
|
175
|
-
|
|
210
|
+
// A disabled emitter delivers nothing, and says so — `false` is the honest answer, not a courtesy.
|
|
211
|
+
const NOOP = { emit: () => Promise.resolve(false), enabled: false, firstRun: false };
|
|
176
212
|
/**
|
|
177
213
|
* Build the telemetry emitter for this process. Resolves identity + opt-out ONCE; each `emit` builds a
|
|
178
214
|
* fully-validated core event, maps it into PostHog capture shape, and fires a best-effort POST.
|
|
@@ -182,7 +218,9 @@ const NOOP = { emit: async () => { }, enabled: false, firstRun: false };
|
|
|
182
218
|
export const createTelemetry = (opts) => {
|
|
183
219
|
const env = opts.env ?? process.env;
|
|
184
220
|
const cwd = opts.cwd ?? process.cwd();
|
|
185
|
-
|
|
221
|
+
// A source checkout silences metrics but must not silence FEEDBACK — see onlyBlockedBySourceCheckout.
|
|
222
|
+
const feedbackOnly = onlyBlockedBySourceCheckout(env, cwd);
|
|
223
|
+
if (isDisabled(env, cwd) && !feedbackOnly)
|
|
186
224
|
return NOOP;
|
|
187
225
|
try {
|
|
188
226
|
if (existsSync(OPT_OUT_FILE))
|
|
@@ -192,13 +230,15 @@ export const createTelemetry = (opts) => {
|
|
|
192
230
|
/* unreadable home dir — fall through; the env-var opt-outs above still apply */
|
|
193
231
|
}
|
|
194
232
|
const apiKey = env[Env.KEY] ?? POSTHOG_KEY;
|
|
195
|
-
if (
|
|
233
|
+
if ('' === apiKey)
|
|
196
234
|
return NOOP; // no key baked in (dev/test build) — nowhere to send, stay silent
|
|
197
235
|
const now = opts.now ?? (() => Date.now());
|
|
198
236
|
const doFetch = opts.fetchImpl ?? fetch;
|
|
199
237
|
const url = `${(env[Env.URL] ?? DEFAULT_URL).replace(/\/+$/, '')}${TELEMETRY_PATH}`;
|
|
200
238
|
const ci = env[Env.CI] !== undefined && env[Env.CI] !== '';
|
|
201
239
|
const os = platform();
|
|
240
|
+
// Negated so it reads the way people say it: UTC+2 is +120, not -120.
|
|
241
|
+
const tzOffsetMin = -new Date().getTimezoneOffset();
|
|
202
242
|
const { anonymousId, firstRun } = resolveIdentity();
|
|
203
243
|
/**
|
|
204
244
|
* One id per PROCESS, minted in memory and never persisted — which is exactly right: a daemon run
|
|
@@ -212,7 +252,22 @@ export const createTelemetry = (opts) => {
|
|
|
212
252
|
((command, args) => {
|
|
213
253
|
spawn(command, args, { detached: true, stdio: 'ignore' }).unref();
|
|
214
254
|
});
|
|
255
|
+
/**
|
|
256
|
+
* Returns whether the event was actually DELIVERED.
|
|
257
|
+
*
|
|
258
|
+
* Every caller but one ignores this and should: a lost metric must never surface to a user. The
|
|
259
|
+
* exception is `reticle_feedback`, which hands a receipt back to an agent — `sent: true` there was
|
|
260
|
+
* unconditional, so a DNS miss or a 4xx both read as "filed", and the only qualitative channel the
|
|
261
|
+
* product has could fail silently while telling the reporter it had worked.
|
|
262
|
+
*
|
|
263
|
+
* A detached send reports false: it is handed to a disowned child precisely so the caller does not
|
|
264
|
+
* wait, so its outcome is genuinely unknown and claiming success would be the same lie.
|
|
265
|
+
*/
|
|
215
266
|
const emit = async (kind, extra) => {
|
|
267
|
+
// In a Reticle source checkout the emitter carries FEEDBACK and nothing else: somebody typed the
|
|
268
|
+
// feedback, and no metric is worth phoning home from a contributor's clone.
|
|
269
|
+
if (feedbackOnly && TelemetryEventKind.FEEDBACK_SUBMITTED !== kind)
|
|
270
|
+
return false;
|
|
216
271
|
const event = {
|
|
217
272
|
v: TELEMETRY_EVENT_VERSION,
|
|
218
273
|
anonymousId,
|
|
@@ -223,6 +278,7 @@ export const createTelemetry = (opts) => {
|
|
|
223
278
|
version: opts.version,
|
|
224
279
|
ci,
|
|
225
280
|
os,
|
|
281
|
+
tzOffsetMin,
|
|
226
282
|
projectIdSource,
|
|
227
283
|
...(extra?.actor !== undefined ? { actor: extra.actor } : {}),
|
|
228
284
|
...(extra?.command !== undefined ? { command: extra.command } : {}),
|
|
@@ -237,19 +293,22 @@ export const createTelemetry = (opts) => {
|
|
|
237
293
|
...(extra?.connection !== undefined ? { connection: extra.connection } : {}),
|
|
238
294
|
...(extra?.init !== undefined ? { init: extra.init } : {}),
|
|
239
295
|
...(extra?.bug !== undefined ? { bug: extra.bug } : {}),
|
|
296
|
+
...(extra?.outage !== undefined ? { outage: extra.outage } : {}),
|
|
240
297
|
};
|
|
241
298
|
// Map the core contract onto PostHog's capture shape: id/name/time move up, the rest are properties.
|
|
242
299
|
// The feedback body is FLATTENED into `feedback_*` properties rather than sent as a nested object:
|
|
243
300
|
// PostHog filters and breakdowns operate on top-level properties, so a nested `feedback.stack`
|
|
244
301
|
// would be invisible to the exact "which stacks report the most bugs" question this exists to
|
|
245
302
|
// answer. One prefix keeps them grouped in the UI without a nested path.
|
|
246
|
-
const { anonymousId: distinctId, event: name, ts, sessionId: eventSessionId, feedback, session, project, verification, versionChange, crash, identity, connection, init, bug, ...rest } = event;
|
|
303
|
+
const { anonymousId: distinctId, event: name, ts, sessionId: eventSessionId, feedback, session, project, verification, versionChange, crash, identity, connection, init, bug, outage, ...rest } = event;
|
|
247
304
|
// `$session_id` is PostHog's OWN session property, so sending ours under that name lights up
|
|
248
305
|
// its built-in session views and funnels for free rather than requiring custom HogQL everywhere.
|
|
306
|
+
// `sessionId` rides only on events that happened inside a daemon run. A one-shot CLI command
|
|
307
|
+
// mints a per-process id that joins to nothing and inflates every session count — see
|
|
308
|
+
// isSessionScoped for the measurement.
|
|
249
309
|
const properties = {
|
|
250
310
|
...rest,
|
|
251
|
-
sessionId: eventSessionId,
|
|
252
|
-
$session_id: eventSessionId,
|
|
311
|
+
...(isSessionScoped(name) ? { sessionId: eventSessionId, $session_id: eventSessionId } : {}),
|
|
253
312
|
};
|
|
254
313
|
// Each block is flattened under its own prefix rather than nested. PostHog filters, breakdowns and
|
|
255
314
|
// insight builders all operate on TOP-LEVEL properties, so a nested `project.stack` is invisible to
|
|
@@ -269,37 +328,68 @@ export const createTelemetry = (opts) => {
|
|
|
269
328
|
connection,
|
|
270
329
|
init,
|
|
271
330
|
bug,
|
|
331
|
+
outage,
|
|
272
332
|
};
|
|
273
333
|
for (const [prefix, block] of Object.entries(blocks)) {
|
|
274
334
|
for (const [key, value] of Object.entries(block ?? {})) {
|
|
275
335
|
properties[`${prefix}_${key}`] = value;
|
|
276
336
|
}
|
|
277
337
|
}
|
|
278
|
-
const
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
338
|
+
const capture = {
|
|
339
|
+
event: name,
|
|
340
|
+
distinct_id: distinctId,
|
|
341
|
+
timestamp: new Date(ts).toISOString(),
|
|
342
|
+
properties: { ...properties, ...POSTHOG_PERSONLESS },
|
|
343
|
+
};
|
|
344
|
+
const body = JSON.stringify({ api_key: apiKey, batch: [capture] });
|
|
345
|
+
const filePath = env[Env.FILE];
|
|
346
|
+
if (filePath !== undefined && filePath !== '') {
|
|
347
|
+
// The SAME payload the wire would have carried, built by the same code path and redacted by
|
|
348
|
+
// the same rules — so what a run records is what a user would have sent. One JSON object per
|
|
349
|
+
// line, appended, so a whole sweep lands in one file in order.
|
|
350
|
+
try {
|
|
351
|
+
appendFileSync(filePath, `${JSON.stringify(capture)}\n`, 'utf8');
|
|
352
|
+
return true;
|
|
353
|
+
}
|
|
354
|
+
catch {
|
|
355
|
+
return false; // a sink that cannot write must not take the daemon down
|
|
356
|
+
}
|
|
357
|
+
}
|
|
289
358
|
try {
|
|
290
|
-
if (extra?.detach
|
|
359
|
+
if (true === extra?.detach && opts.fetchImpl === undefined) {
|
|
291
360
|
spawnDetached(process.execPath, ['-e', DETACHED_SEND_SCRIPT, url, body]);
|
|
292
|
-
return;
|
|
361
|
+
return false; // handed off, outcome unknowable from here
|
|
362
|
+
}
|
|
363
|
+
// Feedback is slower and retried; everything else keeps the fire-and-forget budget.
|
|
364
|
+
const isFeedback = TelemetryEventKind.FEEDBACK_SUBMITTED === kind;
|
|
365
|
+
const timeoutMs = isFeedback ? FEEDBACK_TIMEOUT_MS : SEND_TIMEOUT_MS;
|
|
366
|
+
const attempts = 1 + (isFeedback ? FEEDBACK_RETRIES : 0);
|
|
367
|
+
for (let attempt = 0; attempt < attempts; attempt += 1) {
|
|
368
|
+
if (0 < attempt) {
|
|
369
|
+
await new Promise((r) => setTimeout(r, FEEDBACK_RETRY_BACKOFF_MS * attempt));
|
|
370
|
+
}
|
|
371
|
+
try {
|
|
372
|
+
const response = await doFetch(url, {
|
|
373
|
+
method: 'POST',
|
|
374
|
+
headers: { 'content-type': 'application/json' },
|
|
375
|
+
body,
|
|
376
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
377
|
+
});
|
|
378
|
+
// A 4xx is a rejected payload, not a delivery. It must not read as filed. It is also not
|
|
379
|
+
// worth retrying — the payload will be rejected again — so only a THROW (timeout, DNS,
|
|
380
|
+
// connection reset) goes round again.
|
|
381
|
+
return response.ok !== false;
|
|
382
|
+
}
|
|
383
|
+
catch (error) {
|
|
384
|
+
if (attempt === attempts - 1)
|
|
385
|
+
throw error;
|
|
386
|
+
}
|
|
293
387
|
}
|
|
294
|
-
|
|
295
|
-
method: 'POST',
|
|
296
|
-
headers: { 'content-type': 'application/json' },
|
|
297
|
-
body,
|
|
298
|
-
signal: AbortSignal.timeout(SEND_TIMEOUT_MS),
|
|
299
|
-
});
|
|
388
|
+
return false;
|
|
300
389
|
}
|
|
301
390
|
catch {
|
|
302
391
|
/* best-effort: a lost metric must never surface to the user */
|
|
392
|
+
return false;
|
|
303
393
|
}
|
|
304
394
|
};
|
|
305
395
|
return { emit, enabled: true, firstRun };
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which tool results count as a completed verification, as one pure rule.
|
|
3
|
+
*
|
|
4
|
+
* It used to live inline in `runTool` and read `status === 'pass'`, mapping everything else to
|
|
5
|
+
* "no verdict". That was wrong in both directions at once, and the shape of the error is visible in
|
|
6
|
+
* the production data:
|
|
7
|
+
*
|
|
8
|
+
* - a FAILING suite emitted nothing, so `verification_completed` only ever fired on greens while
|
|
9
|
+
* `bugsInResult` fired on the reds — bugs with no verifications to divide them by;
|
|
10
|
+
* - an EMPTY suite ("all 0 flows pass") emitted `verified: yes, passed: true`.
|
|
11
|
+
*
|
|
12
|
+
* Extracted because a rule this easy to get wrong, feeding the one number shown to investors, should
|
|
13
|
+
* be readable and testable on its own rather than inferred from a ternary inside a dispatcher.
|
|
14
|
+
*/
|
|
15
|
+
import { type BrowserBrand, type Verification } from '@reticlehq/core';
|
|
16
|
+
/**
|
|
17
|
+
* The verification payload for a result, or undefined when nothing was verified.
|
|
18
|
+
*
|
|
19
|
+
* `falseGreenCaught` is the one that matters: the assertion PASSED and Reticle still refused to call
|
|
20
|
+
* it verified. That is the product's thesis reduced to a boolean.
|
|
21
|
+
*/
|
|
22
|
+
export declare function verificationOf(toolName: string, result: Record<string, unknown>, durationMs: number, brand?: BrowserBrand): Verification | undefined;
|