@reticlehq/server 2.7.0 → 2.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/README.md +14 -5
- package/SKILL.md +130 -778
- package/dist/bridge/bridge.d.ts +23 -0
- package/dist/bridge/bridge.js +80 -2
- package/dist/bridge/bridge.test-harness.d.ts +5 -0
- package/dist/bridge/bridge.test-harness.js +6 -4
- package/dist/capsule/causal-summary.d.ts +36 -1
- package/dist/capsule/causal-summary.js +33 -7
- package/dist/capsule/divergence.js +7 -1
- package/dist/cli/chromium-hint.d.ts +33 -5
- package/dist/cli/chromium-hint.js +82 -12
- package/dist/cli/cli-doctor.js +65 -34
- package/dist/cli/cli-flow-commands.js +8 -8
- package/dist/cli/cli-kill.d.ts +58 -0
- package/dist/cli/cli-kill.js +162 -0
- package/dist/cli/cli-parse.d.ts +16 -1
- package/dist/cli/cli-parse.js +32 -9
- package/dist/cli/cli-port.d.ts +32 -0
- package/dist/cli/cli-port.js +82 -35
- package/dist/cli/cli-verify.d.ts +21 -0
- package/dist/cli/cli-verify.js +47 -3
- package/dist/cli/config-discovery.d.ts +35 -0
- package/dist/cli/config-discovery.js +163 -0
- package/dist/cli/doctor-daemon-line.js +4 -1
- package/dist/cli/doctor-project-line.d.ts +37 -0
- package/dist/cli/doctor-project-line.js +35 -0
- package/dist/cli/doctor-rows.d.ts +51 -0
- package/dist/cli/doctor-rows.js +54 -0
- package/dist/cli/doctor-sessions-line.d.ts +12 -0
- package/dist/cli/doctor-sessions-line.js +69 -0
- package/dist/cli/drive-attach.d.ts +99 -0
- package/dist/cli/drive-attach.js +203 -0
- package/dist/cli/drive-command.d.ts +13 -0
- package/dist/cli/drive-command.js +103 -0
- package/dist/cli/open-note.d.ts +20 -0
- package/dist/cli/open-note.js +37 -0
- package/dist/cli/port-holder.d.ts +10 -0
- package/dist/cli/port-holder.js +26 -2
- package/dist/cli/status-next-action.d.ts +37 -0
- package/dist/cli/status-next-action.js +61 -0
- package/dist/cli.js +119 -39
- package/dist/crawl/crawl-tools.js +12 -2
- package/dist/crawl/crawl.d.ts +13 -0
- package/dist/crawl/crawl.js +50 -2
- package/dist/events/contradictions.d.ts +46 -0
- package/dist/events/contradictions.js +35 -22
- package/dist/events/declared.d.ts +45 -0
- package/dist/events/declared.js +73 -0
- package/dist/events/echo-mismatch.d.ts +8 -2
- package/dist/events/echo-mismatch.js +33 -5
- package/dist/events/event-filters.d.ts +1 -1
- package/dist/events/event-filters.js +7 -4
- package/dist/events/predicate-eval.d.ts +17 -72
- package/dist/events/predicate-eval.js +117 -236
- package/dist/events/predicate-parse.js +31 -3
- package/dist/events/predicate-route.d.ts +32 -0
- package/dist/events/predicate-route.js +102 -0
- package/dist/events/predicate-schema.d.ts +99 -0
- package/dist/events/predicate-schema.js +309 -0
- package/dist/events/predicate.d.ts +8 -0
- package/dist/events/predicate.js +296 -22
- package/dist/flows/annotate-tools.js +3 -0
- package/dist/flows/annotate.js +18 -4
- package/dist/flows/coverage.d.ts +38 -5
- package/dist/flows/coverage.js +30 -4
- package/dist/flows/decision.d.ts +15 -0
- package/dist/flows/decision.js +1 -1
- package/dist/flows/flow-classify.js +1 -1
- package/dist/flows/flow-replay-run.js +8 -1
- package/dist/flows/flow-tools.js +21 -5
- package/dist/flows/heal-run.js +1 -1
- package/dist/flows/replay.js +3 -0
- package/dist/honesty/accepted-write.d.ts +3 -0
- package/dist/honesty/accepted-write.js +8 -3
- package/dist/honesty/already-true.js +15 -0
- package/dist/honesty/blind-spots.js +10 -2
- package/dist/honesty/honesty.d.ts +11 -0
- package/dist/honesty/honesty.js +1 -0
- package/dist/honesty/uncaptured-bodies.d.ts +4 -2
- package/dist/honesty/uncaptured-bodies.js +9 -2
- package/dist/honesty/unread-outcome.d.ts +8 -0
- package/dist/honesty/unread-outcome.js +36 -9
- package/dist/honesty/unsettled.d.ts +49 -0
- package/dist/honesty/unsettled.js +82 -0
- package/dist/honesty/verified.d.ts +36 -3
- package/dist/honesty/verified.js +54 -11
- package/dist/http-server.d.ts +19 -0
- package/dist/http-server.js +202 -4
- package/dist/impact/impact-recorder.d.ts +23 -0
- package/dist/impact/impact-recorder.js +76 -0
- package/dist/impact/impact-store.d.ts +47 -0
- package/dist/impact/impact-store.js +171 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +47 -10
- package/dist/init/agent-rules.d.ts +36 -0
- package/dist/init/agent-rules.js +132 -22
- package/dist/init/astro-patch.d.ts +2 -1
- package/dist/init/astro-patch.js +38 -6
- package/dist/init/capabilities.d.ts +22 -0
- package/dist/init/capabilities.js +76 -0
- package/dist/init/confirm.d.ts +114 -0
- package/dist/init/confirm.js +165 -0
- package/dist/init/connect-steps.d.ts +2 -0
- package/dist/init/connect-steps.js +50 -0
- package/dist/init/cra.js +3 -2
- package/dist/init/csp-check.d.ts +38 -0
- package/dist/init/csp-check.js +92 -0
- package/dist/init/csp-doctor.d.ts +25 -0
- package/dist/init/csp-doctor.js +49 -0
- package/dist/init/cursor.js +6 -9
- package/dist/init/detect.d.ts +8 -0
- package/dist/init/detect.js +33 -3
- package/dist/init/mcp-clients.d.ts +1 -0
- package/dist/init/mcp-clients.js +51 -7
- package/dist/init/mcp.d.ts +15 -0
- package/dist/init/mcp.js +19 -0
- package/dist/init/non-js-project.js +29 -5
- package/dist/init/plan-framework.d.ts +10 -0
- package/dist/init/plan-framework.js +78 -16
- package/dist/init/plan.d.ts +47 -6
- package/dist/init/plan.js +228 -69
- package/dist/init/run.d.ts +24 -2
- package/dist/init/run.js +157 -45
- package/dist/init/slash-command.d.ts +1 -1
- package/dist/init/slash-command.js +54 -6
- package/dist/init/snippets.d.ts +45 -5
- package/dist/init/snippets.js +194 -42
- package/dist/init/vite-config.js +53 -5
- package/dist/input/network-mock-tools.d.ts +19 -0
- package/dist/input/network-mock-tools.js +37 -5
- package/dist/input/scroll-find.d.ts +6 -0
- package/dist/input/scroll-find.js +17 -2
- package/dist/input/scroll-tools.js +4 -0
- package/dist/journal/attach-journal.d.ts +9 -1
- package/dist/journal/attach-journal.js +13 -2
- package/dist/journal/journal-query.d.ts +13 -0
- package/dist/journal/journal-query.js +28 -3
- package/dist/journal/workspace-gitignore.d.ts +10 -0
- package/dist/journal/workspace-gitignore.js +59 -0
- package/dist/license/license.js +1 -1
- package/dist/mcp/mcp-proxy.d.ts +35 -30
- package/dist/mcp/mcp-proxy.js +306 -104
- package/dist/mcp/mcp.d.ts +10 -1
- package/dist/mcp/mcp.js +70 -34
- package/dist/mcp/proxy-handshake.d.ts +82 -1
- package/dist/mcp/proxy-handshake.js +109 -2
- package/dist/mcp/proxy-lifecycle.d.ts +11 -2
- package/dist/mcp/proxy-lifecycle.js +12 -3
- package/dist/mcp/proxy-log.d.ts +63 -0
- package/dist/mcp/proxy-log.js +132 -0
- package/dist/mcp/server-instructions.d.ts +17 -0
- package/dist/mcp/server-instructions.js +56 -0
- package/dist/mcp/shared-params.d.ts +27 -0
- package/dist/mcp/shared-params.js +36 -0
- package/dist/pool/browser-pool.d.ts +22 -2
- package/dist/pool/browser-pool.js +44 -4
- package/dist/project/learned-routes.d.ts +24 -0
- package/dist/project/learned-routes.js +63 -0
- package/dist/project/project-store.d.ts +6 -0
- package/dist/project/project-store.js +25 -1
- package/dist/project/reticle-dir.d.ts +2 -0
- package/dist/project/reticle-dir.js +1 -0
- package/dist/session/connection-memory.d.ts +59 -0
- package/dist/session/connection-memory.js +108 -0
- package/dist/session/dev-command.d.ts +26 -0
- package/dist/session/dev-command.js +89 -0
- package/dist/session/dev-server-probe.d.ts +68 -0
- package/dist/session/dev-server-probe.js +102 -7
- package/dist/session/last-act.d.ts +26 -0
- package/dist/session/last-act.js +35 -1
- package/dist/session/live-control-tools.js +42 -5
- package/dist/session/no-session-diagnosis.d.ts +67 -2
- package/dist/session/no-session-diagnosis.js +236 -27
- package/dist/session/no-session-next-action.d.ts +46 -0
- package/dist/session/no-session-next-action.js +85 -0
- package/dist/session/no-session-watch.d.ts +40 -1
- package/dist/session/no-session-watch.js +174 -8
- package/dist/session/pending-commands.d.ts +12 -0
- package/dist/session/pending-commands.js +13 -1
- package/dist/session/ref-provenance.d.ts +54 -0
- package/dist/session/ref-provenance.js +61 -0
- package/dist/session/session-lease.js +1 -1
- package/dist/session/session-manager.d.ts +4 -0
- package/dist/session/session-manager.js +35 -5
- package/dist/session/session.d.ts +10 -1
- package/dist/session/session.js +146 -5
- package/dist/telemetry/app-instrumented.d.ts +8 -0
- package/dist/telemetry/app-instrumented.js +10 -0
- package/dist/telemetry/automation-hint.d.ts +17 -0
- package/dist/telemetry/automation-hint.js +71 -0
- package/dist/telemetry/bug-found.d.ts +15 -17
- package/dist/telemetry/bug-found.js +52 -21
- package/dist/telemetry/cli-telemetry.js +7 -1
- package/dist/telemetry/daemon-telemetry.js +24 -0
- package/dist/telemetry/feedback.js +1 -1
- package/dist/telemetry/init-telemetry.js +9 -1
- package/dist/telemetry/install-source.d.ts +56 -0
- package/dist/telemetry/install-source.js +74 -0
- package/dist/telemetry/instrumentation-stall.d.ts +50 -0
- package/dist/telemetry/instrumentation-stall.js +90 -0
- package/dist/telemetry/run-telemetry.js +4 -1
- package/dist/telemetry/session-metrics.d.ts +8 -0
- package/dist/telemetry/session-metrics.js +15 -0
- package/dist/telemetry/telemetry.d.ts +7 -1
- package/dist/telemetry/telemetry.js +26 -2
- package/dist/telemetry/tool-refused.d.ts +19 -0
- package/dist/telemetry/tool-refused.js +55 -0
- package/dist/telemetry/verification-of.js +46 -7
- package/dist/temp-dir.d.ts +27 -0
- package/dist/temp-dir.js +46 -0
- package/dist/tools/act-tools.js +209 -28
- package/dist/tools/assert-grade.d.ts +1 -1
- package/dist/tools/assert-grade.js +27 -5
- package/dist/tools/assert-verdict.js +49 -7
- package/dist/tools/browser-tools.js +10 -0
- package/dist/tools/contract-tools.js +4 -0
- package/dist/tools/dynamic-tools.js +16 -7
- package/dist/tools/error-recovery.d.ts +10 -0
- package/dist/tools/error-recovery.js +73 -9
- package/dist/tools/feedback-invite.js +1 -1
- package/dist/tools/invoke-tool.js +159 -26
- package/dist/tools/lease-hint.d.ts +30 -8
- package/dist/tools/lease-hint.js +65 -14
- package/dist/tools/lease-tools.d.ts +40 -1
- package/dist/tools/lease-tools.js +181 -83
- package/dist/tools/navigate-result.js +1 -1
- package/dist/tools/observe-tools.js +10 -4
- package/dist/tools/observed-nothing.js +1 -1
- package/dist/tools/react-grace.d.ts +52 -0
- package/dist/tools/react-grace.js +90 -0
- package/dist/tools/read-tools.js +67 -5
- package/dist/tools/resolve-target.d.ts +33 -0
- package/dist/tools/resolve-target.js +53 -0
- package/dist/tools/settle-in-flight.d.ts +94 -0
- package/dist/tools/settle-in-flight.js +146 -0
- package/dist/tools/snapshot-delta.d.ts +10 -0
- package/dist/tools/snapshot-delta.js +194 -9
- package/dist/tools/tool-kit.d.ts +8 -0
- package/dist/tools/tool-surface.d.ts +60 -0
- package/dist/tools/tool-surface.js +86 -1
- package/dist/tools/tools.js +61 -8
- package/dist/tools/unadvertised-help.js +9 -6
- package/docs/README.md +55 -11
- package/docs/actions.mdx +120 -0
- package/docs/agent-cheatsheet.md +95 -29
- package/docs/architecture.md +52 -35
- package/docs/benchmarks.md +127 -75
- package/docs/best-practices.mdx +216 -0
- package/docs/capabilities.mdx +534 -0
- package/docs/cli/affected.mdx +53 -0
- package/docs/cli/capsules.mdx +70 -0
- package/docs/cli/cloud.mdx +153 -0
- package/docs/cli/doctor.mdx +109 -0
- package/docs/cli/drive.mdx +74 -0
- package/docs/cli/feedback.mdx +67 -0
- package/docs/cli/gate.mdx +64 -0
- package/docs/cli/help.mdx +94 -0
- package/docs/cli/hunt.mdx +73 -0
- package/docs/cli/identify.mdx +76 -0
- package/docs/cli/init.mdx +121 -0
- package/docs/cli/kill.mdx +91 -0
- package/docs/cli/license.mdx +48 -0
- package/docs/cli/mcp.mdx +61 -0
- package/docs/cli/open.mdx +74 -0
- package/docs/cli/restart.mdx +60 -0
- package/docs/cli/rollback.mdx +35 -0
- package/docs/cli/serve.mdx +69 -0
- package/docs/cli/status.mdx +88 -0
- package/docs/cli/stop.mdx +64 -0
- package/docs/cli/telemetry.mdx +57 -0
- package/docs/cli/update.mdx +52 -0
- package/docs/cli/verify.mdx +76 -0
- package/docs/cli/version.mdx +39 -0
- package/docs/cli/watch.mdx +62 -0
- package/docs/cli.mdx +241 -0
- package/docs/debugging.md +16 -10
- package/docs/deploy-checks.md +45 -12
- package/docs/desktop.mdx +384 -0
- package/docs/docs.json +364 -0
- package/docs/enterprise.md +22 -16
- package/docs/faq.mdx +102 -0
- package/docs/fixtures.md +13 -7
- package/docs/flows.md +29 -23
- package/docs/for-agents.md +71 -0
- package/docs/frameworks.mdx +238 -0
- package/docs/gate-plan.md +48 -42
- package/docs/gates.md +28 -22
- package/docs/getting-started.md +129 -77
- package/docs/human-control.md +24 -18
- package/docs/index.mdx +143 -0
- package/docs/install-agentic.mdx +224 -0
- package/docs/install-manual.mdx +196 -0
- package/docs/instrumentation.mdx +238 -0
- package/docs/integration-patterns.md +41 -35
- package/docs/local-registry.md +19 -13
- package/docs/multi-agent-testing.md +19 -13
- package/docs/packages/babel-plugin.mdx +79 -0
- package/docs/packages/browser.mdx +111 -0
- package/docs/packages/core.mdx +116 -0
- package/docs/packages/electron.mdx +85 -0
- package/docs/packages/eslint-plugin.mdx +86 -0
- package/docs/packages/next.mdx +89 -0
- package/docs/packages/react.mdx +91 -0
- package/docs/packages/server.mdx +98 -0
- package/docs/packages/tauri.mdx +63 -0
- package/docs/packages/test.mdx +119 -0
- package/docs/packages/vite-plugin.mdx +94 -0
- package/docs/packages.mdx +218 -0
- package/docs/platform-integration.md +35 -26
- package/docs/predicates.mdx +248 -0
- package/docs/quickstart.mdx +257 -0
- package/docs/recipes.mdx +282 -0
- package/docs/skill-file.mdx +107 -0
- package/docs/state-management.mdx +199 -0
- package/docs/style.css +14 -0
- package/docs/system-map.md +50 -33
- package/docs/telemetry-contract.md +106 -52
- package/docs/telemetry-events.mdx +475 -0
- package/docs/telemetry.md +63 -55
- package/docs/testing.md +44 -6
- package/docs/token-efficiency.md +37 -27
- package/docs/tools-act-and-wait.mdx +173 -0
- package/docs/tools-act-sequence.mdx +137 -0
- package/docs/tools-act.mdx +95 -0
- package/docs/tools-assert.mdx +129 -0
- package/docs/tools-console.mdx +63 -0
- package/docs/tools-inspect.mdx +88 -0
- package/docs/tools-navigate.mdx +99 -0
- package/docs/tools-network.mdx +119 -0
- package/docs/tools-observe.mdx +145 -0
- package/docs/tools-overview.mdx +160 -0
- package/docs/tools-query.mdx +126 -0
- package/docs/tools-session-and-feedback.mdx +112 -0
- package/docs/tools-sessions.mdx +94 -0
- package/docs/tools-snapshot.mdx +129 -0
- package/docs/tools-state.mdx +104 -0
- package/docs/tools-tools-and-run.mdx +156 -0
- package/docs/tools-wait-for.mdx +85 -0
- package/docs/troubleshooting.mdx +263 -0
- package/docs/usage.md +209 -165
- package/docs/vs-chrome-devtools-mcp.mdx +123 -0
- package/docs/vs-playwright-mcp.mdx +125 -0
- package/docs/vs-screenshots.mdx +137 -0
- package/docs/what-is-reticle.mdx +63 -0
- package/docs/why-reticle.mdx +109 -0
- package/package.json +5 -5
- package/docs/desktop-apps.md +0 -256
- package/docs/matrix/2.5.0/claude-code-machine.json +0 -19
- package/docs/matrix/2.5.0/codex-machine.json +0 -20
- package/docs/matrix/2.5.0/cursor-machine.json +0 -22
- package/docs/matrix/2.5.0/gemini-machine.json +0 -22
- package/docs/matrix/2.5.0/opencode-machine.json +0 -22
- package/docs/matrix/2.5.0/vscode-machine.json +0 -22
- package/docs/matrix/2.5.0/windsurf-machine.json +0 -22
- package/docs/matrix/MATRIX.md +0 -26
- package/docs/matrix/README.md +0 -59
- package/docs/mint.json +0 -61
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which published route brought an install in.
|
|
3
|
+
*
|
|
4
|
+
* Four install routes ship at once and none of them could be told apart, so every question about
|
|
5
|
+
* where distribution effort is working had no data behind it at all.
|
|
6
|
+
*
|
|
7
|
+
* ONE mechanism: an environment variable a channel sets on itself. Everything else that looked like
|
|
8
|
+
* a signal turned out not to be one — see `InstallSource` in core for why `npm_config_user_agent`,
|
|
9
|
+
* a `.claude-plugin/` directory and an installed skill folder all fail to separate these routes.
|
|
10
|
+
*
|
|
11
|
+
* WHICH CHANNELS ARE GENUINELY DETECTABLE TODAY, so nobody later reads a small `unknown` as success:
|
|
12
|
+
* - `plugin` — real. The Claude Code plugin registers the MCP server itself and can set the marker
|
|
13
|
+
* in that server's `env`, so it is carried by the process without anybody typing anything.
|
|
14
|
+
* - `skill_file` / `npx_skill` / `docs_site` / `readme` — real ONLY where that channel's own copy
|
|
15
|
+
* of the install command carries the marker. Each is a separately published artifact, so a copy
|
|
16
|
+
* that has not been updated is indistinguishable from no channel at all.
|
|
17
|
+
* - `cli_direct` — NOT detectable. Nobody types a marker to say they typed a command. It exists so
|
|
18
|
+
* a wrapper can declare it, and until something does, a direct install reports `unknown`.
|
|
19
|
+
*
|
|
20
|
+
* `unknown` is therefore expected to be the largest bucket for as long as the marker is spreading,
|
|
21
|
+
* and shrinking it is a distribution job, never a classifier job. A guessed attribution is worse
|
|
22
|
+
* than none: it is the number distribution decisions get steered on, and a guess and a measurement
|
|
23
|
+
* are indistinguishable once they are in the same column.
|
|
24
|
+
*/
|
|
25
|
+
import { InstallSource } from '@reticlehq/core';
|
|
26
|
+
/** The one marker. Set by a channel on the process that runs the install. */
|
|
27
|
+
export declare const INSTALL_SOURCE_ENV = "RETICLE_INSTALL_SOURCE";
|
|
28
|
+
/**
|
|
29
|
+
* The declared install source, or `unknown`.
|
|
30
|
+
*
|
|
31
|
+
* Narrowed against the closed list rather than echoed: an echo would forward whatever somebody
|
|
32
|
+
* exported — a path, a URL, a campaign string — straight onto the wire, which is the rule this
|
|
33
|
+
* whole contract is built on. Trimmed and lowercased first, because the marker travels through
|
|
34
|
+
* shell snippets and copy-paste, and `Skill_File` is the same channel as `skill_file`.
|
|
35
|
+
*/
|
|
36
|
+
export declare function resolveInstallSource(env?: NodeJS.ProcessEnv): InstallSource;
|
|
37
|
+
/**
|
|
38
|
+
* The install source declared by the environment, or undefined when nothing declared one.
|
|
39
|
+
*
|
|
40
|
+
* Distinct from `resolveInstallSource`, which answers `unknown` so an event always carries a value.
|
|
41
|
+
* A config file wants the opposite: writing `unknown` into `.reticle.json` is indistinguishable
|
|
42
|
+
* from a config written before this field existed, and those two are different facts.
|
|
43
|
+
*/
|
|
44
|
+
export declare function declaredInstallSource(env?: NodeJS.ProcessEnv): InstallSource | undefined;
|
|
45
|
+
/**
|
|
46
|
+
* The install source for a project: what its config recorded, else what the environment declares.
|
|
47
|
+
*
|
|
48
|
+
* The config wins because it is the durable answer. The marker is an environment variable set by
|
|
49
|
+
* whichever channel ran the install, so it exists for exactly one command and is gone by the next
|
|
50
|
+
* one — which is why this field reached a handful of events and nothing else, and why the question
|
|
51
|
+
* it exists to answer could not be asked.
|
|
52
|
+
*
|
|
53
|
+
* Narrowed against the closed list on the way out, the same as the env path: a config file is user
|
|
54
|
+
* data, and a hand-edited one must not be able to put an arbitrary string on the wire.
|
|
55
|
+
*/
|
|
56
|
+
export declare function projectInstallSource(cwd: string, env?: NodeJS.ProcessEnv): InstallSource;
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which published route brought an install in.
|
|
3
|
+
*
|
|
4
|
+
* Four install routes ship at once and none of them could be told apart, so every question about
|
|
5
|
+
* where distribution effort is working had no data behind it at all.
|
|
6
|
+
*
|
|
7
|
+
* ONE mechanism: an environment variable a channel sets on itself. Everything else that looked like
|
|
8
|
+
* a signal turned out not to be one — see `InstallSource` in core for why `npm_config_user_agent`,
|
|
9
|
+
* a `.claude-plugin/` directory and an installed skill folder all fail to separate these routes.
|
|
10
|
+
*
|
|
11
|
+
* WHICH CHANNELS ARE GENUINELY DETECTABLE TODAY, so nobody later reads a small `unknown` as success:
|
|
12
|
+
* - `plugin` — real. The Claude Code plugin registers the MCP server itself and can set the marker
|
|
13
|
+
* in that server's `env`, so it is carried by the process without anybody typing anything.
|
|
14
|
+
* - `skill_file` / `npx_skill` / `docs_site` / `readme` — real ONLY where that channel's own copy
|
|
15
|
+
* of the install command carries the marker. Each is a separately published artifact, so a copy
|
|
16
|
+
* that has not been updated is indistinguishable from no channel at all.
|
|
17
|
+
* - `cli_direct` — NOT detectable. Nobody types a marker to say they typed a command. It exists so
|
|
18
|
+
* a wrapper can declare it, and until something does, a direct install reports `unknown`.
|
|
19
|
+
*
|
|
20
|
+
* `unknown` is therefore expected to be the largest bucket for as long as the marker is spreading,
|
|
21
|
+
* and shrinking it is a distribution job, never a classifier job. A guessed attribution is worse
|
|
22
|
+
* than none: it is the number distribution decisions get steered on, and a guess and a measurement
|
|
23
|
+
* are indistinguishable once they are in the same column.
|
|
24
|
+
*/
|
|
25
|
+
import { InstallSource } from '@reticlehq/core';
|
|
26
|
+
import { findProjectConfig } from '../cli/cli-port.js';
|
|
27
|
+
/** The one marker. Set by a channel on the process that runs the install. */
|
|
28
|
+
export const INSTALL_SOURCE_ENV = 'RETICLE_INSTALL_SOURCE';
|
|
29
|
+
const KNOWN = new Set(Object.values(InstallSource));
|
|
30
|
+
/**
|
|
31
|
+
* The declared install source, or `unknown`.
|
|
32
|
+
*
|
|
33
|
+
* Narrowed against the closed list rather than echoed: an echo would forward whatever somebody
|
|
34
|
+
* exported — a path, a URL, a campaign string — straight onto the wire, which is the rule this
|
|
35
|
+
* whole contract is built on. Trimmed and lowercased first, because the marker travels through
|
|
36
|
+
* shell snippets and copy-paste, and `Skill_File` is the same channel as `skill_file`.
|
|
37
|
+
*/
|
|
38
|
+
export function resolveInstallSource(env = process.env) {
|
|
39
|
+
const declared = (env[INSTALL_SOURCE_ENV] ?? '').trim().toLowerCase();
|
|
40
|
+
return KNOWN.has(declared) ? declared : InstallSource.UNKNOWN;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* The install source declared by the environment, or undefined when nothing declared one.
|
|
44
|
+
*
|
|
45
|
+
* Distinct from `resolveInstallSource`, which answers `unknown` so an event always carries a value.
|
|
46
|
+
* A config file wants the opposite: writing `unknown` into `.reticle.json` is indistinguishable
|
|
47
|
+
* from a config written before this field existed, and those two are different facts.
|
|
48
|
+
*/
|
|
49
|
+
export function declaredInstallSource(env = process.env) {
|
|
50
|
+
const declared = (env[INSTALL_SOURCE_ENV] ?? '').trim().toLowerCase();
|
|
51
|
+
return KNOWN.has(declared) && InstallSource.UNKNOWN !== declared
|
|
52
|
+
? declared
|
|
53
|
+
: undefined;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* The install source for a project: what its config recorded, else what the environment declares.
|
|
57
|
+
*
|
|
58
|
+
* The config wins because it is the durable answer. The marker is an environment variable set by
|
|
59
|
+
* whichever channel ran the install, so it exists for exactly one command and is gone by the next
|
|
60
|
+
* one — which is why this field reached a handful of events and nothing else, and why the question
|
|
61
|
+
* it exists to answer could not be asked.
|
|
62
|
+
*
|
|
63
|
+
* Narrowed against the closed list on the way out, the same as the env path: a config file is user
|
|
64
|
+
* data, and a hand-edited one must not be able to put an arbitrary string on the wire.
|
|
65
|
+
*/
|
|
66
|
+
export function projectInstallSource(cwd, env = process.env) {
|
|
67
|
+
const recorded = findProjectConfig(cwd)?.['installSource'];
|
|
68
|
+
if ('string' === typeof recorded) {
|
|
69
|
+
const narrowed = recorded.trim().toLowerCase();
|
|
70
|
+
if (KNOWN.has(narrowed))
|
|
71
|
+
return narrowed;
|
|
72
|
+
}
|
|
73
|
+
return resolveInstallSource(env);
|
|
74
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* When a daemon has waited long enough that no app is coming.
|
|
3
|
+
*
|
|
4
|
+
* `app_instrumented` fires on success and only on success, so the population that matters most —
|
|
5
|
+
* daemons that never get an app wired at all — exists in the data only as an absence. An absence is
|
|
6
|
+
* the one thing telemetry cannot interpret: it cannot separate "nothing was ever wired" from "the
|
|
7
|
+
* process was killed before it could say so" from "we never sent the event". The funnel breaks at
|
|
8
|
+
* exactly this step and nowhere else, which makes it the one silence worth converting into a signal.
|
|
9
|
+
*
|
|
10
|
+
* Once per daemon run, and never after an app has connected. A daemon that takes a while and then
|
|
11
|
+
* works reports nothing at all: the question is "did this run ever get an app", and a run that did
|
|
12
|
+
* is not stalled no matter how long it took.
|
|
13
|
+
*
|
|
14
|
+
* The threshold is a judgement, not a measurement, and it is deliberately generous. Someone running
|
|
15
|
+
* `init` and then going to make coffee has not hit a defect, and calling that a stall would drown
|
|
16
|
+
* the real signal in ordinary human latency.
|
|
17
|
+
*
|
|
18
|
+
* Read the result against `agentAttached`, which is what makes it actionable: a stalled daemon with
|
|
19
|
+
* no agent is somebody who installed and walked away, and is not a defect. A stalled daemon WITH an
|
|
20
|
+
* agent attached is the failure the product actually has — the tools are loaded, something is asking
|
|
21
|
+
* for them, and there is nothing on the other end to drive.
|
|
22
|
+
*/
|
|
23
|
+
/**
|
|
24
|
+
* How long a daemon may sit with no app before it counts as stalled.
|
|
25
|
+
*
|
|
26
|
+
* Ten minutes sits well past setup latency — reading a doc, restarting a dev server, opening a tab —
|
|
27
|
+
* and well short of the half-hour idle windows that dominate the field, so a stall is reported while
|
|
28
|
+
* the session is still alive rather than inferred from its corpse.
|
|
29
|
+
*/
|
|
30
|
+
export declare const STALL_AFTER_MS: number;
|
|
31
|
+
/** Start the clock. Called from the same place the instrumentation clock is marked. */
|
|
32
|
+
export declare function markStallClock(now: number): void;
|
|
33
|
+
export interface StallFacts {
|
|
34
|
+
/** Whether this project has a projectId stamped — i.e. `init` has run here. */
|
|
35
|
+
initialized: boolean;
|
|
36
|
+
/** Whether an MCP client is attached right now. */
|
|
37
|
+
agentAttached: boolean;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Report a stalled install, at most once per daemon run.
|
|
41
|
+
*
|
|
42
|
+
* Returns whether it emitted, so the caller's test can assert the once-only rule without reaching
|
|
43
|
+
* into the telemetry client. Best-effort like every other metric here: it must never be the reason
|
|
44
|
+
* a daemon misbehaves.
|
|
45
|
+
*/
|
|
46
|
+
export declare function reportInstrumentationStall(facts: StallFacts, now?: () => number, opts?: {
|
|
47
|
+
atShutdown?: boolean;
|
|
48
|
+
}): boolean;
|
|
49
|
+
/** Tests only. */
|
|
50
|
+
export declare function resetInstrumentationStall(): void;
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* When a daemon has waited long enough that no app is coming.
|
|
3
|
+
*
|
|
4
|
+
* `app_instrumented` fires on success and only on success, so the population that matters most —
|
|
5
|
+
* daemons that never get an app wired at all — exists in the data only as an absence. An absence is
|
|
6
|
+
* the one thing telemetry cannot interpret: it cannot separate "nothing was ever wired" from "the
|
|
7
|
+
* process was killed before it could say so" from "we never sent the event". The funnel breaks at
|
|
8
|
+
* exactly this step and nowhere else, which makes it the one silence worth converting into a signal.
|
|
9
|
+
*
|
|
10
|
+
* Once per daemon run, and never after an app has connected. A daemon that takes a while and then
|
|
11
|
+
* works reports nothing at all: the question is "did this run ever get an app", and a run that did
|
|
12
|
+
* is not stalled no matter how long it took.
|
|
13
|
+
*
|
|
14
|
+
* The threshold is a judgement, not a measurement, and it is deliberately generous. Someone running
|
|
15
|
+
* `init` and then going to make coffee has not hit a defect, and calling that a stall would drown
|
|
16
|
+
* the real signal in ordinary human latency.
|
|
17
|
+
*
|
|
18
|
+
* Read the result against `agentAttached`, which is what makes it actionable: a stalled daemon with
|
|
19
|
+
* no agent is somebody who installed and walked away, and is not a defect. A stalled daemon WITH an
|
|
20
|
+
* agent attached is the failure the product actually has — the tools are loaded, something is asking
|
|
21
|
+
* for them, and there is nothing on the other end to drive.
|
|
22
|
+
*/
|
|
23
|
+
import { TelemetryEventKind } from '@reticlehq/core';
|
|
24
|
+
import { getTelemetry } from './telemetry.js';
|
|
25
|
+
import { appEverConnected } from './app-instrumented.js';
|
|
26
|
+
/**
|
|
27
|
+
* How long a daemon may sit with no app before it counts as stalled.
|
|
28
|
+
*
|
|
29
|
+
* Ten minutes sits well past setup latency — reading a doc, restarting a dev server, opening a tab —
|
|
30
|
+
* and well short of the half-hour idle windows that dominate the field, so a stall is reported while
|
|
31
|
+
* the session is still alive rather than inferred from its corpse.
|
|
32
|
+
*/
|
|
33
|
+
export const STALL_AFTER_MS = 10 * 60 * 1000;
|
|
34
|
+
let reported = false;
|
|
35
|
+
let daemonStartedAt;
|
|
36
|
+
/** Start the clock. Called from the same place the instrumentation clock is marked. */
|
|
37
|
+
export function markStallClock(now) {
|
|
38
|
+
daemonStartedAt = now;
|
|
39
|
+
reported = false;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Report a stalled install, at most once per daemon run.
|
|
43
|
+
*
|
|
44
|
+
* Returns whether it emitted, so the caller's test can assert the once-only rule without reaching
|
|
45
|
+
* into the telemetry client. Best-effort like every other metric here: it must never be the reason
|
|
46
|
+
* a daemon misbehaves.
|
|
47
|
+
*/
|
|
48
|
+
export function reportInstrumentationStall(facts, now = () => Date.now(), opts = {}) {
|
|
49
|
+
if (reported)
|
|
50
|
+
return false;
|
|
51
|
+
// An app arrived. Whatever this run was, it was not stalled.
|
|
52
|
+
if (appEverConnected())
|
|
53
|
+
return false;
|
|
54
|
+
if (daemonStartedAt === undefined)
|
|
55
|
+
return false;
|
|
56
|
+
const waited = now() - daemonStartedAt;
|
|
57
|
+
// The threshold is a guard against crying stall seconds after boot. At SHUTDOWN there is nothing
|
|
58
|
+
// left to wait for: this daemon ran, no app ever connected, and it is now gone. That is a fact
|
|
59
|
+
// rather than an inference, and it does not become truer at ten minutes than at five.
|
|
60
|
+
//
|
|
61
|
+
// Skipping it here is what makes the event able to see anything at all. An UNATTENDED daemon
|
|
62
|
+
// idle-exits at DAEMON_IDLE_SHUTDOWN_MS (5 min), well short of STALL_AFTER_MS (10) — only an
|
|
63
|
+
// attached one gets the six-times grace that reaches the threshold. So on the periodic path alone
|
|
64
|
+
// `agentAttached` was forced to true on every row, and the bucket this event exists for —
|
|
65
|
+
// installed it, walked away, never wired the app — could not appear in the data at all.
|
|
66
|
+
//
|
|
67
|
+
// `msWaited` still carries the duration, so a consumer that wants to exclude short runs can do it
|
|
68
|
+
// on the data instead of being unable to see them.
|
|
69
|
+
if (true !== opts.atShutdown && waited < STALL_AFTER_MS)
|
|
70
|
+
return false;
|
|
71
|
+
reported = true;
|
|
72
|
+
try {
|
|
73
|
+
void getTelemetry().emit(TelemetryEventKind.INSTRUMENTATION_STALLED, {
|
|
74
|
+
stall: {
|
|
75
|
+
initialized: facts.initialized,
|
|
76
|
+
agentAttached: facts.agentAttached,
|
|
77
|
+
msWaited: Math.max(0, waited),
|
|
78
|
+
},
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
/* never let a metric interfere with a running daemon */
|
|
83
|
+
}
|
|
84
|
+
return true;
|
|
85
|
+
}
|
|
86
|
+
/** Tests only. */
|
|
87
|
+
export function resetInstrumentationStall() {
|
|
88
|
+
reported = false;
|
|
89
|
+
daemonStartedAt = undefined;
|
|
90
|
+
}
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* finished, already-classified verdict, so this cannot double-count and does not need to understand
|
|
11
11
|
* how a replay works.
|
|
12
12
|
*/
|
|
13
|
-
import { BugSource, RunFlowStatus, TelemetryActor, TelemetryEventKind, VerdictStatus, } from '@reticlehq/core';
|
|
13
|
+
import { BugAttribution, BugSource, RunFlowStatus, TelemetryActor, TelemetryEventKind, VerdictStatus, } from '@reticlehq/core';
|
|
14
14
|
import { getSessionMetrics } from './session-metrics.js';
|
|
15
15
|
import { getTelemetry } from './telemetry.js';
|
|
16
16
|
/** The tool name reported for this path, so CI runs are distinguishable from agent-driven ones. */
|
|
@@ -52,6 +52,9 @@ export function reportRunTelemetry(run) {
|
|
|
52
52
|
falseGreen: false,
|
|
53
53
|
tool: VERIFY_SURFACE,
|
|
54
54
|
repeat: !first,
|
|
55
|
+
// A regression SOMEWHERE, and the row does not say where — the app changed, or a selector
|
|
56
|
+
// strategy of ours did. `app` needs positive evidence; see bug-found.ts.
|
|
57
|
+
attribution: BugAttribution.UNCLASSIFIED,
|
|
55
58
|
},
|
|
56
59
|
});
|
|
57
60
|
}
|
|
@@ -130,6 +130,14 @@ export declare class SessionMetrics {
|
|
|
130
130
|
* false so a session killed before it can exit is not lost entirely (see the flush note in cli.ts).
|
|
131
131
|
*/
|
|
132
132
|
summarize(final: boolean, exit?: SessionSummary['exit']): SessionSummary;
|
|
133
|
+
/**
|
|
134
|
+
* Has an agent ever attached to this daemon?
|
|
135
|
+
*
|
|
136
|
+
* Read by the instrumentation-stall check, which needs to tell two very different silences apart:
|
|
137
|
+
* a daemon nobody is using, and a daemon with an agent sitting on it that has no app to drive.
|
|
138
|
+
* The second is the funnel's failure case and the first is not a problem at all.
|
|
139
|
+
*/
|
|
140
|
+
get agentEverAttached(): boolean;
|
|
133
141
|
/** True when nothing at all happened — a flush of an idle daemon is not worth an event. */
|
|
134
142
|
get empty(): boolean;
|
|
135
143
|
/** Zero the counters after a non-final flush so the next flush reports the NEXT window, not a total. */
|
|
@@ -515,6 +515,11 @@ export class SessionMetrics {
|
|
|
515
515
|
: {}),
|
|
516
516
|
...(this.#surface === undefined ? {} : { surface: this.#surface }),
|
|
517
517
|
...(final ? { endReason: this.#endReason() } : {}),
|
|
518
|
+
// The headline metric, as a FIELD. Read off the lifetime counter, because every other
|
|
519
|
+
// verification number in this payload is windowed and a reader who summed the wrong one got a
|
|
520
|
+
// different question's answer. `false` is sent, not omitted: a session that drove an app and
|
|
521
|
+
// never asked whether it worked is the whole finding.
|
|
522
|
+
...(final ? { endedWithVerdict: this.#lifetimeVerifications > 0 } : {}),
|
|
518
523
|
...(this.#feedbackPrompted > 0 ? { feedbackPrompted: this.#feedbackPrompted } : {}),
|
|
519
524
|
...(this.#errorClasses.size > 0
|
|
520
525
|
? { errorClasses: Object.fromEntries(this.#errorClasses) }
|
|
@@ -567,6 +572,16 @@ export class SessionMetrics {
|
|
|
567
572
|
return 'verified';
|
|
568
573
|
return 'explored';
|
|
569
574
|
}
|
|
575
|
+
/**
|
|
576
|
+
* Has an agent ever attached to this daemon?
|
|
577
|
+
*
|
|
578
|
+
* Read by the instrumentation-stall check, which needs to tell two very different silences apart:
|
|
579
|
+
* a daemon nobody is using, and a daemon with an agent sitting on it that has no app to drive.
|
|
580
|
+
* The second is the funnel's failure case and the first is not a problem at all.
|
|
581
|
+
*/
|
|
582
|
+
get agentEverAttached() {
|
|
583
|
+
return this.#clients.size > 0;
|
|
584
|
+
}
|
|
570
585
|
/** True when nothing at all happened — a flush of an idle daemon is not worth an event. */
|
|
571
586
|
get empty() {
|
|
572
587
|
return (0 === this.#toolCalls &&
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { TelemetryEventKind, ProjectIdSource, type Crash, type Feedback, type Identity, type BugFound, type InitOutcome, type McpConnection, type McpOutage, type AppInstrumentation, type ProjectProfile, type SessionSummary, type TelemetryActor, type Verification, type VersionChange } from '@reticlehq/core';
|
|
1
|
+
import { TelemetryEventKind, ProjectIdSource, type Crash, type Feedback, type Identity, type BugFound, type InitOutcome, type InstallSource, type McpConnection, type McpOutage, type ToolRefusal, type AppInstrumentation, type InstrumentationStall, type ProjectProfile, type SessionSummary, type TelemetryActor, type Verification, type VersionChange } from '@reticlehq/core';
|
|
2
2
|
/**
|
|
3
3
|
* Feedback gets its own, much larger budget — and a retry.
|
|
4
4
|
*
|
|
@@ -72,10 +72,16 @@ export interface TelemetryExtra {
|
|
|
72
72
|
init?: InitOutcome;
|
|
73
73
|
/** `bug_found`: one defect Reticle found in the app under test. */
|
|
74
74
|
bug?: BugFound;
|
|
75
|
+
/** `tool_refused`: a call Reticle could not serve, and why. */
|
|
76
|
+
refusal?: ToolRefusal;
|
|
75
77
|
/** `mcp_connection_lost`: the agent lost its tools — which stage, and why. */
|
|
76
78
|
outage?: McpOutage;
|
|
77
79
|
/** `app_instrumented`: an app carrying the SDK reached this daemon for the first time. */
|
|
78
80
|
instrumentation?: AppInstrumentation;
|
|
81
|
+
/** `instrumentation_stalled`: the daemon waited and no app ever arrived. */
|
|
82
|
+
stall?: InstrumentationStall;
|
|
83
|
+
/** `reticle_installed` / `init_completed`: which published route brought this install in. */
|
|
84
|
+
installSource?: InstallSource;
|
|
79
85
|
}
|
|
80
86
|
export interface Telemetry {
|
|
81
87
|
/**
|
|
@@ -70,7 +70,9 @@ const DETACHED_SEND_SCRIPT = "fetch(process.argv[1],{method:'POST',headers:{'con
|
|
|
70
70
|
'.catch(()=>{}).finally(()=>process.exit(0))';
|
|
71
71
|
/** Env var names — mirror cloud-sync's `RETICLE_*` convention. */
|
|
72
72
|
import { isReticleSourceCheckout } from './dev-repo.js';
|
|
73
|
+
import { projectInstallSource } from './install-source.js';
|
|
73
74
|
import { gitFacts } from './git-facts.js';
|
|
75
|
+
import { currentAutomationHint } from './automation-hint.js';
|
|
74
76
|
const Env = {
|
|
75
77
|
DISABLE: 'RETICLE_TELEMETRY', // "0" / "false" / "off" → disabled
|
|
76
78
|
DO_NOT_TRACK: 'DO_NOT_TRACK', // the cross-tool opt-out convention (any truthy value)
|
|
@@ -200,7 +202,7 @@ export const describeTelemetry = (env = process.env, dir = RETICLE_DIR, cwd = pr
|
|
|
200
202
|
if (isReticleSourceCheckout(cwd)) {
|
|
201
203
|
return {
|
|
202
204
|
enabled: false,
|
|
203
|
-
reason: 'this is a Reticle source checkout
|
|
205
|
+
reason: 'this is a Reticle source checkout; developing it is not using it',
|
|
204
206
|
policyUrl: POLICY_URL,
|
|
205
207
|
};
|
|
206
208
|
}
|
|
@@ -259,6 +261,9 @@ export const createTelemetry = (opts) => {
|
|
|
259
261
|
const doFetch = opts.fetchImpl ?? fetch;
|
|
260
262
|
const url = `${(env[Env.URL] ?? DEFAULT_URL).replace(/\/+$/, '')}${TELEMETRY_PATH}`;
|
|
261
263
|
const ci = env[Env.CI] !== undefined && env[Env.CI] !== '';
|
|
264
|
+
// Read once per process, next to `ci` and never instead of it: this is a second angle on the same
|
|
265
|
+
// question, and the rule that it can never filter a row lives in automation-hint.ts.
|
|
266
|
+
const automation = currentAutomationHint(env);
|
|
262
267
|
const os = platform();
|
|
263
268
|
// Negated so it reads the way people say it: UTC+2 is +120, not -120.
|
|
264
269
|
const tzOffsetMin = -new Date().getTimezoneOffset();
|
|
@@ -270,6 +275,12 @@ export const createTelemetry = (opts) => {
|
|
|
270
275
|
*/
|
|
271
276
|
const sessionId = randomUUID();
|
|
272
277
|
const { projectId, source: projectIdSource } = projectFingerprint(opts.cwd ?? process.cwd());
|
|
278
|
+
// Resolved ONCE, beside projectId, and for the same reason: it is a property of this install, not
|
|
279
|
+
// of any one event. It used to be passed per-call and therefore rode only `reticle_installed` and
|
|
280
|
+
// `init_completed` — two of the rarest events there are — so the channel that brought a user in
|
|
281
|
+
// was unknown for every event that could show whether they got anywhere. Reading it here puts it
|
|
282
|
+
// on all of them, which is the only shape in which the question can be asked.
|
|
283
|
+
const installSource = projectInstallSource(opts.cwd ?? process.cwd());
|
|
273
284
|
showNoticeOnce();
|
|
274
285
|
const spawnDetached = opts.spawnImpl ??
|
|
275
286
|
((command, args) => {
|
|
@@ -300,6 +311,9 @@ export const createTelemetry = (opts) => {
|
|
|
300
311
|
ts: now(),
|
|
301
312
|
version: opts.version,
|
|
302
313
|
ci,
|
|
314
|
+
// A SCALAR, so it needs the event build and the wire schema and no `blocks` entry — the same
|
|
315
|
+
// three-place trap `outage` fell into, and `installSource` documents below.
|
|
316
|
+
...(automation !== undefined ? { automation } : {}),
|
|
303
317
|
os,
|
|
304
318
|
tzOffsetMin,
|
|
305
319
|
projectIdSource,
|
|
@@ -316,15 +330,23 @@ export const createTelemetry = (opts) => {
|
|
|
316
330
|
...(extra?.connection !== undefined ? { connection: extra.connection } : {}),
|
|
317
331
|
...(extra?.init !== undefined ? { init: extra.init } : {}),
|
|
318
332
|
...(extra?.bug !== undefined ? { bug: extra.bug } : {}),
|
|
333
|
+
...(extra?.refusal !== undefined ? { refusal: extra.refusal } : {}),
|
|
319
334
|
...(extra?.outage !== undefined ? { outage: extra.outage } : {}),
|
|
320
335
|
...(extra?.instrumentation !== undefined ? { instrumentation: extra.instrumentation } : {}),
|
|
336
|
+
...(extra?.stall !== undefined ? { stall: extra.stall } : {}),
|
|
337
|
+
// A SCALAR, so it needs the event build and the wire schema but no entry in `blocks` below —
|
|
338
|
+
// there is nothing to flatten. It still needs both of those, which is the trap `outage` fell
|
|
339
|
+
// into: a field declared on TelemetryExtra and missing from either one is dropped in silence.
|
|
340
|
+
// Always present now, rather than only when a caller remembered to pass it. A per-call
|
|
341
|
+
// override still wins so a command that genuinely knows better can say so.
|
|
342
|
+
installSource: extra?.installSource ?? installSource,
|
|
321
343
|
};
|
|
322
344
|
// Map the core contract onto PostHog's capture shape: id/name/time move up, the rest are properties.
|
|
323
345
|
// The feedback body is FLATTENED into `feedback_*` properties rather than sent as a nested object:
|
|
324
346
|
// PostHog filters and breakdowns operate on top-level properties, so a nested `feedback.stack`
|
|
325
347
|
// would be invisible to the exact "which stacks report the most bugs" question this exists to
|
|
326
348
|
// answer. One prefix keeps them grouped in the UI without a nested path.
|
|
327
|
-
const { anonymousId: distinctId, event: name, ts, sessionId: eventSessionId, feedback, session, project, verification, versionChange, crash, identity, connection, init, bug, outage, instrumentation, ...rest } = event;
|
|
349
|
+
const { anonymousId: distinctId, event: name, ts, sessionId: eventSessionId, feedback, session, project, verification, versionChange, crash, identity, connection, init, bug, refusal, outage, instrumentation, stall, ...rest } = event;
|
|
328
350
|
// `$session_id` is PostHog's OWN session property, so sending ours under that name lights up
|
|
329
351
|
// its built-in session views and funnels for free rather than requiring custom HogQL everywhere.
|
|
330
352
|
// `sessionId` rides only on events that happened inside a daemon run. A one-shot CLI command
|
|
@@ -352,8 +374,10 @@ export const createTelemetry = (opts) => {
|
|
|
352
374
|
connection,
|
|
353
375
|
init,
|
|
354
376
|
bug,
|
|
377
|
+
refusal,
|
|
355
378
|
outage,
|
|
356
379
|
instrumentation,
|
|
380
|
+
stall,
|
|
357
381
|
};
|
|
358
382
|
for (const [prefix, block] of Object.entries(blocks)) {
|
|
359
383
|
for (const [key, value] of Object.entries(block ?? {})) {
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reporting the calls Reticle could not serve.
|
|
3
|
+
*
|
|
4
|
+
* The refusal path is the largest thing this product does and the least measured one. It computes a
|
|
5
|
+
* precise diagnosis — which of three no-session situations this is, that a ref went stale, that a
|
|
6
|
+
* predicate did not parse — hands it to the agent as prose, and throws it away. So a user who
|
|
7
|
+
* attaches an agent, hits a wall on the first call and never comes back emits nothing at all, and
|
|
8
|
+
* the whole cohort is visible only as the gap between two other numbers. See issue #172.
|
|
9
|
+
*
|
|
10
|
+
* Emitted from `runTool`, which is the one place both dispatch paths cross, so a tool added later is
|
|
11
|
+
* covered without anybody remembering to cover it.
|
|
12
|
+
*/
|
|
13
|
+
import { type RefusalReason } from '@reticlehq/core';
|
|
14
|
+
/** A call that was served. Breaks the retry chain: what follows it is not a retry of anything. */
|
|
15
|
+
export declare function noteToolServed(): void;
|
|
16
|
+
/** One refused call, reported with its cause and whether it was itself a retry. */
|
|
17
|
+
export declare function reportToolRefused(tool: string, reason: RefusalReason): void;
|
|
18
|
+
/** Tests only — the counters are process-lifetime, so each case has to start from zero. */
|
|
19
|
+
export declare function resetToolRefusals(): void;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reporting the calls Reticle could not serve.
|
|
3
|
+
*
|
|
4
|
+
* The refusal path is the largest thing this product does and the least measured one. It computes a
|
|
5
|
+
* precise diagnosis — which of three no-session situations this is, that a ref went stale, that a
|
|
6
|
+
* predicate did not parse — hands it to the agent as prose, and throws it away. So a user who
|
|
7
|
+
* attaches an agent, hits a wall on the first call and never comes back emits nothing at all, and
|
|
8
|
+
* the whole cohort is visible only as the gap between two other numbers. See issue #172.
|
|
9
|
+
*
|
|
10
|
+
* Emitted from `runTool`, which is the one place both dispatch paths cross, so a tool added later is
|
|
11
|
+
* covered without anybody remembering to cover it.
|
|
12
|
+
*/
|
|
13
|
+
import { TelemetryActor, TelemetryEventKind } from '@reticlehq/core';
|
|
14
|
+
import { getTelemetry } from './telemetry.js';
|
|
15
|
+
/**
|
|
16
|
+
* Refusal events one daemon run will send.
|
|
17
|
+
*
|
|
18
|
+
* Volume is part of this taxonomy's design — the per-tool-call event was removed for exactly this
|
|
19
|
+
* reason — and a stuck agent retrying one failing call is the shape most likely to produce hundreds.
|
|
20
|
+
* The cap means the bill cannot scale with the pathology while the SHAPE of it still arrives: the
|
|
21
|
+
* first fifty carry the tool, the cause and the retry flag, and `consecutiveRepeats` on the session
|
|
22
|
+
* summary already reports how long the loop ran.
|
|
23
|
+
*/
|
|
24
|
+
const MAX_REFUSALS_PER_SESSION = 50;
|
|
25
|
+
let sent = 0;
|
|
26
|
+
/**
|
|
27
|
+
* The tool whose call refused immediately before this one, if the last call refused at all.
|
|
28
|
+
*
|
|
29
|
+
* This is what `retried` is computed from, and it is why the event is sent at the moment of the
|
|
30
|
+
* refusal rather than held back until the next call reveals whether one came. Holding it back would
|
|
31
|
+
* lose the event entirely for an agent that gives up — which is precisely the agent this exists to
|
|
32
|
+
* describe — so the flag lands on the RETRY instead of on the first refusal.
|
|
33
|
+
*/
|
|
34
|
+
let lastRefusedTool;
|
|
35
|
+
/** A call that was served. Breaks the retry chain: what follows it is not a retry of anything. */
|
|
36
|
+
export function noteToolServed() {
|
|
37
|
+
lastRefusedTool = undefined;
|
|
38
|
+
}
|
|
39
|
+
/** One refused call, reported with its cause and whether it was itself a retry. */
|
|
40
|
+
export function reportToolRefused(tool, reason) {
|
|
41
|
+
const retried = lastRefusedTool === tool;
|
|
42
|
+
lastRefusedTool = tool;
|
|
43
|
+
if (sent >= MAX_REFUSALS_PER_SESSION)
|
|
44
|
+
return;
|
|
45
|
+
sent += 1;
|
|
46
|
+
void getTelemetry().emit(TelemetryEventKind.TOOL_REFUSED, {
|
|
47
|
+
actor: TelemetryActor.AGENT,
|
|
48
|
+
refusal: { tool, reason, retried },
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
/** Tests only — the counters are process-lifetime, so each case has to start from zero. */
|
|
52
|
+
export function resetToolRefusals() {
|
|
53
|
+
sent = 0;
|
|
54
|
+
lastRefusedTool = undefined;
|
|
55
|
+
}
|
|
@@ -53,6 +53,38 @@ const SuiteStatus = {
|
|
|
53
53
|
PASS: 'pass',
|
|
54
54
|
FAIL: 'fail',
|
|
55
55
|
};
|
|
56
|
+
/**
|
|
57
|
+
* Did the assertion itself pass, across three tool shapes that spell it three ways.
|
|
58
|
+
*
|
|
59
|
+
* `reticle_assert` spreads its verdict at the top level, so `pass` is right there. `flow_verify`
|
|
60
|
+
* reports a suite `status`. **`reticle_act_and_wait` nests it under `verdict`**, and that omission is
|
|
61
|
+
* why `falseGreenCaught` had never once been true in the field: `passed` came back `undefined` for
|
|
62
|
+
* every act_and_wait, so `true === passed` was false whatever the app did, on the one tool the
|
|
63
|
+
* product tells agents to reach for first.
|
|
64
|
+
*
|
|
65
|
+
* The metric it feeds is the product's central claim reduced to a boolean, and it was structurally
|
|
66
|
+
* unable to fire on the flagship path. Worse, the test file pinned that: one case used the real
|
|
67
|
+
* nested shape and asserted `falseGreenCaught: false`, directly beneath a comment calling this case
|
|
68
|
+
* "the thesis", while the case that asserted `true` used a flat shape act_and_wait never emits.
|
|
69
|
+
*
|
|
70
|
+
* A permanently-zero metric is indistinguishable from a working one that has nothing to report, so
|
|
71
|
+
* nobody could have noticed this from the data. It took reading the emit path.
|
|
72
|
+
*/
|
|
73
|
+
function passedOf(result, status) {
|
|
74
|
+
if ('boolean' === typeof result['pass'])
|
|
75
|
+
return result['pass'];
|
|
76
|
+
const verdict = result['verdict'];
|
|
77
|
+
if (null !== verdict && 'object' === typeof verdict) {
|
|
78
|
+
const nested = verdict['pass'];
|
|
79
|
+
if ('boolean' === typeof nested)
|
|
80
|
+
return nested;
|
|
81
|
+
}
|
|
82
|
+
if (status === SuiteStatus.PASS)
|
|
83
|
+
return true;
|
|
84
|
+
if (status === SuiteStatus.FAIL)
|
|
85
|
+
return false;
|
|
86
|
+
return undefined;
|
|
87
|
+
}
|
|
56
88
|
/**
|
|
57
89
|
* The verification payload for a result, or undefined when nothing was verified.
|
|
58
90
|
*
|
|
@@ -73,13 +105,7 @@ export function verificationOf(toolName, result, durationMs, brand) {
|
|
|
73
105
|
// metric's convenience — but a status that is neither pass nor fail is NOT a verdict, and an empty
|
|
74
106
|
// suite reports exactly that.
|
|
75
107
|
const status = result['status'];
|
|
76
|
-
const passed =
|
|
77
|
-
? result['pass']
|
|
78
|
-
: status === SuiteStatus.PASS
|
|
79
|
-
? true
|
|
80
|
-
: status === SuiteStatus.FAIL
|
|
81
|
-
? false
|
|
82
|
-
: undefined;
|
|
108
|
+
const passed = passedOf(result, status);
|
|
83
109
|
if (verified === undefined && passed === undefined)
|
|
84
110
|
return undefined;
|
|
85
111
|
const reason = reasonOf(result);
|
|
@@ -88,6 +114,19 @@ export function verificationOf(toolName, result, durationMs, brand) {
|
|
|
88
114
|
via: toolName,
|
|
89
115
|
verified: verified ?? (true === passed ? 'yes' : 'no'),
|
|
90
116
|
passed: passed ?? 'yes' === verified,
|
|
117
|
+
// The assertion passed and Reticle answered `no` anyway: a green actively refuted.
|
|
118
|
+
//
|
|
119
|
+
// Deliberately NOT widened to every refusal of a passing assertion, though several of them are
|
|
120
|
+
// arguably false greens caught too. `decideVerified` refuses a green seven ways and only
|
|
121
|
+
// `contradicted` answers `no`; the rest answer `unknown` on purpose, because UNKNOWN must never
|
|
122
|
+
// collapse into NO. `already_true` is the tempting one: a green that proved nothing because the
|
|
123
|
+
// condition already held before the action, found by a check built for exactly that.
|
|
124
|
+
//
|
|
125
|
+
// It stays out because `reason` is on this same payload. Anyone who wants the wider number can
|
|
126
|
+
// count it without a definition change, and a definition that shifts underneath a metric is
|
|
127
|
+
// worse than one that is narrow, because the old and new series look comparable and are not.
|
|
128
|
+
// Widening it would also have landed in the same release as the fix above, making it impossible
|
|
129
|
+
// to tell how much of any rise was the bug and how much was the redefinition.
|
|
91
130
|
falseGreenCaught: true === passed && 'no' === verified,
|
|
92
131
|
durationMs,
|
|
93
132
|
// Headless CI, a human watching, or somebody's own dev server — three different products behind
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Removing a temp directory on Windows, without inventing a product failure.
|
|
3
|
+
*
|
|
4
|
+
* `rm(dir, { recursive: true, force: true })` is the obvious teardown and it is not enough on
|
|
5
|
+
* Windows. Deletion there is not immediate: a handle the OS, the indexer or a virus scanner still
|
|
6
|
+
* holds keeps the entry alive for a moment after the last close, so the directory is observed
|
|
7
|
+
* non-empty at the instant `rmdir` runs and Node throws `ENOTEMPTY`. `force` does not cover it —
|
|
8
|
+
* that suppresses "missing", not "busy".
|
|
9
|
+
*
|
|
10
|
+
* The failure lands in `afterEach`, so it is reported against the test that just PASSED, and the
|
|
11
|
+
* leftover directory then bleeds state into what runs next. On CI this presented as five failures in
|
|
12
|
+
* one file, four of them consequential and one of them the wonderfully misleading "expected length 1
|
|
13
|
+
* but got 2" — a fake production bug manufactured entirely by teardown. This repo already has that
|
|
14
|
+
* exact story written up in `project-store.test.ts` about a timed-out test's stray writes; this is the
|
|
15
|
+
* same failure arriving through the filesystem instead of through the clock.
|
|
16
|
+
*
|
|
17
|
+
* `maxRetries`/`retryDelay` exist in Node for precisely this, and are documented as Windows-oriented.
|
|
18
|
+
* They are a no-op on POSIX, where the first attempt already succeeds.
|
|
19
|
+
*/
|
|
20
|
+
/**
|
|
21
|
+
* Delete a temp directory tree, tolerating Windows' delayed handle release.
|
|
22
|
+
*
|
|
23
|
+
* Never throws: a temp directory that outlives the test is the operating system's problem, not a
|
|
24
|
+
* reason to fail a run or to report a defect the product does not have. What must not happen is the
|
|
25
|
+
* opposite — a red that says nothing about the code under test.
|
|
26
|
+
*/
|
|
27
|
+
export declare function removeTempDir(dir: string): Promise<void>;
|