@reticlehq/server 2.7.0 → 2.8.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 +64 -29
- package/dist/capsule/causal-summary.d.ts +20 -0
- package/dist/capsule/causal-summary.js +31 -6
- 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 +28 -25
- package/dist/cli/cli-kill.d.ts +58 -0
- package/dist/cli/cli-kill.js +162 -0
- package/dist/cli/cli-launch.d.ts +17 -0
- package/dist/cli/cli-launch.js +28 -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 +8 -0
- package/dist/cli/cli-port.js +16 -0
- package/dist/cli/cli-verify.js +2 -2
- package/dist/cli/doctor-sessions-line.d.ts +12 -0
- package/dist/cli/doctor-sessions-line.js +66 -0
- package/dist/cli/port-holder.d.ts +10 -0
- package/dist/cli/port-holder.js +26 -2
- package/dist/cli.js +99 -14
- 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 +14 -0
- package/dist/events/contradictions.js +26 -17
- package/dist/events/predicate-eval.d.ts +20 -1
- package/dist/events/predicate-eval.js +125 -3
- package/dist/events/predicate.js +170 -18
- package/dist/flows/flow-classify.js +1 -1
- package/dist/flows/flow-tools.js +12 -4
- package/dist/flows/heal-run.js +1 -1
- package/dist/honesty/blind-spots.js +6 -2
- package/dist/honesty/unsettled.d.ts +40 -0
- package/dist/honesty/unsettled.js +78 -0
- package/dist/honesty/verified.d.ts +7 -0
- package/dist/honesty/verified.js +3 -3
- package/dist/index.js +3 -0
- package/dist/init/agent-rules.d.ts +16 -0
- package/dist/init/agent-rules.js +96 -22
- package/dist/init/astro-patch.d.ts +2 -1
- package/dist/init/astro-patch.js +11 -5
- package/dist/init/capabilities.d.ts +22 -0
- package/dist/init/capabilities.js +76 -0
- package/dist/init/confirm.d.ts +96 -0
- package/dist/init/confirm.js +138 -0
- package/dist/init/cra.js +3 -2
- package/dist/init/non-js-project.js +29 -5
- package/dist/init/plan-framework.js +53 -15
- package/dist/init/plan.d.ts +40 -4
- package/dist/init/plan.js +163 -40
- package/dist/init/run.d.ts +24 -2
- package/dist/init/run.js +148 -42
- package/dist/init/slash-command.d.ts +1 -1
- package/dist/init/slash-command.js +46 -6
- package/dist/init/snippets.d.ts +44 -4
- package/dist/init/snippets.js +161 -37
- package/dist/init/vite-config.js +29 -3
- package/dist/input/network-mock-tools.d.ts +19 -0
- package/dist/input/network-mock-tools.js +29 -4
- package/dist/license/license.js +1 -1
- package/dist/mcp/mcp-proxy.d.ts +69 -3
- package/dist/mcp/mcp-proxy.js +205 -20
- package/dist/mcp/mcp.js +23 -7
- package/dist/mcp/proxy-lifecycle.d.ts +1 -1
- package/dist/mcp/proxy-lifecycle.js +1 -1
- 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/session/no-session-diagnosis.d.ts +19 -0
- package/dist/session/no-session-diagnosis.js +71 -18
- package/dist/session/no-session-watch.js +12 -0
- 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/telemetry/automation-hint.d.ts +17 -0
- package/dist/telemetry/automation-hint.js +71 -0
- package/dist/telemetry/bug-found.d.ts +0 -20
- package/dist/telemetry/bug-found.js +38 -21
- package/dist/telemetry/cli-telemetry.js +7 -1
- package/dist/telemetry/feedback.js +1 -1
- package/dist/telemetry/init-telemetry.js +9 -1
- package/dist/telemetry/install-source.d.ts +36 -0
- package/dist/telemetry/install-source.js +40 -0
- package/dist/telemetry/run-telemetry.js +4 -1
- package/dist/telemetry/session-metrics.js +5 -0
- package/dist/telemetry/telemetry.d.ts +5 -1
- package/dist/telemetry/telemetry.js +15 -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 +45 -1
- package/dist/tools/assert-grade.d.ts +1 -1
- package/dist/tools/assert-grade.js +27 -5
- package/dist/tools/assert-verdict.js +18 -1
- package/dist/tools/contract-tools.js +4 -0
- package/dist/tools/dynamic-tools.js +1 -1
- package/dist/tools/error-recovery.d.ts +10 -0
- package/dist/tools/error-recovery.js +64 -4
- package/dist/tools/feedback-invite.js +1 -1
- package/dist/tools/invoke-tool.js +139 -23
- package/dist/tools/lease-tools.d.ts +29 -1
- package/dist/tools/lease-tools.js +58 -5
- package/dist/tools/navigate-result.js +1 -1
- package/dist/tools/observe-tools.js +1 -1
- package/dist/tools/observed-nothing.js +1 -1
- package/dist/tools/react-grace.d.ts +48 -0
- package/dist/tools/react-grace.js +87 -0
- package/dist/tools/read-tools.js +20 -4
- package/dist/tools/settle-in-flight.d.ts +79 -0
- package/dist/tools/settle-in-flight.js +106 -0
- package/dist/tools/snapshot-delta.d.ts +7 -0
- package/dist/tools/snapshot-delta.js +130 -6
- package/dist/tools/tool-surface.js +19 -0
- package/dist/tools/tools.js +16 -4
- package/docs/README.md +56 -11
- package/docs/actions.mdx +87 -0
- package/docs/agent-cheatsheet.md +93 -29
- package/docs/architecture.md +46 -35
- package/docs/benchmarks.md +76 -72
- package/docs/best-practices.mdx +211 -0
- package/docs/capabilities.mdx +387 -0
- package/docs/cli/affected.mdx +51 -0
- package/docs/cli/capsules.mdx +69 -0
- package/docs/cli/cloud.mdx +127 -0
- package/docs/cli/doctor.mdx +76 -0
- package/docs/cli/drive.mdx +55 -0
- package/docs/cli/feedback.mdx +67 -0
- package/docs/cli/gate.mdx +64 -0
- package/docs/cli/help.mdx +92 -0
- package/docs/cli/hunt.mdx +57 -0
- package/docs/cli/identify.mdx +62 -0
- package/docs/cli/init.mdx +94 -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 +68 -0
- package/docs/cli/restart.mdx +60 -0
- package/docs/cli/rollback.mdx +35 -0
- package/docs/cli/serve.mdx +62 -0
- package/docs/cli/status.mdx +64 -0
- package/docs/cli/stop.mdx +56 -0
- package/docs/cli/telemetry.mdx +57 -0
- package/docs/cli/update.mdx +52 -0
- package/docs/cli/verify.mdx +57 -0
- package/docs/cli/version.mdx +39 -0
- package/docs/cli/watch.mdx +55 -0
- package/docs/cli.mdx +219 -0
- package/docs/debugging.md +13 -9
- package/docs/deploy-checks.md +15 -11
- package/docs/desktop-apps.md +49 -40
- package/docs/desktop.mdx +188 -0
- package/docs/docs.json +363 -0
- package/docs/enterprise.md +20 -16
- package/docs/faq.mdx +88 -0
- package/docs/fixtures.md +11 -7
- package/docs/flows.md +26 -22
- package/docs/for-agents.md +71 -0
- package/docs/frameworks.mdx +199 -0
- package/docs/gate-plan.md +45 -41
- package/docs/gates.md +22 -18
- package/docs/getting-started.md +76 -65
- package/docs/human-control.md +22 -18
- package/docs/index.mdx +101 -0
- package/docs/install-agentic.mdx +215 -0
- package/docs/install-manual.mdx +194 -0
- package/docs/instrumentation.mdx +232 -0
- package/docs/integration-patterns.md +38 -34
- package/docs/local-registry.md +16 -12
- package/docs/multi-agent-testing.md +16 -12
- package/docs/packages/babel-plugin.mdx +70 -0
- package/docs/packages/browser.mdx +111 -0
- package/docs/packages/core.mdx +106 -0
- package/docs/packages/electron.mdx +87 -0
- package/docs/packages/eslint-plugin.mdx +84 -0
- package/docs/packages/next.mdx +91 -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 +108 -0
- package/docs/packages/vite-plugin.mdx +96 -0
- package/docs/packages.mdx +217 -0
- package/docs/platform-integration.md +33 -26
- package/docs/predicates.mdx +179 -0
- package/docs/quickstart.mdx +211 -0
- package/docs/recipes.mdx +242 -0
- package/docs/skill-file.mdx +105 -0
- package/docs/state-management.mdx +197 -0
- package/docs/style.css +14 -0
- package/docs/system-map.md +47 -32
- package/docs/telemetry-contract.md +93 -50
- package/docs/telemetry-events.mdx +430 -0
- package/docs/telemetry.md +57 -52
- package/docs/testing.md +10 -6
- package/docs/token-efficiency.md +30 -26
- package/docs/tools-act-and-wait.mdx +123 -0
- package/docs/tools-act-sequence.mdx +120 -0
- package/docs/tools-act.mdx +82 -0
- package/docs/tools-assert.mdx +70 -0
- package/docs/tools-console.mdx +59 -0
- package/docs/tools-inspect.mdx +85 -0
- package/docs/tools-navigate.mdx +63 -0
- package/docs/tools-network.mdx +86 -0
- package/docs/tools-observe.mdx +89 -0
- package/docs/tools-overview.mdx +157 -0
- package/docs/tools-query.mdx +82 -0
- package/docs/tools-session-and-feedback.mdx +91 -0
- package/docs/tools-sessions.mdx +65 -0
- package/docs/tools-snapshot.mdx +75 -0
- package/docs/tools-state.mdx +81 -0
- package/docs/tools-tools-and-run.mdx +125 -0
- package/docs/tools-wait-for.mdx +66 -0
- package/docs/troubleshooting.mdx +193 -0
- package/docs/usage.md +175 -153
- package/docs/vs-chrome-devtools-mcp.mdx +82 -0
- package/docs/vs-playwright-mcp.mdx +85 -0
- package/docs/vs-screenshots.mdx +96 -0
- package/docs/what-is-reticle.mdx +63 -0
- package/docs/why-reticle.mdx +107 -0
- package/package.json +2 -2
- 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
|
@@ -27,6 +27,14 @@ interface NoSessionFacts {
|
|
|
27
27
|
listening: readonly number[];
|
|
28
28
|
/** The port this daemon is on — half of the mismatch the old message asked about. */
|
|
29
29
|
port: number;
|
|
30
|
+
/**
|
|
31
|
+
* The directory `initialized` was decided in, named in the message when it is known.
|
|
32
|
+
*
|
|
33
|
+
* "There is no `.reticle.json`" is only ever true OF A DIRECTORY, and a reader who cannot see
|
|
34
|
+
* which one has to guess whether the claim is about their app at all — reported by someone whose
|
|
35
|
+
* app lives in `src/admin` while the daemon stands at the repo root.
|
|
36
|
+
*/
|
|
37
|
+
directory?: string;
|
|
30
38
|
/**
|
|
31
39
|
* This daemon has reaped at least one EXPIRED pooled lease.
|
|
32
40
|
*
|
|
@@ -34,6 +42,17 @@ interface NoSessionFacts {
|
|
|
34
42
|
* evidence, not proof, and the message below is worded accordingly.
|
|
35
43
|
*/
|
|
36
44
|
leaseExpired?: boolean;
|
|
45
|
+
/**
|
|
46
|
+
* The port `.reticle.json` configures for this project, when there is one.
|
|
47
|
+
*
|
|
48
|
+
* Half of a mismatch the daemon can settle rather than hint at: it knows the port it bound, and
|
|
49
|
+
* this is the number the app's SDK was built to dial. `doctor` already compares them and prints a
|
|
50
|
+
* precise line; the agent reading `why` was getting "check that the port matches" instead.
|
|
51
|
+
*
|
|
52
|
+
* Absent means no config, or one without a port, which is NOT a mismatch: a plugin-wired app has
|
|
53
|
+
* no `.reticle.json` at all and is working perfectly.
|
|
54
|
+
*/
|
|
55
|
+
projectPort?: number;
|
|
37
56
|
}
|
|
38
57
|
/**
|
|
39
58
|
* Every branch ends with this, and `recoveryFor` keys on it to suppress the generic no-session
|
|
@@ -69,9 +69,57 @@ const NON_LOCALHOST_GATE = 'One more cause that leaves every other check healthy
|
|
|
69
69
|
'is given `allowNonLocalhost: true` — the refusal is page-side, so nothing here can see it. ' +
|
|
70
70
|
"Check the browser console for that message and pass the flag in the app's reticle.connect().";
|
|
71
71
|
const SCANNED_PORTS = [...DEV_SERVER_PORTS].join(', ');
|
|
72
|
+
/**
|
|
73
|
+
* The commonest first-run state there is, and until #320 the message named it nowhere.
|
|
74
|
+
*
|
|
75
|
+
* Reticle sees a page only once a browser has LOADED it. A wired app that nobody has opened produces
|
|
76
|
+
* exactly the same empty list as a broken install, and the reporter who hit it spent an afternoon
|
|
77
|
+
* re-verifying their init output, diffing their Vite config and curling their own page before
|
|
78
|
+
* discovering that one `reticle open` fixed it instantly.
|
|
79
|
+
*/
|
|
80
|
+
const OPEN_THE_APP = 'Reticle only ever sees a page that is LOADED, so the commonest cause by a distance is that no ' +
|
|
81
|
+
"browser has opened the app yet: run `reticle open <url>` with the app's own URL (or ask the " +
|
|
82
|
+
'human to open it).';
|
|
83
|
+
/**
|
|
84
|
+
* What a machine-wide port scan can and cannot say.
|
|
85
|
+
*
|
|
86
|
+
* It finds listeners anywhere on localhost and knows nothing about who owns them. The old wording
|
|
87
|
+
* spent that as evidence — "something IS listening on port 5173, 8000, 8080, SO a server is up and
|
|
88
|
+
* has never dialled this daemon" — about three ports that belonged to three other repositories on
|
|
89
|
+
* the reporter's machine, while their own app sat on a port the scan does not cover. On any machine
|
|
90
|
+
* running more than one instrumented repo, which is the normal case here, that inference is unsound.
|
|
91
|
+
*/
|
|
92
|
+
function unattributedListeners(listening) {
|
|
93
|
+
if (0 === listening.length) {
|
|
94
|
+
return (`No listener found that I can attribute to this project: the scan covers ${SCANNED_PORTS} ` +
|
|
95
|
+
'across the whole machine, so an app on any other port is invisible to it.');
|
|
96
|
+
}
|
|
97
|
+
return (`Something is listening on port ${listening.join(', ')}, but I cannot attribute any of it to ` +
|
|
98
|
+
`this project: that is a machine-wide scan of ${SCANNED_PORTS}, so on a machine running more ` +
|
|
99
|
+
"than one repo those are as likely to be somebody else's dev server, and the app's own port " +
|
|
100
|
+
'may not be in the set at all. Treat them as unattributed rather than as evidence.');
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* The mismatch, stated with both numbers, when we can actually see one. Empty otherwise.
|
|
104
|
+
*
|
|
105
|
+
* Leading space so the caller can concatenate it unconditionally without producing a double space
|
|
106
|
+
* in the common case where there is nothing to say.
|
|
107
|
+
*/
|
|
108
|
+
function portMismatchClause(facts) {
|
|
109
|
+
const configured = facts.projectPort;
|
|
110
|
+
if (configured === undefined || configured === facts.port)
|
|
111
|
+
return '';
|
|
112
|
+
return (` They already disagree: \`.reticle.json\` says port ${String(configured)} and this daemon is` +
|
|
113
|
+
` on ${String(facts.port)}, so the SDK is dialling ${String(configured)} and nothing is there.` +
|
|
114
|
+
` Either restart the daemon on ${String(configured)}, or update \`.reticle.json\` to` +
|
|
115
|
+
` ${String(facts.port)} and restart the dev server so the app picks it up.`);
|
|
116
|
+
}
|
|
72
117
|
export function diagnoseNoSession(facts) {
|
|
73
118
|
const { everConnected, initialized, listening, port } = facts;
|
|
74
|
-
|
|
119
|
+
// Named when known: a claim about a missing file is a claim about ONE directory.
|
|
120
|
+
const where = facts.directory === undefined
|
|
121
|
+
? 'the directory this daemon is running in'
|
|
122
|
+
: `the directory this daemon is running in (${facts.directory})`;
|
|
75
123
|
if (everConnected) {
|
|
76
124
|
// A reaped lease first, because it is the one cause we have POSITIVE evidence for. Reported
|
|
77
125
|
// from the field (#157): an aged-out lease produced "the tab was closed … ask the human to
|
|
@@ -80,14 +128,14 @@ export function diagnoseNoSession(facts) {
|
|
|
80
128
|
// went looking for a port mismatch. Hedged rather than swapped: a human tab may ALSO have
|
|
81
129
|
// closed, and this does not know which session went.
|
|
82
130
|
if (true === facts.leaseExpired) {
|
|
83
|
-
return ('no browser session connected
|
|
131
|
+
return ('no browser session connected, but one WAS connected to this daemon earlier, so the wiring ' +
|
|
84
132
|
'is correct. This daemon has expired at least one pooled lease, so the likeliest cause is ' +
|
|
85
133
|
'that a lease you were using aged out; a lease is a headless context, not a human tab, and ' +
|
|
86
134
|
'it takes its cookies with it (so an authenticated app needs signing in again). Re-acquire ' +
|
|
87
135
|
'with reticle_lease {action:"acquire", url} and carry on. If you were driving a human tab ' +
|
|
88
136
|
`instead, it went away — reopen it or run \`reticle open\`. ${RETRY}`);
|
|
89
137
|
}
|
|
90
|
-
return ('no browser session connected
|
|
138
|
+
return ('no browser session connected, but one WAS connected to this daemon earlier, so the wiring ' +
|
|
91
139
|
'is correct. The tab was closed, navigated away, or hard-reloaded. Ask the human to reopen ' +
|
|
92
140
|
`the app (or run \`reticle open\`), or reload the tab. ${SELF_SERVE} ${RETRY}`);
|
|
93
141
|
}
|
|
@@ -111,16 +159,17 @@ export function diagnoseNoSession(facts) {
|
|
|
111
159
|
`(1) Nothing is listening on the ports Reticle scans (${SCANNED_PORTS}), so the dev server ` +
|
|
112
160
|
'may not be running — ask the human to start it (`npm run dev`). That scan is narrow ' +
|
|
113
161
|
'though: a server on any other port is invisible to it, so if the app IS running, ask for ' +
|
|
114
|
-
'its URL rather than assuming it is down
|
|
115
|
-
|
|
162
|
+
'its URL rather than assuming it is down, and open it with `reticle open <url>`. ' +
|
|
163
|
+
`(2) There is no \`.reticle.json\` in ${where}. That is the ` +
|
|
116
164
|
"file `reticle init` writes, so the app may carry no Reticle SDK — but check the app's " +
|
|
117
165
|
'OWN directory before re-running `init`: in a monorepo the daemon often runs at the root ' +
|
|
118
166
|
'while the app lives in a subdirectory, and an app wired by the Vite or Babel plugin ' +
|
|
119
167
|
`carries the SDK without that file at all. ${RETRY}`);
|
|
120
168
|
}
|
|
121
|
-
return (
|
|
122
|
-
|
|
123
|
-
|
|
169
|
+
return (`no browser session connected, and this daemon has never seen one. ${OPEN_THE_APP} ` +
|
|
170
|
+
'Nothing is listening on the ports Reticle scans ' +
|
|
171
|
+
`(${SCANNED_PORTS}) either, and the most common reason for that is a dev server that is not ` +
|
|
172
|
+
'running: ask the human to start it (`npm run dev`), then open the app in a browser. ' +
|
|
124
173
|
// The caveat is here rather than omitted because the scan is NARROW, and the old sentence
|
|
125
174
|
// spent its confidence as though an empty result were proof of absence. Reported twice: a
|
|
126
175
|
// scripted drive of 2.5.0 asserted the app was not running while it served 200 on :7699, and
|
|
@@ -137,15 +186,19 @@ export function diagnoseNoSession(facts) {
|
|
|
137
186
|
`${RETRY}`);
|
|
138
187
|
}
|
|
139
188
|
if (!initialized) {
|
|
140
|
-
return (
|
|
141
|
-
|
|
142
|
-
'
|
|
143
|
-
|
|
144
|
-
|
|
189
|
+
return ('no browser session connected, and this daemon has never seen one. ' +
|
|
190
|
+
`What was actually checked: there is no \`.reticle.json\` in ${where}. That is the file ` +
|
|
191
|
+
'`reticle init` writes, so the app may carry no Reticle SDK — but it is not proof, and the ' +
|
|
192
|
+
'same absence is expected in a monorepo whose daemon runs at the root while the app lives in ' +
|
|
193
|
+
"a subdirectory, or in an app wired by the Vite or Babel plugin. Check the app's OWN " +
|
|
194
|
+
'directory: if it has no config, run `reticle init` there and restart the dev server; if it ' +
|
|
195
|
+
'has one, the app is wired and simply has no page open — `reticle open <url>`. ' +
|
|
196
|
+
`${unattributedListeners(listening)} ${RETRY}`);
|
|
145
197
|
}
|
|
146
|
-
return (
|
|
147
|
-
`wired for Reticle
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
198
|
+
return ('no browser session connected, and this daemon has never seen one for this project, which is ' +
|
|
199
|
+
`wired for Reticle. ${OPEN_THE_APP} ${unattributedListeners(listening)} ` +
|
|
200
|
+
'If the page IS open and still does not appear, the app is either serving a build made before ' +
|
|
201
|
+
`the wiring landed or dialling a different daemon than this one (on ${String(port)}): restart ` +
|
|
202
|
+
"the dev server, hard-reload the page, and check that the app's reticle port matches " +
|
|
203
|
+
`${String(port)}.${portMismatchClause(facts)} ${NON_LOCALHOST_GATE} ${SELF_SERVE} ${RETRY}`);
|
|
151
204
|
}
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
*/
|
|
12
12
|
import { probeDevServers } from './dev-server-probe.js';
|
|
13
13
|
import { diagnoseNoSession } from './no-session-diagnosis.js';
|
|
14
|
+
import { readProjectPort } from '../cli/cli-port.js';
|
|
14
15
|
/** Slow enough to be free, fast enough that a dev server started 15s ago is already reflected. */
|
|
15
16
|
const REFRESH_MS = 15_000;
|
|
16
17
|
/** Start the watch. Returns a stop function; the timer is unref'd so it never holds the daemon up. */
|
|
@@ -42,7 +43,18 @@ function startNoSessionWatch(options) {
|
|
|
42
43
|
initialized: options.initialized,
|
|
43
44
|
listening,
|
|
44
45
|
port: options.port,
|
|
46
|
+
// The directory `initialized` was decided in. Named in the message because "there is no
|
|
47
|
+
// `.reticle.json`" is a claim about ONE directory, and a reader standing somewhere else
|
|
48
|
+
// cannot tell whether it is a claim about their app at all.
|
|
49
|
+
directory: options.directory ?? process.cwd(),
|
|
45
50
|
leaseExpired: (options.reapedLeases?.() ?? 0) > 0,
|
|
51
|
+
// Read here rather than at boot: `.reticle.json` can be written by `init` after this daemon
|
|
52
|
+
// started, which is the ordinary first-install order, and a port cached from before it existed
|
|
53
|
+
// would make the daemon confidently report no mismatch on the one run where there is one.
|
|
54
|
+
...(() => {
|
|
55
|
+
const configured = readProjectPort(options.directory ?? process.cwd());
|
|
56
|
+
return configured === undefined ? {} : { projectPort: configured };
|
|
57
|
+
})(),
|
|
46
58
|
}));
|
|
47
59
|
return () => {
|
|
48
60
|
clearInterval(timer);
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which tab a ref came out of, so the call that spends it cannot be routed to a different one.
|
|
3
|
+
*
|
|
4
|
+
* Reported from the field: three tabs of one project were connected, a snapshot was taken against a
|
|
5
|
+
* named session, and the act that followed omitted `sessionId`. Auto-selection picked a different
|
|
6
|
+
* tab, the ref legitimately did not exist there, and the refusal read "that ref is stale: refs are
|
|
7
|
+
* invalidated whenever the DOM re-renders" — a confident diagnosis of the wrong thing, and one that
|
|
8
|
+
* accuses the app under test. The recovery text then sends the agent to re-snapshot, which mints a
|
|
9
|
+
* fresh ref in whichever tab is chosen next, so the lesson it teaches is wrong either way.
|
|
10
|
+
*
|
|
11
|
+
* The quiet outcome is the worse one. Refs are minted from a per-document block, so the same number
|
|
12
|
+
* is a real, live, DIFFERENT element in the tab that was guessed: the identical call on a luckier day
|
|
13
|
+
* drives the wrong tab and returns a green about a page nobody asked about. That is a false green
|
|
14
|
+
* with extra steps, and it is the failure class this product exists to remove.
|
|
15
|
+
*
|
|
16
|
+
* Every `sessionId` description on the surface already promises that Reticle "refuses rather than
|
|
17
|
+
* guesses when ambiguous". This is the fact that makes the ambiguity visible: the tab a ref was
|
|
18
|
+
* handed out of is known at the moment it is handed out. Resolution is otherwise untouched — a call
|
|
19
|
+
* that names a session, carries no ref, or lands on the tab it looked at resolves silently, which is
|
|
20
|
+
* the cost the surface wording was rewritten to avoid.
|
|
21
|
+
*
|
|
22
|
+
* ponytail: one slot, not a per-ref map. It answers the reported shape — look at one tab, act on
|
|
23
|
+
* what you saw — for the cost of an assignment. A drive that interleaves refs from several tabs
|
|
24
|
+
* inside one turn needs per-ref provenance, and that is the upgrade if it ever shows up.
|
|
25
|
+
*/
|
|
26
|
+
/** Record that a tool handed out refs from this session. */
|
|
27
|
+
export declare function noteRefsMinted(sessionId: string): void;
|
|
28
|
+
export declare function refsLastMintedIn(): string | undefined;
|
|
29
|
+
/** Test seam: the module holds one slot for the process, so a test has to be able to clear it. */
|
|
30
|
+
export declare function forgetRefProvenance(): void;
|
|
31
|
+
interface RefRouting {
|
|
32
|
+
/** The ref this call is about to spend, if it carries one. */
|
|
33
|
+
ref: string | undefined;
|
|
34
|
+
/** The `sessionId` the CALLER passed — when present, nothing was guessed. */
|
|
35
|
+
explicitSessionId: string | undefined;
|
|
36
|
+
/** The session resolution actually picked. */
|
|
37
|
+
chosenSessionId: string;
|
|
38
|
+
/**
|
|
39
|
+
* The connected sessions, read ONLY once a refusal is certain. A thunk because listing them walks
|
|
40
|
+
* every session and builds a health block each time, and the answer is wanted on the rare path.
|
|
41
|
+
*/
|
|
42
|
+
connected: () => readonly {
|
|
43
|
+
sessionId: string;
|
|
44
|
+
url: string;
|
|
45
|
+
}[];
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* The refusal for a ref that belongs to another tab, or undefined when there is no ambiguity.
|
|
49
|
+
*
|
|
50
|
+
* Silent when the minting tab is no longer connected: that ref really is dead, and the stale-ref
|
|
51
|
+
* refusal that already exists is the honest answer for it.
|
|
52
|
+
*/
|
|
53
|
+
export declare function wrongTabRefusal(routing: RefRouting): string | undefined;
|
|
54
|
+
export {};
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which tab a ref came out of, so the call that spends it cannot be routed to a different one.
|
|
3
|
+
*
|
|
4
|
+
* Reported from the field: three tabs of one project were connected, a snapshot was taken against a
|
|
5
|
+
* named session, and the act that followed omitted `sessionId`. Auto-selection picked a different
|
|
6
|
+
* tab, the ref legitimately did not exist there, and the refusal read "that ref is stale: refs are
|
|
7
|
+
* invalidated whenever the DOM re-renders" — a confident diagnosis of the wrong thing, and one that
|
|
8
|
+
* accuses the app under test. The recovery text then sends the agent to re-snapshot, which mints a
|
|
9
|
+
* fresh ref in whichever tab is chosen next, so the lesson it teaches is wrong either way.
|
|
10
|
+
*
|
|
11
|
+
* The quiet outcome is the worse one. Refs are minted from a per-document block, so the same number
|
|
12
|
+
* is a real, live, DIFFERENT element in the tab that was guessed: the identical call on a luckier day
|
|
13
|
+
* drives the wrong tab and returns a green about a page nobody asked about. That is a false green
|
|
14
|
+
* with extra steps, and it is the failure class this product exists to remove.
|
|
15
|
+
*
|
|
16
|
+
* Every `sessionId` description on the surface already promises that Reticle "refuses rather than
|
|
17
|
+
* guesses when ambiguous". This is the fact that makes the ambiguity visible: the tab a ref was
|
|
18
|
+
* handed out of is known at the moment it is handed out. Resolution is otherwise untouched — a call
|
|
19
|
+
* that names a session, carries no ref, or lands on the tab it looked at resolves silently, which is
|
|
20
|
+
* the cost the surface wording was rewritten to avoid.
|
|
21
|
+
*
|
|
22
|
+
* ponytail: one slot, not a per-ref map. It answers the reported shape — look at one tab, act on
|
|
23
|
+
* what you saw — for the cost of an assignment. A drive that interleaves refs from several tabs
|
|
24
|
+
* inside one turn needs per-ref provenance, and that is the upgrade if it ever shows up.
|
|
25
|
+
*/
|
|
26
|
+
/** The session the last ref-bearing result was produced from. Undefined until something mints one. */
|
|
27
|
+
let mintedIn;
|
|
28
|
+
/** Record that a tool handed out refs from this session. */
|
|
29
|
+
export function noteRefsMinted(sessionId) {
|
|
30
|
+
mintedIn = sessionId;
|
|
31
|
+
}
|
|
32
|
+
export function refsLastMintedIn() {
|
|
33
|
+
return mintedIn;
|
|
34
|
+
}
|
|
35
|
+
/** Test seam: the module holds one slot for the process, so a test has to be able to clear it. */
|
|
36
|
+
export function forgetRefProvenance() {
|
|
37
|
+
mintedIn = undefined;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* The refusal for a ref that belongs to another tab, or undefined when there is no ambiguity.
|
|
41
|
+
*
|
|
42
|
+
* Silent when the minting tab is no longer connected: that ref really is dead, and the stale-ref
|
|
43
|
+
* refusal that already exists is the honest answer for it.
|
|
44
|
+
*/
|
|
45
|
+
export function wrongTabRefusal(routing) {
|
|
46
|
+
if (routing.ref === undefined || routing.explicitSessionId !== undefined)
|
|
47
|
+
return undefined;
|
|
48
|
+
const source = mintedIn;
|
|
49
|
+
if (source === undefined || source === routing.chosenSessionId)
|
|
50
|
+
return undefined;
|
|
51
|
+
const connected = routing.connected();
|
|
52
|
+
if (!connected.some((s) => s.sessionId === source))
|
|
53
|
+
return undefined;
|
|
54
|
+
const listed = connected.map((s) => `'${s.sessionId}' (${s.url})`).join(', ');
|
|
55
|
+
return (`ref '${routing.ref}' was handed out by session '${source}', and this call named no sessionId so ` +
|
|
56
|
+
`it resolved to '${routing.chosenSessionId}'. A ref only means something in the tab it was taken ` +
|
|
57
|
+
'in — refs are minted per document, so the same one can point at a different element in another ' +
|
|
58
|
+
'tab — and Reticle refuses rather than guessing which tab you meant. Connected right now: ' +
|
|
59
|
+
`${listed}. Pass sessionId: '${source}' to act on the tab you looked at, or re-read the tab you ` +
|
|
60
|
+
'actually want first. Nothing was driven.');
|
|
61
|
+
}
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
*
|
|
9
9
|
* Any edit here is a per-session tax on every user: say it in as few words as still work.
|
|
10
10
|
*/
|
|
11
|
-
const SESSION_LEASE_NOTICE = 'When you stop driving, call reticle_session {action:"yield", mode:"waiting"|"ask"}
|
|
11
|
+
const SESSION_LEASE_NOTICE = 'When you stop driving, call reticle_session {action:"yield", mode:"waiting"|"ask"}. The panel reads "live" until you do. {action:"end"} when the task is done; it revives on your next action.';
|
|
12
12
|
export function buildSessionLease(sessionId, openedAt) {
|
|
13
13
|
return { sessionId, opened_at: openedAt, IMPORTANT: SESSION_LEASE_NOTICE };
|
|
14
14
|
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { AutomationHint } from '@reticlehq/core';
|
|
2
|
+
export interface AutomationInput {
|
|
3
|
+
env: NodeJS.ProcessEnv;
|
|
4
|
+
fileExists: (path: string) => boolean;
|
|
5
|
+
hasTty: boolean;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* The most specific signal that fires, or undefined.
|
|
9
|
+
*
|
|
10
|
+
* Ordered most specific first: a container inside a Codespace with no terminal is one fact about the
|
|
11
|
+
* run, and sending the weakest of the three would waste the field. Omitted rather than sent as a
|
|
12
|
+
* "none" value, so its presence is itself the signal — the convention every other advisory field
|
|
13
|
+
* here follows.
|
|
14
|
+
*/
|
|
15
|
+
export declare function resolveAutomationHint(input: AutomationInput): AutomationHint | undefined;
|
|
16
|
+
/** The real reading, taken once per process. */
|
|
17
|
+
export declare function currentAutomationHint(env: NodeJS.ProcessEnv): AutomationHint | undefined;
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* An ADVISORY marker that a run looks automated, for the runs `ci` cannot see.
|
|
3
|
+
*
|
|
4
|
+
* `ci` is a single environment variable, which is exactly right for a GitHub Actions runner and
|
|
5
|
+
* blind to everything else: our own gate has run from a datacenter and landed in the user data
|
|
6
|
+
* indistinguishable from somebody at a laptop.
|
|
7
|
+
*
|
|
8
|
+
* ADVISORY, and never a filter. A real person works inside a container, inside a Codespace, and over
|
|
9
|
+
* an ssh session with no terminal attached, and every one of them is a user. Nothing may drop a row
|
|
10
|
+
* because this is set; it exists so a surprising number can be looked at from a second angle, and a
|
|
11
|
+
* hint that starts being used as an exclusion has stopped being a hint. Absent means "nothing here
|
|
12
|
+
* looked automated", never "a human was present".
|
|
13
|
+
*
|
|
14
|
+
* WHAT WAS REJECTED, so nobody re-proposes it:
|
|
15
|
+
* - IP address, ASN or datacenter geolocation. Not knowable client-side, and it is a stronger
|
|
16
|
+
* identifier than anything else we collect. The one signal that would actually separate a cloud
|
|
17
|
+
* sandbox from a laptop is the one we will not collect.
|
|
18
|
+
* - Hostname patterns (`runner-*`, `ip-10-*`). A hostname is user data, and matching names is
|
|
19
|
+
* guesswork dressed as a measurement — a guessed dimension and a measured one are
|
|
20
|
+
* indistinguishable once they are in the same column.
|
|
21
|
+
* - Process ancestry. Costs a spawn on a path that must never tax a command, and reads the name of
|
|
22
|
+
* somebody else's program to classify our own user.
|
|
23
|
+
* - `npm_config_user_agent`. Says which package manager ran us, which every route shares.
|
|
24
|
+
* - An absent `$DISPLAY`. Every Linux server, every ssh session, and a large share of genuine
|
|
25
|
+
* developer machines trip it; it separates nothing.
|
|
26
|
+
*/
|
|
27
|
+
import { existsSync } from 'node:fs';
|
|
28
|
+
import { AutomationHint } from '@reticlehq/core';
|
|
29
|
+
/** Container runtimes drop one of these at the filesystem root. */
|
|
30
|
+
const CONTAINER_MARKERS = ['/.dockerenv', '/run/.containerenv'];
|
|
31
|
+
/** Hosted dev environments that declare themselves. Self-declared, never inferred. */
|
|
32
|
+
const HOSTED_WORKSPACE_ENV = [
|
|
33
|
+
'CODESPACES',
|
|
34
|
+
'GITPOD_WORKSPACE_ID',
|
|
35
|
+
'CLOUD_SHELL',
|
|
36
|
+
'REMOTE_CONTAINERS',
|
|
37
|
+
];
|
|
38
|
+
/**
|
|
39
|
+
* The most specific signal that fires, or undefined.
|
|
40
|
+
*
|
|
41
|
+
* Ordered most specific first: a container inside a Codespace with no terminal is one fact about the
|
|
42
|
+
* run, and sending the weakest of the three would waste the field. Omitted rather than sent as a
|
|
43
|
+
* "none" value, so its presence is itself the signal — the convention every other advisory field
|
|
44
|
+
* here follows.
|
|
45
|
+
*/
|
|
46
|
+
export function resolveAutomationHint(input) {
|
|
47
|
+
if (CONTAINER_MARKERS.some((m) => input.fileExists(m)))
|
|
48
|
+
return AutomationHint.CONTAINER;
|
|
49
|
+
if (HOSTED_WORKSPACE_ENV.some((k) => (input.env[k] ?? '') !== ''))
|
|
50
|
+
return AutomationHint.HOSTED_WORKSPACE;
|
|
51
|
+
if (!input.hasTty)
|
|
52
|
+
return AutomationHint.NO_TTY;
|
|
53
|
+
return undefined;
|
|
54
|
+
}
|
|
55
|
+
/** The real reading, taken once per process. */
|
|
56
|
+
export function currentAutomationHint(env) {
|
|
57
|
+
return resolveAutomationHint({
|
|
58
|
+
env,
|
|
59
|
+
fileExists: (p) => {
|
|
60
|
+
try {
|
|
61
|
+
return existsSync(p);
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
return false; // an unreadable root is not evidence of anything
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
// Both ends, because a piped stdout alone is how `reticle status | jq` looks, and that is a
|
|
68
|
+
// person typing.
|
|
69
|
+
hasTty: true === process.stdout.isTTY || true === process.stdin.isTTY,
|
|
70
|
+
});
|
|
71
|
+
}
|
|
@@ -6,24 +6,4 @@ import { type BugFound } from '@reticlehq/core';
|
|
|
6
6
|
* nothing about sessions and should stay that way. The emission site owns the dedup flag.
|
|
7
7
|
*/
|
|
8
8
|
export type BugCandidate = Omit<BugFound, 'repeat'>;
|
|
9
|
-
/**
|
|
10
|
-
* `bug.attribution` was REMOVED, deliberately, and this note is here so it is not re-added casually.
|
|
11
|
-
*
|
|
12
|
-
* It shipped twice and was wrong both times. First it stamped `app` on every contradiction, including
|
|
13
|
-
* `request-never-settled` — which Reticle raises from the ABSENCE of a settle event, so a dev-overlay
|
|
14
|
-
* poll manufactured it with the app doing nothing wrong. That was fixed, and a real drive then found
|
|
15
|
-
* it still inverted: across two full runs, EVERY `attribution: 'app'` was a misattribution, while the
|
|
16
|
-
* one defect that genuinely was a bad agent predicate carried none. One session would have published
|
|
17
|
-
* "2 defects in the app" against a true count of 0.
|
|
18
|
-
*
|
|
19
|
-
* A metric that is confidently wrong about WHOSE fault a defect is, is worse than no metric: it is
|
|
20
|
-
* the number a founder steers on, and it points at the customer. Counting nothing is recoverable;
|
|
21
|
-
* publishing a false accusation about somebody's product is not.
|
|
22
|
-
*
|
|
23
|
-
* Bringing it back needs evidence that actually separates the cases. It does not exist in the payload
|
|
24
|
-
* today: `element.present` covers "the button is missing", "the API is down" and "the agent mistyped
|
|
25
|
-
* a testid" identically, and `signal.absent` covers both a signal the app never fired and a ref
|
|
26
|
-
* Reticle drove into nothing. Until a verdict carries the reason it came out that way, the honest
|
|
27
|
-
* output is no owner at all.
|
|
28
|
-
*/
|
|
29
9
|
export declare function bugsInResult(toolName: string, result: Record<string, unknown>): BugCandidate[];
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
* anyone's app: that a class of defect was found, never what it was found in.
|
|
17
17
|
*/
|
|
18
18
|
import { ReticleTool } from '../tools/tool-names.js';
|
|
19
|
-
import { BugSource, ContradictionKind } from '@reticlehq/core';
|
|
19
|
+
import { BugAttribution, BugSource, ContradictionKind, isAbsenceDerived, } from '@reticlehq/core';
|
|
20
20
|
/** Cap per tool call — a pathological crawl must not turn one call into a thousand events. */
|
|
21
21
|
const MAX_BUGS_PER_CALL = 25;
|
|
22
22
|
function kindsOf(value) {
|
|
@@ -90,25 +90,35 @@ function couldNotRun(failure) {
|
|
|
90
90
|
true === failure.couldNotRun);
|
|
91
91
|
}
|
|
92
92
|
/**
|
|
93
|
-
*
|
|
93
|
+
* WHOSE fault a contradiction was.
|
|
94
94
|
*
|
|
95
|
-
*
|
|
96
|
-
* `request-never-settled` — which Reticle raises from the ABSENCE of a settle event, so a
|
|
97
|
-
* poll manufactured it with the app doing nothing wrong.
|
|
98
|
-
*
|
|
99
|
-
*
|
|
100
|
-
*
|
|
95
|
+
* `attribution` shipped twice and was wrong both times. It stamped `app` on every contradiction,
|
|
96
|
+
* including `request-never-settled` — which Reticle raises from the ABSENCE of a settle event, so a
|
|
97
|
+
* dev-overlay poll manufactured it with the app doing nothing wrong. Across two full real drives
|
|
98
|
+
* EVERY `attribution: 'app'` was a misattribution, while the one defect that genuinely was a bad
|
|
99
|
+
* agent predicate carried none. A metric confidently wrong about whose fault a defect is, is worse
|
|
100
|
+
* than no metric: it is the number a founder steers on, and it points at the customer.
|
|
101
101
|
*
|
|
102
|
-
*
|
|
103
|
-
*
|
|
104
|
-
*
|
|
102
|
+
* The evidence that was missing then exists now, and it is already drawn in core for the verdict:
|
|
103
|
+
* `ABSENCE_DERIVED_CONTRADICTIONS` separates the kinds inferred from something NOT having happened
|
|
104
|
+
* inside a window Reticle chose the end of, from the kinds positively OBSERVED — a request that came
|
|
105
|
+
* back failed, a signal the app fired that disagrees with its own screen, a written field echoed
|
|
106
|
+
* back changed. Only the second class is evidence against the app, and it is the same line that
|
|
107
|
+
* decides whether a verdict may say `no`. Reusing it rather than inventing a second judgement beside
|
|
108
|
+
* it is the whole point: every historical misattribution was an absence-derived kind, so this rule
|
|
109
|
+
* produces zero of them on the data that broke the last two versions.
|
|
105
110
|
*
|
|
106
|
-
*
|
|
107
|
-
*
|
|
108
|
-
*
|
|
109
|
-
*
|
|
110
|
-
*
|
|
111
|
+
* Everything else — a failed assertion, a replay regression, a crawl anomaly that is not a
|
|
112
|
+
* contradiction — is `unclassified`, which is a VALUE and not a gap. A failed `element.present`
|
|
113
|
+
* covers "the button is missing", "the API is down" and "the agent mistyped a testid" identically,
|
|
114
|
+
* and a console error can be the app, a browser extension or a framework dev overlay. Guessing there
|
|
115
|
+
* puts a guess into the one number we intend to publish. See issue #122.
|
|
111
116
|
*/
|
|
117
|
+
function attributionOf(kind) {
|
|
118
|
+
return CONTRADICTION_KINDS.has(kind) && !isAbsenceDerived(kind)
|
|
119
|
+
? BugAttribution.APP
|
|
120
|
+
: BugAttribution.UNCLASSIFIED;
|
|
121
|
+
}
|
|
112
122
|
export function bugsInResult(toolName, result) {
|
|
113
123
|
// `reticle_run` is a WRAPPER: its handler calls runTool on the real tool, which already reported
|
|
114
124
|
// that result's defects under the real tool's name — then the outer chokepoint reports the very
|
|
@@ -133,6 +143,7 @@ export function bugsInResult(toolName, result) {
|
|
|
133
143
|
kind,
|
|
134
144
|
falseGreen: passed,
|
|
135
145
|
tool: toolName,
|
|
146
|
+
attribution: attributionOf(kind),
|
|
136
147
|
});
|
|
137
148
|
}
|
|
138
149
|
// 2. Crawl anomalies — found autonomously, with nobody writing a test. Crawl reports its own
|
|
@@ -148,8 +159,9 @@ export function bugsInResult(toolName, result) {
|
|
|
148
159
|
// — even though no assertion was made. See the definition on `falseGreen`.
|
|
149
160
|
falseGreen: CONTRADICTION_KINDS.has(kind),
|
|
150
161
|
tool: toolName,
|
|
151
|
-
//
|
|
152
|
-
// fault
|
|
162
|
+
// Same kind, same owner, wherever it arrives. A `request-never-settled` found by the crawler
|
|
163
|
+
// is no better evidence about whose fault it was than one found by an assertion.
|
|
164
|
+
attribution: attributionOf(kind),
|
|
153
165
|
});
|
|
154
166
|
}
|
|
155
167
|
// 3. A failed assertion — the agent declared a consequence and it did not hold.
|
|
@@ -167,7 +179,9 @@ export function bugsInResult(toolName, result) {
|
|
|
167
179
|
kind,
|
|
168
180
|
falseGreen: false,
|
|
169
181
|
tool: toolName,
|
|
170
|
-
//
|
|
182
|
+
// The assertion label cannot say whose fault it was: `element.present` covers a missing
|
|
183
|
+
// button, a downed API and a mistyped testid identically.
|
|
184
|
+
attribution: BugAttribution.UNCLASSIFIED,
|
|
171
185
|
});
|
|
172
186
|
}
|
|
173
187
|
// 4. Replay failures — a saved flow that used to pass no longer does. That is a regression caught,
|
|
@@ -184,8 +198,11 @@ export function bugsInResult(toolName, result) {
|
|
|
184
198
|
kind: 'flow-regression',
|
|
185
199
|
falseGreen: false,
|
|
186
200
|
tool: toolName,
|
|
187
|
-
// A
|
|
188
|
-
//
|
|
201
|
+
// A flow that used to pass and no longer does is a regression SOMEWHERE, and the row does
|
|
202
|
+
// not say where: the app changed, or a selector strategy of ours did. Rows that never RAN
|
|
203
|
+
// are skipped above, which keeps Reticle's outright failures out of the count entirely, but
|
|
204
|
+
// that is a weaker fact than the positive evidence `app` requires.
|
|
205
|
+
attribution: BugAttribution.UNCLASSIFIED,
|
|
189
206
|
});
|
|
190
207
|
}
|
|
191
208
|
}
|
|
@@ -8,6 +8,7 @@ import { TelemetryEventKind } from '@reticlehq/core';
|
|
|
8
8
|
import { DAEMON_INNER_COMMAND, knownCommand } from '../cli/cli-parse.js';
|
|
9
9
|
import { getTelemetry } from './telemetry.js';
|
|
10
10
|
import { describeCliFlags } from './argument-shape.js';
|
|
11
|
+
import { resolveInstallSource } from './install-source.js';
|
|
11
12
|
/**
|
|
12
13
|
* Report that a person ran a `reticle` subcommand.
|
|
13
14
|
*
|
|
@@ -45,7 +46,12 @@ export function reportCliRun(argv) {
|
|
|
45
46
|
// installed the SDK into nine apps — the top of every funnel, missing.
|
|
46
47
|
// `detach` so a quick command (`version`/`gate`) exits immediately instead of waiting out the POST.
|
|
47
48
|
if (telemetry.firstRun)
|
|
48
|
-
void telemetry.emit(TelemetryEventKind.RETICLE_INSTALLED, {
|
|
49
|
+
void telemetry.emit(TelemetryEventKind.RETICLE_INSTALLED, {
|
|
50
|
+
detach: true,
|
|
51
|
+
// WHICH published route brought this machine in. Self-declared by the channel, never inferred
|
|
52
|
+
// — see install-source.ts for which of the four routes can actually carry the marker.
|
|
53
|
+
installSource: resolveInstallSource(),
|
|
54
|
+
});
|
|
49
55
|
if (!isHumanCliCommand(command))
|
|
50
56
|
return;
|
|
51
57
|
void telemetry.emit(TelemetryEventKind.CLI_COMMAND_RUN, {
|
|
@@ -147,7 +147,7 @@ export async function submitFeedback(input, opts = {}) {
|
|
|
147
147
|
// Reticle SOURCE CHECKOUT disables telemetry by cwd, and that is exactly where release runs and
|
|
148
148
|
// contributor sessions happen — so the reports most worth having were the ones silently lost.
|
|
149
149
|
reason: isReticleSourceCheckout(opts.cwd ?? process.cwd())
|
|
150
|
-
? 'this is a Reticle source checkout, where telemetry is disabled by design
|
|
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
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.`,
|
|
152
152
|
redacted,
|
|
153
153
|
context,
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
*/
|
|
13
13
|
import { TelemetryEventKind } from '@reticlehq/core';
|
|
14
14
|
import { getTelemetry } from './telemetry.js';
|
|
15
|
+
import { resolveInstallSource } from './install-source.js';
|
|
15
16
|
/** Classified init failures — our own vocabulary, never a raw error or a path. */
|
|
16
17
|
export const InitFailure = {
|
|
17
18
|
/** Run outside a project root. The single most common first-run mistake. */
|
|
@@ -25,7 +26,14 @@ export const InitFailure = {
|
|
|
25
26
|
/** Report one init outcome. Best-effort; setup must never fail because a metric did. */
|
|
26
27
|
export function reportInitOutcome(init) {
|
|
27
28
|
try {
|
|
28
|
-
void getTelemetry().emit(TelemetryEventKind.INIT_COMPLETED, {
|
|
29
|
+
void getTelemetry().emit(TelemetryEventKind.INIT_COMPLETED, {
|
|
30
|
+
init,
|
|
31
|
+
detach: true,
|
|
32
|
+
// The other half of attribution: `reticle_installed` says a machine arrived, this says the
|
|
33
|
+
// setup on it finished, and only together do they say which route converts. Same self-declared
|
|
34
|
+
// marker, same refusal to infer — see install-source.ts.
|
|
35
|
+
installSource: resolveInstallSource(),
|
|
36
|
+
});
|
|
29
37
|
}
|
|
30
38
|
catch {
|
|
31
39
|
/* a metric must never break `reticle init` */
|