@reticlehq/server 2.6.0 → 2.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/SKILL.md +201 -18
- package/dist/bridge/bridge.d.ts +6 -1
- package/dist/bridge/bridge.js +27 -4
- package/dist/cli/chromium-hint.d.ts +40 -0
- package/dist/cli/chromium-hint.js +75 -0
- package/dist/cli/cli-doctor.js +3 -11
- package/dist/cli/cli-launch.d.ts +1 -0
- package/dist/cli/cli-launch.js +4 -1
- package/dist/cli/cli-port.js +20 -1
- package/dist/cli/doctor-daemon-line.js +10 -1
- package/dist/cli.js +40 -9
- package/dist/daemon/daemon-resilience.js +2 -2
- package/dist/daemon/loopback-alias.d.ts +32 -0
- package/dist/daemon/loopback-alias.js +80 -0
- package/dist/events/predicate-eval.d.ts +7 -3
- package/dist/events/predicate-eval.js +159 -92
- package/dist/events/predicate-parse.d.ts +1 -1
- package/dist/events/predicate-parse.js +18 -4
- package/dist/events/ring-buffer.d.ts +8 -0
- package/dist/events/ring-buffer.js +34 -1
- package/dist/honesty/honesty.d.ts +11 -0
- package/dist/honesty/honesty.js +9 -1
- package/dist/honesty/verified.js +26 -3
- package/dist/http-server.js +11 -1
- package/dist/index.js +26 -2
- package/dist/init/agent-rules.js +6 -1
- package/dist/init/detect.d.ts +8 -0
- package/dist/init/detect.js +15 -0
- package/dist/init/non-js-project.d.ts +27 -0
- package/dist/init/non-js-project.js +60 -0
- package/dist/init/plan-framework.d.ts +9 -0
- package/dist/init/plan-framework.js +19 -1
- package/dist/init/plan.js +20 -3
- package/dist/init/run.js +38 -9
- package/dist/init/snippets.d.ts +20 -1
- package/dist/init/snippets.js +71 -6
- package/dist/mcp/mcp-proxy.js +10 -3
- package/dist/mcp/mcp.js +35 -0
- package/dist/mcp/proxy-lifecycle.d.ts +19 -0
- package/dist/mcp/proxy-lifecycle.js +21 -0
- package/dist/pool/playwright-launcher.js +10 -2
- package/dist/session/live-control-tools.js +39 -2
- package/dist/session/live-control.d.ts +7 -9
- package/dist/session/live-control.js +34 -1
- package/dist/session/no-session-diagnosis.d.ts +4 -4
- package/dist/session/no-session-diagnosis.js +60 -29
- package/dist/session/review-store.d.ts +19 -0
- package/dist/session/review-store.js +34 -5
- package/dist/session/session-manager.d.ts +9 -0
- package/dist/session/session-manager.js +11 -0
- package/dist/session/session.d.ts +8 -0
- package/dist/session/session.js +12 -0
- package/dist/status-payload.d.ts +13 -1
- package/dist/status-payload.js +6 -1
- package/dist/telemetry/app-instrumented.d.ts +37 -0
- package/dist/telemetry/app-instrumented.js +54 -0
- package/dist/telemetry/daemon-telemetry.d.ts +4 -4
- package/dist/telemetry/daemon-telemetry.js +8 -4
- package/dist/telemetry/session-metrics.d.ts +3 -3
- package/dist/telemetry/session-metrics.js +4 -4
- package/dist/telemetry/telemetry.d.ts +3 -1
- package/dist/telemetry/telemetry.js +3 -1
- package/dist/telemetry/verification-of.js +26 -1
- package/dist/tools/act-tools.js +32 -10
- package/dist/tools/assert-verdict.js +7 -0
- package/dist/tools/error-recovery.js +6 -1
- package/dist/tools/invoke-tool.js +11 -10
- package/dist/tools/tool-kit.d.ts +52 -5
- package/dist/tools/tool-kit.js +55 -24
- package/dist/tools/tool-surface.js +9 -9
- package/dist/tools/tools.js +22 -0
- package/dist/visual/capture-cleanup.d.ts +16 -0
- package/dist/visual/capture-cleanup.js +39 -0
- package/dist/visual/visual-tools.d.ts +18 -0
- package/dist/visual/visual-tools.js +24 -5
- package/docs/agent-cheatsheet.md +14 -2
- package/docs/gate-plan.md +1 -1
- package/docs/gates.md +1 -1
- package/docs/telemetry-contract.md +13 -1
- package/docs/telemetry.md +9 -1
- package/docs/token-efficiency.md +2 -2
- package/package.json +2 -2
|
@@ -8,6 +8,7 @@ import { noteToolCall } from '../daemon/daemon-usefulness.js';
|
|
|
8
8
|
import { bugsInResult } from '../telemetry/bug-found.js';
|
|
9
9
|
import { verificationOf } from '../telemetry/verification-of.js';
|
|
10
10
|
import { asString } from './tools-helpers.js';
|
|
11
|
+
import { EnvelopeKey } from './tool-kit.js';
|
|
11
12
|
import { ReticleTool } from './tool-names.js';
|
|
12
13
|
import { takeFeedbackPrompt } from './feedback-tools.js';
|
|
13
14
|
import { takeFeedbackUndelivered } from '../telemetry/feedback-delivery.js';
|
|
@@ -262,12 +263,12 @@ export async function runTool(tool, deps, args) {
|
|
|
262
263
|
? raw
|
|
263
264
|
: {
|
|
264
265
|
...raw,
|
|
265
|
-
...(prompt !== undefined ? {
|
|
266
|
-
...(update !== undefined ? {
|
|
267
|
-
...(skew !== undefined ? {
|
|
266
|
+
...(prompt !== undefined ? { [EnvelopeKey.FEEDBACK_PROMPT]: prompt } : {}),
|
|
267
|
+
...(update !== undefined ? { [EnvelopeKey.UPDATE_AVAILABLE]: update } : {}),
|
|
268
|
+
...(skew !== undefined ? { [EnvelopeKey.VERSION_SKEW]: skew } : {}),
|
|
268
269
|
...(undelivered !== undefined
|
|
269
270
|
? {
|
|
270
|
-
|
|
271
|
+
[EnvelopeKey.FEEDBACK_UNDELIVERED]: `your earlier report did NOT send: ${undelivered}. Tell the human what you found so it is not lost.`,
|
|
271
272
|
}
|
|
272
273
|
: {}),
|
|
273
274
|
};
|
|
@@ -286,17 +287,17 @@ export async function runTool(tool, deps, args) {
|
|
|
286
287
|
// one-time pool-lease reminder or the age cleanup nudge. Splice them regardless.
|
|
287
288
|
const lease = resolved.takeSessionLease();
|
|
288
289
|
if (lease !== undefined)
|
|
289
|
-
envelope[
|
|
290
|
+
envelope[EnvelopeKey.SESSION_LEASE] = lease;
|
|
290
291
|
const warning = resolved.ageWarning();
|
|
291
292
|
if (warning !== undefined)
|
|
292
|
-
envelope[
|
|
293
|
-
// Ask for a verdict when the agent has driven the page and not asked for one.
|
|
294
|
-
// verdict-less
|
|
293
|
+
envelope[EnvelopeKey.SESSION_AGE_WARNING] = warning;
|
|
294
|
+
// Ask for a verdict when the agent has driven the page and not asked for one. Almost every
|
|
295
|
+
// verdict-less session in the field never called a verdict-producing tool ONCE;
|
|
295
296
|
// the counter behind this already existed and was reported only to us. One-shot per abandoned
|
|
296
297
|
// run, same discipline as the pool lease — a hint on every call is noise that gets tuned out.
|
|
297
298
|
const unverified = getSessionMetrics().takeUnverifiedNudge();
|
|
298
299
|
if (unverified !== undefined)
|
|
299
|
-
envelope[
|
|
300
|
+
envelope[EnvelopeKey.VERIFY_NEXT] = unverified;
|
|
300
301
|
// Invite feedback at the moment of friction, worded for what just happened. Feedback produced
|
|
301
302
|
// ~11 of this release's fixes from 14 reports; the handshake instruction is what got those 14,
|
|
302
303
|
// and this closes the gap it leaves — the agent mid-problem, not thinking about feedback tooling.
|
|
@@ -311,7 +312,7 @@ export async function runTool(tool, deps, args) {
|
|
|
311
312
|
producedVerdict: 'verified' in result,
|
|
312
313
|
});
|
|
313
314
|
if (friction !== undefined) {
|
|
314
|
-
envelope[
|
|
315
|
+
envelope[EnvelopeKey.FEEDBACK_INVITE] = inviteFor(friction);
|
|
315
316
|
getSessionMetrics().recordFeedbackPrompt();
|
|
316
317
|
}
|
|
317
318
|
return Object.keys(envelope).length > 0 ? { ...result, ...envelope } : result;
|
package/dist/tools/tool-kit.d.ts
CHANGED
|
@@ -84,11 +84,58 @@ export declare const sessionIdShape: {
|
|
|
84
84
|
sessionId: z.ZodOptional<z.ZodString>;
|
|
85
85
|
};
|
|
86
86
|
/**
|
|
87
|
-
*
|
|
88
|
-
*
|
|
89
|
-
*
|
|
90
|
-
*
|
|
91
|
-
* envelope
|
|
87
|
+
* Every key `runTool` splices onto a session-bound result, as a closed list.
|
|
88
|
+
*
|
|
89
|
+
* These are wire strings and were inlined at the splice sites, which is exactly how four of them
|
|
90
|
+
* came to be spliced and never declared. `sessionEnvelopeShape` is derived from this object and
|
|
91
|
+
* `envelope-keys-declared.test.ts` fails if a member is missing from it, so the two cannot drift.
|
|
92
|
+
*
|
|
93
|
+
* Each of these is a channel that exists BECAUSE the agent had no other way to learn the fact.
|
|
94
|
+
* Undeclared, a schema-strict client validating `structuredContent` drops it, and the channel is
|
|
95
|
+
* indistinguishable from never having been built.
|
|
96
|
+
*/
|
|
97
|
+
export declare const EnvelopeKey: {
|
|
98
|
+
/** Session health — which tab, how fresh. */
|
|
99
|
+
readonly SESSION: "session";
|
|
100
|
+
/** One-shot pool-lease reminder. */
|
|
101
|
+
readonly SESSION_LEASE: "session_lease";
|
|
102
|
+
/** One-shot "these sessions are old, clean them up". */
|
|
103
|
+
readonly SESSION_AGE_WARNING: "session_age_warning";
|
|
104
|
+
/** The delivered-once human-in-the-loop guidance envelope. Losing it is a safety failure. */
|
|
105
|
+
readonly CONTROL: "control";
|
|
106
|
+
/**
|
|
107
|
+
* Rides alongside `session` whenever the tab is THROTTLED. It was declared on `reticle_act`'s
|
|
108
|
+
* schema and nowhere else, so on a validating profile every other session-bound tool silently
|
|
109
|
+
* dropped it — a throttled tab, where drives can no-op, returned a healthy-looking result.
|
|
110
|
+
*/
|
|
111
|
+
readonly WARNING: "warning";
|
|
112
|
+
/** One-shot "ask the human how this went". */
|
|
113
|
+
readonly FEEDBACK_PROMPT: "feedback_prompt";
|
|
114
|
+
/** One-shot "a newer Reticle exists". */
|
|
115
|
+
readonly UPDATE_AVAILABLE: "update_available";
|
|
116
|
+
/**
|
|
117
|
+
* "You have driven this page N times without asking for a verdict."
|
|
118
|
+
*
|
|
119
|
+
* The largest known lever on the metric this product is judged by: verdict-less sessions
|
|
120
|
+
* overwhelmingly never call a verdict-producing tool at all. It was spliced undeclared, so on a
|
|
121
|
+
* validating client the nudge was built, fired, and thrown away before any agent saw it.
|
|
122
|
+
*/
|
|
123
|
+
readonly VERIFY_NEXT: "verify_next";
|
|
124
|
+
/** The contextual "tell us what just went wrong" invitation. Same story, same release. */
|
|
125
|
+
readonly FEEDBACK_INVITE: "feedback_invite";
|
|
126
|
+
/** SDK/daemon version skew — often the one fact that explains everything else in the session. */
|
|
127
|
+
readonly VERSION_SKEW: "version_skew";
|
|
128
|
+
/** A feedback report that was accepted and then failed to send. Only the reporter can act on it. */
|
|
129
|
+
readonly FEEDBACK_UNDELIVERED: "feedback_undelivered";
|
|
130
|
+
};
|
|
131
|
+
export type EnvelopeKey = (typeof EnvelopeKey)[keyof typeof EnvelopeKey];
|
|
132
|
+
/**
|
|
133
|
+
* Fields that `runTool` / `withControl` splice onto EVERY session-bound tool result at runtime.
|
|
134
|
+
* Merged into each session-bound tool's outputSchema so a schema-strict client (structuredContent
|
|
135
|
+
* validation) keeps them instead of silently dropping them.
|
|
136
|
+
*
|
|
137
|
+
* Derived from `EnvelopeKey` rather than re-listed: this shape and the splice sites disagreed for a
|
|
138
|
+
* whole release, and every key that went missing was a channel nobody could see was missing.
|
|
92
139
|
*/
|
|
93
140
|
export declare const sessionEnvelopeShape: z.ZodRawShape;
|
|
94
141
|
/** Unwrap a browser command result or throw its error so the agent sees a clean failure. */
|
package/dist/tools/tool-kit.js
CHANGED
|
@@ -14,32 +14,63 @@ export const sessionIdShape = {
|
|
|
14
14
|
.describe('OMIT THIS unless you mean a specific tab — Reticle scopes to your project, prefers the active one, and refuses rather than guesses when ambiguous. Pass an id from reticle_sessions only to target a particular tab. The previous wording ("omit when only ONE session is open") was wrong and cost real turns: with three sessions connected — an app plus two pool leases — resolution is still unambiguous, but that sentence sent the agent off to list and filter sessions by hand before every single call.'),
|
|
15
15
|
};
|
|
16
16
|
/**
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
* envelope
|
|
17
|
+
* Every key `runTool` splices onto a session-bound result, as a closed list.
|
|
18
|
+
*
|
|
19
|
+
* These are wire strings and were inlined at the splice sites, which is exactly how four of them
|
|
20
|
+
* came to be spliced and never declared. `sessionEnvelopeShape` is derived from this object and
|
|
21
|
+
* `envelope-keys-declared.test.ts` fails if a member is missing from it, so the two cannot drift.
|
|
22
|
+
*
|
|
23
|
+
* Each of these is a channel that exists BECAUSE the agent had no other way to learn the fact.
|
|
24
|
+
* Undeclared, a schema-strict client validating `structuredContent` drops it, and the channel is
|
|
25
|
+
* indistinguishable from never having been built.
|
|
22
26
|
*/
|
|
23
|
-
export const
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
27
|
+
export const EnvelopeKey = {
|
|
28
|
+
/** Session health — which tab, how fresh. */
|
|
29
|
+
SESSION: 'session',
|
|
30
|
+
/** One-shot pool-lease reminder. */
|
|
31
|
+
SESSION_LEASE: 'session_lease',
|
|
32
|
+
/** One-shot "these sessions are old, clean them up". */
|
|
33
|
+
SESSION_AGE_WARNING: 'session_age_warning',
|
|
34
|
+
/** The delivered-once human-in-the-loop guidance envelope. Losing it is a safety failure. */
|
|
35
|
+
CONTROL: 'control',
|
|
36
|
+
/**
|
|
37
|
+
* Rides alongside `session` whenever the tab is THROTTLED. It was declared on `reticle_act`'s
|
|
38
|
+
* schema and nowhere else, so on a validating profile every other session-bound tool silently
|
|
39
|
+
* dropped it — a throttled tab, where drives can no-op, returned a healthy-looking result.
|
|
40
|
+
*/
|
|
41
|
+
WARNING: 'warning',
|
|
42
|
+
/** One-shot "ask the human how this went". */
|
|
43
|
+
FEEDBACK_PROMPT: 'feedback_prompt',
|
|
44
|
+
/** One-shot "a newer Reticle exists". */
|
|
45
|
+
UPDATE_AVAILABLE: 'update_available',
|
|
46
|
+
/**
|
|
47
|
+
* "You have driven this page N times without asking for a verdict."
|
|
48
|
+
*
|
|
49
|
+
* The largest known lever on the metric this product is judged by: verdict-less sessions
|
|
50
|
+
* overwhelmingly never call a verdict-producing tool at all. It was spliced undeclared, so on a
|
|
51
|
+
* validating client the nudge was built, fired, and thrown away before any agent saw it.
|
|
52
|
+
*/
|
|
53
|
+
VERIFY_NEXT: 'verify_next',
|
|
54
|
+
/** The contextual "tell us what just went wrong" invitation. Same story, same release. */
|
|
55
|
+
FEEDBACK_INVITE: 'feedback_invite',
|
|
56
|
+
/** SDK/daemon version skew — often the one fact that explains everything else in the session. */
|
|
57
|
+
VERSION_SKEW: 'version_skew',
|
|
58
|
+
/** A feedback report that was accepted and then failed to send. Only the reporter can act on it. */
|
|
59
|
+
FEEDBACK_UNDELIVERED: 'feedback_undelivered',
|
|
42
60
|
};
|
|
61
|
+
/**
|
|
62
|
+
* Fields that `runTool` / `withControl` splice onto EVERY session-bound tool result at runtime.
|
|
63
|
+
* Merged into each session-bound tool's outputSchema so a schema-strict client (structuredContent
|
|
64
|
+
* validation) keeps them instead of silently dropping them.
|
|
65
|
+
*
|
|
66
|
+
* Derived from `EnvelopeKey` rather than re-listed: this shape and the splice sites disagreed for a
|
|
67
|
+
* whole release, and every key that went missing was a channel nobody could see was missing.
|
|
68
|
+
*/
|
|
69
|
+
export const sessionEnvelopeShape = Object.fromEntries(Object.values(EnvelopeKey).map((key) => [
|
|
70
|
+
key,
|
|
71
|
+
// `warning` is the one with a narrower type; the rest are opaque envelopes the client forwards.
|
|
72
|
+
EnvelopeKey.WARNING === key ? z.string().optional() : z.unknown().optional(),
|
|
73
|
+
]));
|
|
43
74
|
/** Unwrap a browser command result or throw its error so the agent sees a clean failure. */
|
|
44
75
|
export async function commandOrThrow(deps, sessionId,
|
|
45
76
|
// ReticleCommand, not string: the union already exists and every caller passes a member. Typed as
|
|
@@ -100,16 +100,16 @@ export const CORE_TOOL_NAMES = new Set([
|
|
|
100
100
|
ReticleTool.ACT_AND_WAIT,
|
|
101
101
|
// ACT_SEQUENCE is here because its absence was measurably CAUSING the biggest loop in the data.
|
|
102
102
|
//
|
|
103
|
-
//
|
|
104
|
-
//
|
|
105
|
-
//
|
|
106
|
-
//
|
|
103
|
+
// In the field `reticle_act` is called overwhelmingly more often than `reticle_act_sequence`, and
|
|
104
|
+
// `act` leads the repeat table by a wide margin — inside those sessions the repeated calls are
|
|
105
|
+
// clicks and fills, a login form driven one round trip at a time, which is exactly the antipattern
|
|
106
|
+
// SKILL.md warns about and exactly what this tool exists to collapse.
|
|
107
107
|
//
|
|
108
|
-
// The repeats are NOT retries: looping sessions have a
|
|
109
|
-
//
|
|
110
|
-
//
|
|
111
|
-
//
|
|
112
|
-
//
|
|
108
|
+
// The repeats are NOT retries: looping sessions have a LOWER error rate than non-looping ones. The
|
|
109
|
+
// calls succeed and get repeated, because the batching tool was reachable only through
|
|
110
|
+
// `reticle_run` — so an agent had to already know it existed to use it, and essentially nobody
|
|
111
|
+
// did. A tool an agent must already know about is a tool that never gets called; the same argument
|
|
112
|
+
// that put INSPECT and FEEDBACK here.
|
|
113
113
|
ReticleTool.ACT_SEQUENCE,
|
|
114
114
|
ReticleTool.OBSERVE,
|
|
115
115
|
ReticleTool.NETWORK,
|
package/dist/tools/tools.js
CHANGED
|
@@ -78,6 +78,10 @@ const RAW_TOOLS = [
|
|
|
78
78
|
recommendation: z.string().optional(),
|
|
79
79
|
}))
|
|
80
80
|
.describe('Connected browser sessions with health state. `projectId` groups sessions by app (stable across port changes); `leased` marks a pool-managed headless context vs a human tab.'),
|
|
81
|
+
why: z
|
|
82
|
+
.string()
|
|
83
|
+
.optional()
|
|
84
|
+
.describe('Present ONLY when `sessions` is empty: why nothing is connected, and the next action that fixes it. An empty list is never the end of the road — read this before concluding the app cannot be driven.'),
|
|
81
85
|
},
|
|
82
86
|
handler: async (deps) => {
|
|
83
87
|
const provider = deps.realInput;
|
|
@@ -87,6 +91,14 @@ const RAW_TOOLS = [
|
|
|
87
91
|
realInputAvailable: provider !== undefined ? await provider.isAvailableFor(s.url) : false,
|
|
88
92
|
leased: leasedIds.has(s.sessionId),
|
|
89
93
|
})));
|
|
94
|
+
// An empty list is the most common thing this tool ever returns, and on its own it is a dead
|
|
95
|
+
// end: the agent asked the one question it knows to ask, got a confident-looking answer with
|
|
96
|
+
// no next step, and stopped. The daemon already knows WHY nothing is connected — say it here
|
|
97
|
+
// rather than only when a later tool fails for want of a session.
|
|
98
|
+
if (0 === sessions.length) {
|
|
99
|
+
const why = deps.sessions.noSessionHint();
|
|
100
|
+
return { sessions, ...(why === undefined ? {} : { why }) };
|
|
101
|
+
}
|
|
90
102
|
return { sessions };
|
|
91
103
|
},
|
|
92
104
|
},
|
|
@@ -193,6 +205,10 @@ const RAW_TOOLS = [
|
|
|
193
205
|
.string()
|
|
194
206
|
.optional()
|
|
195
207
|
.describe('CSS selector or element ref to restrict the search to a subtree.'),
|
|
208
|
+
self: z
|
|
209
|
+
.boolean()
|
|
210
|
+
.optional()
|
|
211
|
+
.describe('Return the `scope` element ITSELF instead of searching inside it. Use when the target is a plain layout container with no role, name, testid or text of its own — which is routinely the element that carries the handler, and is otherwise unreachable because every query excludes its own scope root. Requires `scope`.'),
|
|
196
212
|
attrs: z
|
|
197
213
|
.array(z.string())
|
|
198
214
|
.optional()
|
|
@@ -297,7 +313,13 @@ const RAW_TOOLS = [
|
|
|
297
313
|
// The handler forwards an explicit allowlist, so a new input is silently dropped unless it is
|
|
298
314
|
// added here — the browser saw no `attrs` and returned undefined while the unit tests, which
|
|
299
315
|
// call matchQuery directly, passed. Schema plus implementation is not the whole wire.
|
|
316
|
+
//
|
|
317
|
+
// It happened again with `self`, in the same session that wrote this comment: declared on the
|
|
318
|
+
// tool, implemented in the browser, verified by unit tests that call `runQuery` directly, and
|
|
319
|
+
// dropped here — so the live call returned zero matches with no error. `query-forwarding.test.ts`
|
|
320
|
+
// now asserts the payload, because a comment is not a guard.
|
|
300
321
|
attrs: args['attrs'],
|
|
322
|
+
self: args['self'],
|
|
301
323
|
}).then((result) => withSizeCost(paginateQueryResult(result, asNumber(args['limit']), true === args['count_only'])));
|
|
302
324
|
},
|
|
303
325
|
},
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Remember the private parent of a capture after the daemon has read it.
|
|
3
|
+
*
|
|
4
|
+
* Legacy captures live directly in `tmpdir()` and therefore have no directory to reclaim. The
|
|
5
|
+
* caller has already applied the capture-path trust-boundary check; this second exact parent check
|
|
6
|
+
* keeps the cleanup set limited to the one-level private layout.
|
|
7
|
+
*/
|
|
8
|
+
export declare function trackCaptureDirectory(path: string): void;
|
|
9
|
+
/**
|
|
10
|
+
* Remove empty private capture directories during the daemon's existing awaited shutdown path.
|
|
11
|
+
*
|
|
12
|
+
* Deliberately `rmdir`, never recursive removal: captures are unlinked after reading, so a genuine
|
|
13
|
+
* directory is empty by shutdown. If anything unexpected remains, refusing to remove the directory
|
|
14
|
+
* is safer than widening a renderer-supplied path into a recursive delete.
|
|
15
|
+
*/
|
|
16
|
+
export declare function cleanupCaptureDirectories(): Promise<void>;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { rmdir } from 'node:fs/promises';
|
|
2
|
+
import { tmpdir } from 'node:os';
|
|
3
|
+
import { basename, dirname } from 'node:path';
|
|
4
|
+
import { RETICLE_CAPTURE_FILE_PREFIX } from '@reticlehq/core';
|
|
5
|
+
/** Private shell directories whose captures this daemon has successfully consumed. */
|
|
6
|
+
const captureDirectories = new Set();
|
|
7
|
+
/**
|
|
8
|
+
* Remember the private parent of a capture after the daemon has read it.
|
|
9
|
+
*
|
|
10
|
+
* Legacy captures live directly in `tmpdir()` and therefore have no directory to reclaim. The
|
|
11
|
+
* caller has already applied the capture-path trust-boundary check; this second exact parent check
|
|
12
|
+
* keeps the cleanup set limited to the one-level private layout.
|
|
13
|
+
*/
|
|
14
|
+
export function trackCaptureDirectory(path) {
|
|
15
|
+
const parent = dirname(path);
|
|
16
|
+
if (dirname(parent) === tmpdir() && basename(parent).startsWith(RETICLE_CAPTURE_FILE_PREFIX)) {
|
|
17
|
+
captureDirectories.add(parent);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Remove empty private capture directories during the daemon's existing awaited shutdown path.
|
|
22
|
+
*
|
|
23
|
+
* Deliberately `rmdir`, never recursive removal: captures are unlinked after reading, so a genuine
|
|
24
|
+
* directory is empty by shutdown. If anything unexpected remains, refusing to remove the directory
|
|
25
|
+
* is safer than widening a renderer-supplied path into a recursive delete.
|
|
26
|
+
*/
|
|
27
|
+
export async function cleanupCaptureDirectories() {
|
|
28
|
+
await Promise.all([...captureDirectories].map(async (dir) => {
|
|
29
|
+
try {
|
|
30
|
+
await rmdir(dir);
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
// Best-effort shutdown housekeeping. A non-empty or already-removed directory is harmless.
|
|
34
|
+
}
|
|
35
|
+
finally {
|
|
36
|
+
captureDirectories.delete(dir);
|
|
37
|
+
}
|
|
38
|
+
}));
|
|
39
|
+
}
|
|
@@ -1,4 +1,22 @@
|
|
|
1
1
|
import type { ToolDef } from '../tools/tools.js';
|
|
2
|
+
/**
|
|
3
|
+
* Only read a capture the shell wrote. The path arrives from the PAGE, and the daemon must not
|
|
4
|
+
* become a file-read oracle for whatever a compromised renderer names.
|
|
5
|
+
*
|
|
6
|
+
* Two layouts are accepted, and the pair is the whole subtlety:
|
|
7
|
+
*
|
|
8
|
+
* - `<tmp>/<prefix><dir>/<prefix><n>.png` — the shell's own PRIVATE capture directory, created 0700
|
|
9
|
+
* so no other local user can read the screenshot or pre-place a symlink at the path we write.
|
|
10
|
+
* - `<tmp>/<prefix><pid>-<n>.png` — the legacy flat layout. Kept because the shell ships as a
|
|
11
|
+
* SEPARATE package on the user's own upgrade schedule: an app still on the older
|
|
12
|
+
* `@reticlehq/electron` talking to a newer daemon must keep getting screenshots, not silently
|
|
13
|
+
* start returning no image. It grants the daemon nothing it did not already have.
|
|
14
|
+
*
|
|
15
|
+
* Every comparison is an exact match against an UNRESOLVED path, which is what refuses traversal:
|
|
16
|
+
* `<tmp>/<prefix>x/../../etc/<prefix>passwd.png` has a grandparent of `<tmp>/<prefix>x/..`, not
|
|
17
|
+
* `<tmp>`. Exactly one directory level is allowed, so a deeper nesting is refused too.
|
|
18
|
+
*/
|
|
19
|
+
export declare function isCapturePath(path: string): boolean;
|
|
2
20
|
/**
|
|
3
21
|
* The opt-in pixel layer. Both tools require a DRIVEN browser (reticle drive /
|
|
4
22
|
* RETICLE_CDP_URL) — the always-on SDK ships no screenshotter — and return a structured NO_PROVIDER
|
|
@@ -5,6 +5,7 @@ import { sessionIdShape } from '../tools/tool-kit.js';
|
|
|
5
5
|
import { asNumber, asRecord, asString } from '../tools/tools-helpers.js';
|
|
6
6
|
import { diffPng } from './visual-diff.js';
|
|
7
7
|
import { VisualStore } from './visual-store.js';
|
|
8
|
+
import { trackCaptureDirectory } from './capture-cleanup.js';
|
|
8
9
|
import { readFile, unlink } from 'node:fs/promises';
|
|
9
10
|
import { tmpdir } from 'node:os';
|
|
10
11
|
import { basename, dirname } from 'node:path';
|
|
@@ -87,6 +88,7 @@ async function desktopCapture(deps, sessionId, fullPage) {
|
|
|
87
88
|
try {
|
|
88
89
|
const bytes = await readFile(path);
|
|
89
90
|
await unlink(path).catch(() => undefined);
|
|
91
|
+
trackCaptureDirectory(path);
|
|
90
92
|
return isCompletePng(bytes) ? { png: new Uint8Array(bytes) } : {};
|
|
91
93
|
}
|
|
92
94
|
catch {
|
|
@@ -94,12 +96,29 @@ async function desktopCapture(deps, sessionId, fullPage) {
|
|
|
94
96
|
}
|
|
95
97
|
}
|
|
96
98
|
/**
|
|
97
|
-
* Only read a capture the shell wrote
|
|
98
|
-
*
|
|
99
|
-
*
|
|
99
|
+
* Only read a capture the shell wrote. The path arrives from the PAGE, and the daemon must not
|
|
100
|
+
* become a file-read oracle for whatever a compromised renderer names.
|
|
101
|
+
*
|
|
102
|
+
* Two layouts are accepted, and the pair is the whole subtlety:
|
|
103
|
+
*
|
|
104
|
+
* - `<tmp>/<prefix><dir>/<prefix><n>.png` — the shell's own PRIVATE capture directory, created 0700
|
|
105
|
+
* so no other local user can read the screenshot or pre-place a symlink at the path we write.
|
|
106
|
+
* - `<tmp>/<prefix><pid>-<n>.png` — the legacy flat layout. Kept because the shell ships as a
|
|
107
|
+
* SEPARATE package on the user's own upgrade schedule: an app still on the older
|
|
108
|
+
* `@reticlehq/electron` talking to a newer daemon must keep getting screenshots, not silently
|
|
109
|
+
* start returning no image. It grants the daemon nothing it did not already have.
|
|
110
|
+
*
|
|
111
|
+
* Every comparison is an exact match against an UNRESOLVED path, which is what refuses traversal:
|
|
112
|
+
* `<tmp>/<prefix>x/../../etc/<prefix>passwd.png` has a grandparent of `<tmp>/<prefix>x/..`, not
|
|
113
|
+
* `<tmp>`. Exactly one directory level is allowed, so a deeper nesting is refused too.
|
|
100
114
|
*/
|
|
101
|
-
function isCapturePath(path) {
|
|
102
|
-
|
|
115
|
+
export function isCapturePath(path) {
|
|
116
|
+
if (!basename(path).startsWith(RETICLE_CAPTURE_FILE_PREFIX))
|
|
117
|
+
return false;
|
|
118
|
+
const parent = dirname(path);
|
|
119
|
+
if (parent === tmpdir())
|
|
120
|
+
return true;
|
|
121
|
+
return dirname(parent) === tmpdir() && basename(parent).startsWith(RETICLE_CAPTURE_FILE_PREFIX);
|
|
103
122
|
}
|
|
104
123
|
/**
|
|
105
124
|
* A PNG is complete only if it ends with IEND. This check exists because a truncated image once
|
package/docs/agent-cheatsheet.md
CHANGED
|
@@ -7,10 +7,12 @@ One screen to get fluent. Reticle is the **proof layer for AI agents** — no sc
|
|
|
7
7
|
| Verb | Tool | One-liner |
|
|
8
8
|
| --- | --- | --- |
|
|
9
9
|
| **look** | `reticle_snapshot` / `reticle_query` | See the page (semantic tree) / find one specific element. |
|
|
10
|
-
| **act** |
|
|
10
|
+
| **act** | **`reticle_act_and_wait`** / `reticle_act_sequence` / `reticle_act` | **Act + name the consequence, one hop — reach for this first.** / batch a whole journey in one hop / move the app and prove nothing. |
|
|
11
11
|
| **observe** | `reticle_observe` / `reticle_wait_for` | Everything the app did after `since` / block until true. |
|
|
12
12
|
| **assert** | `reticle_assert` | Evaluate a predicate → `{ pass, evidence, failureReason? }`. The end of every loop. |
|
|
13
13
|
|
|
14
|
+
> **Only `reticle_act_and_wait` and `reticle_assert` produce a verdict.** Everything else moves or reads the app and proves nothing, so a drive ending without one of those two has no result however many tools it used. `reticle_act` is the tool agents reach for by habit and it catches nothing; `act_and_wait` is where defects actually surface. And `verified: "unknown"` is not a pass — it means Reticle drove the app and could not tell what happened. Report it as unknown.
|
|
15
|
+
|
|
14
16
|
`reticle_act` returns a `since` cursor — pass it to `reticle_observe({ since })` to scope the window. Elements are addressed by stable refs (`e7`) from `snapshot`/`query`; they re-resolve across re-renders.
|
|
15
17
|
|
|
16
18
|
**`assert`/`wait_for` are auto-scoped to your last act.** By default they only count events buffered _since_ the most recent act, so a stale signal from a previous step can't fake a pass — pass an explicit `since` to override. **Clicks run the code, not pixels:** `reticle_act` click fires the full pointer sequence on the element (no coordinate gesture for the HUD to intercept), reports `occluded:true` when something covers the target, and stays synthetic even with CDP configured (use `args:{ native:true }` for a trusted native click).
|
|
@@ -21,6 +23,16 @@ One screen to get fluent. Reticle is the **proof layer for AI agents** — no sc
|
|
|
21
23
|
- Need to wait without acting? `reticle_wait_for({ predicate: { kind: "settled", quietMs } })`.
|
|
22
24
|
- Waiting for a specific outcome? Pass that consequence as the predicate (`{ signal }` / `{ net }`), or `allOf` it with `{ kind: "settled" }` to wait for both the event _and_ the page going quiet.
|
|
23
25
|
|
|
26
|
+
**A predicate that does not parse produces NO verdict — not a failing one.** Nothing runs, so the drive ends with no result at all, which is strictly worse than a failure. The shapes below are the ones agents reach for most; the first column now works, but knowing the second saves you the round trip:
|
|
27
|
+
|
|
28
|
+
| You may write | It means |
|
|
29
|
+
| --- | --- |
|
|
30
|
+
| `{ kind: "text", text: "Saved" }` | `{ kind: "text", contains: "Saved" }` |
|
|
31
|
+
| `{ kind: "element", role: "button", text: "Save" }` | `{ kind: "element", query: { role, text } }` |
|
|
32
|
+
| `{ kind: "route", url: "/checkout" }` | `{ kind: "route", contains: "/checkout" }` |
|
|
33
|
+
|
|
34
|
+
If a predicate is still rejected, the error names the fields **that kind** accepts — read it rather than guessing again, and note `state` spells its selector `path` while `route` spells its `pathname`.
|
|
35
|
+
|
|
24
36
|
**Assert a consequence, not just presence.** `{ signal }` / `{ net }` prove the feature actually did something; `{ element }` / `{ text }` only prove something is on screen — which a stale render or a locator healed to the wrong element can fake. A _passing_ presence-only `reticle_assert` returns `advice` nudging you to a consequence; heed it on anything that matters.
|
|
25
37
|
|
|
26
38
|
## The 4-layer cross-check — never trust a green the state contradicts
|
|
@@ -71,7 +83,7 @@ Frequently useful but NOT core, so reach them through `reticle_run({ tool, args
|
|
|
71
83
|
## Start here
|
|
72
84
|
|
|
73
85
|
0. Just ran `reticle init` / started the dev server? Poll `reticle_sessions()` until your tab appears — readiness is server-internal now, so the first live call already blocks until the SDK connects.
|
|
74
|
-
1. `reticle_sessions` — find the connected tab (omit `sessionId` if there's only one).
|
|
86
|
+
1. `reticle_sessions` — find the connected tab (omit `sessionId` if there's only one). **An empty list is not a dead end: read the `why` field.** It names which case this is — no app running, an app running that has never dialled this daemon, a project that never went through `init`, or a tab that closed — and the fix for each. Do not fall back to static reasoning until you have read it.
|
|
75
87
|
2. `reticle_domain` — learn the app BEFORE testing: the saved flows, what each asserts, and the **gaps** (declared signals/testids that no flow verifies — untested intent). Tells you what to test and where the real risk is without crawling the whole app. Falls back to `reticle_capabilities` for the raw testable surface (`testids`, `signals`, `stores`, `flows`).
|
|
76
88
|
3. Run the loop: **look → act → observe → assert**, cross-checking the 4 layers on anything that matters.
|
|
77
89
|
|
package/docs/gate-plan.md
CHANGED
|
@@ -77,7 +77,7 @@ Nothing below is trustworthy until a gate result can be distinguished from a gat
|
|
|
77
77
|
- [x] **`init` support per client** — `init/mcp-clients.ts` registers Reticle with **seven** clients, not two. Every path and shape was read from that client's own documentation rather than recalled: Cursor/Windsurf/Gemini share `mcpServers`, VS Code uses `servers`, OpenCode uses `mcp` with the command as an **array**, Codex is TOML. A single "write mcp.json" assuming the Cursor shape produces a file three of them ignore — an install that reports success and registers nothing.
|
|
78
78
|
- Detection is conservative and one-directional: we write into a config a client **already has**, never creating `~/.gemini` or `~/.codeium` for somebody who does not use them.
|
|
79
79
|
- Codex is deliberately not auto-written. Editing TOML without a parser risks every _other_ server in the user's file; it returns MANUAL with an exact block.
|
|
80
|
-
- [x] **Per-client compat script** — `apps/e2e/client-compat.mjs`. Extracts the command from the config **exactly the way that client would**, runs it, and speaks MCP to it. 7/7 clients, 23 assertions, each advertising
|
|
80
|
+
- [x] **Per-client compat script** — `apps/e2e/client-compat.mjs`. Extracts the command from the config **exactly the way that client would**, runs it, and speaks MCP to it. 7/7 clients, 23 assertions, each advertising 18 tools.
|
|
81
81
|
- It proves only the half a machine can prove, and the verdict is named `runnable-unverified` so the distinction survives into the matrix. Whether the client _reads_ that path needs the real client, which is the submitted half.
|
|
82
82
|
- [x] **Submission flow** — `apps/e2e/matrix.mjs --validate`, run by CI's `matrix-records` job on every PR. The validator refuses what cannot be acted on: `works` without `checks.toolsVisible` (the self-report the flow exists to replace), `broken` without the client's verbatim error, or any record with no host or version. Its self-check runs first, because a validator that has never refused anything is not a validator. Contributor guide in [`docs/matrix/README.md`](./matrix/README.md).
|
|
83
83
|
- [x] **Generated `MATRIX.md`** — from submitted records, prettier-ignored because a generated file that fails `format:check` teaches people to skip the gate. **The current honest state is seven clients and ZERO ticks**: `◐` means a runnable entry was written where the client documents it and _nobody has run that client_, and the generated legend says so rather than rounding `◐` up to `✅`.
|
package/docs/gates.md
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
## 0. Last verified
|
|
10
10
|
|
|
11
|
-
Every gate below was executed end to end against `main` on **2026-08-
|
|
11
|
+
Every gate below was executed end to end against `main` on **2026-08-12** (macOS, M-series, `v2.6.0`). A green row means somebody watched it go green, not that it is supposed to be green.
|
|
12
12
|
|
|
13
13
|
| Gate | Result | Wall clock |
|
|
14
14
|
| --- | --- | --- |
|
|
@@ -33,7 +33,7 @@ Do **not** add telemetry inside a tool handler. If you find yourself wanting to,
|
|
|
33
33
|
|
|
34
34
|
`bug_found` fires once per OCCURRENCE. A defect hit five times in a session is five events, which is the right raw signal — frequency is what says which classes of defect actually cost anybody anything. But it means a naive count answers "how often were defects hit", not "how many defects were found", while looking like it answers the second.
|
|
35
35
|
|
|
36
|
-
So every `bug_found` carries **`repeat`**: false the first time a KIND is seen in a session, true after. Count `repeat: false` for **distinct defects**; count everything for **instances**. Measured on a real app
|
|
36
|
+
So every `bug_found` carries **`repeat`**: false the first time a KIND is seen in a session, true after. Count `repeat: false` for **distinct defects**; count everything for **instances**. Measured on a real app, the instance count was more than double the defect count. Publishing instances as defects inflates the claim accordingly.
|
|
37
37
|
|
|
38
38
|
The denominator is **`verification_completed`**, which fires per verdict with `via`, `verified`, `passed` and `falseGreenCaught`. Defects per verification is the honest rate; raw defect counts grow with usage and say nothing on their own.
|
|
39
39
|
|
|
@@ -98,10 +98,21 @@ The single exception is `daemon_stopped`, which is **awaited** — because the p
|
|
|
98
98
|
| `feedback_submitted` | `reticle feedback`, or an agent's report | the qualitative channel |
|
|
99
99
|
| `identified` | `reticle identify` | joins anonymous machine ids to a person who volunteered one |
|
|
100
100
|
| `mcp_client_connected` | an MCP client attached | how many sessions are agent-driven at all |
|
|
101
|
+
| `app_instrumented` | the first app carrying the SDK reached this daemon | **the funnel step everything turns on** — see below |
|
|
101
102
|
| `mcp_connection_lost` | the proxy lost its daemon | **the transport-stability metric.** The disconnect that makes a user reopen `/mcp` is invisible without it |
|
|
102
103
|
| `init_completed` | `reticle init` finished | does install actually work, outside the fixtures gate |
|
|
103
104
|
| `bug_found` | a defect was detected in the app under test | the value delivered, as opposed to the work done |
|
|
104
105
|
|
|
106
|
+
## The install has two halves — `app_instrumented`
|
|
107
|
+
|
|
108
|
+
Reticle is only usable when both halves are done: the MCP server is registered so the agent has the tools, and the SDK is loaded by a running page so there is something for those tools to look at. They are done by different commands, at different times, often in different directories. Almost everyone completes the first. The second is where the users go.
|
|
109
|
+
|
|
110
|
+
Nothing measured the second. `daemon_started` and `mcp_client_connected` describe the agent half. `session_appConnects` describes the app half but is a **window counter** — it resets on every flush, so a user whose app connected in one window reads zero in every other. The population it under-counts is precisely the population being measured, and a funnel built on it reported fewer instrumented users than there were users calling tools, which is impossible on its face.
|
|
111
|
+
|
|
112
|
+
`app_instrumented` fires **once per daemon run**, on the first session-ready only, so `daemon_started` → `app_instrumented` is a rate rather than an inference and a reloading page cannot inflate it. It carries `initialized` (had `init` run here), `agentAttached` (was an agent already waiting), and `msToFirstApp` (how long the daemon sat with nothing wired). It deliberately carries no stack and no framework: `project_profiled` already reports both for the same run, and the two join on `sessionId`.
|
|
113
|
+
|
|
114
|
+
What it still cannot see is **why** an app never connected — every cause for that is page-side (the non-localhost gate, a port mismatch, a stale build, a dev server never restarted), where the daemon has no visibility. That needs the SDK to report its own refusals, and it is the next thing to build.
|
|
115
|
+
|
|
105
116
|
## Sessions: `daemon_stopped` vs `session_progress`
|
|
106
117
|
|
|
107
118
|
Count sessions with **`daemon_stopped`** (`final: true`). It fires once, at a clean exit.
|
|
@@ -124,6 +135,7 @@ Four counters and one flag were added because the data could not answer question
|
|
|
124
135
|
| `verification.browser` | `verification_completed` | `headless` \| `headed` \| `attached` — who DROVE the browser. `attached` (Reticle launched nothing, the SDK connected from a browser somebody else opened) is the common case in production, so on its own this is mostly "somebody's own browser". |
|
|
125
136
|
| `verification.brand` | `verification_completed` | WHICH browser it was: `chrome` \| `edge` \| `arc` \| `dia` \| `brave` \| `opera` \| `firefox` \| `safari` \| `other` — the closed `BrowserBrand` list in core. The axis `engine` cannot answer, since Chrome, Edge, Arc, Dia and Brave are all `blink`. The SDK reads `navigator.userAgentData.brands` (and the UA string on Firefox/Safari, which expose no `userAgentData`) and normalises IN THE PAGE: a raw brand or UA string is unbounded and fingerprintable and never leaves. Anything unrecognised is `other`. **Omitted rather than `"unknown"`** when the page did not say — a desktop webview has no brand and an older SDK does not report one, and a guess is indistinguishable from a measurement on a dashboard. |
|
|
126
137
|
| `verification.reason` | `verification_completed` | WHICH clause of `decideVerified` produced the verdict, from core's closed `VerifiedReason`: `inconclusive` \| `observation_lost` \| `assertion_failed` \| `contradicted` \| `already_true` \| `unclean_capture` \| `vacuous_grade` \| `outcome_pending` \| `outcome_unread` \| `unsettled` \| `proved`. See below. |
|
|
138
|
+
| `verification.uncleanLoss` | `verification_completed` | WHAT was lost when `reason` is `unclean_capture`, from core's closed `CaptureLoss`: `buffer_loss` (our server ring buffer evicted evidence from the window) \| `transport_gap` (our browser queue overflowed) \| `blind_spot` (a boundary in the page — a cross-origin frame, a closed shadow root) \| `other`. Three owners, three fixes, and one bar on a dashboard until this existed. ONE value, not a list — a multi-value property is not something a breakdown can group by, so the first is sent, ours before the page's. **Absent whenever the capture was clean**, so its presence is itself the signal. Reported as `other` rather than omitted when the block says dirty and names nothing: a gap there would read as "no unclean verdicts happened". |
|
|
127
139
|
| `bug.attribution` | `bug_found` | `app` \| `request` \| `reticle` — whose fault the defect was. **Absent means unclassified**, never `app`. See below. |
|
|
128
140
|
| `outage.stage` / `outage.reason` / `outage.attempts` | `mcp_connection_lost` | which stage of the outage, why the stream went away (closed `OutageReason`, `other` for anything unnamed), and how many reconnects had been tried. See below. |
|
|
129
141
|
| `tzOffsetMin` | every event | minutes offset from UTC. One integer, no location. |
|
package/docs/telemetry.md
CHANGED
|
@@ -25,7 +25,7 @@ Thirteen kinds of events, each a single small JSON object:
|
|
|
25
25
|
| `cli_command_run` | You run a `reticle` command | Which subcommand (`verify`, `status`, …) and which flags were present, by name |
|
|
26
26
|
| `daemon_started` | The local daemon starts | — |
|
|
27
27
|
| `daemon_stopped` | The local daemon stops | A summary of the session — see below |
|
|
28
|
-
| `verification_completed` | A verification produces a verdict | Whether it passed,
|
|
28
|
+
| `verification_completed` | A verification produces a verdict | Whether it passed, whether Reticle refused to call a passing check verified, and **why** the verdict came out that way — see below |
|
|
29
29
|
| `project_profiled` | Once per daemon start | The shape of the project — see below |
|
|
30
30
|
| `version_changed` | You update or roll back | The two version numbers, and which direction |
|
|
31
31
|
| `runtime_crashed` | The daemon hits an uncaught error | The error's type, **Reticle's own** stack frames, and the message with variables stripped — see below |
|
|
@@ -44,6 +44,14 @@ Thirteen kinds of events, each a single small JSON object:
|
|
|
44
44
|
|
|
45
45
|
It also carries a snapshot of the **machine's** state — our own process's memory, free and total system RAM, load average and CPU count — taken at shutdown and again on any crash. This is what separates "your machine ran out of memory" from "Reticle has a bug", which otherwise produce identical-looking failures. No hostname, no username, no paths, no process list.
|
|
46
46
|
|
|
47
|
+
### Why a verdict came out that way
|
|
48
|
+
|
|
49
|
+
`verification_completed` carries the **clause** that decided the verdict, from a fixed list we define: `proved`, `contradicted`, `assertion_failed`, `already_true`, `unclean_capture`, `vacuous_grade`, `outcome_pending`, `outcome_unread`, `unsettled`, `observation_lost`, `inconclusive`.
|
|
50
|
+
|
|
51
|
+
It exists because `verified: "unknown"` covered seven different situations belonging to three different owners — your app, your agent, and Reticle's own blind spots — and they arrived as one value. It is a **name from our own vocabulary**, never a description of your app: `contradicted` says two channels disagreed, not which ones, about what, or on which page.
|
|
52
|
+
|
|
53
|
+
When the clause is `unclean_capture`, one further name says which of our three losses caused it — `buffer_loss` (our server's event buffer), `transport_gap` (our browser-side queue), `blind_spot` (a boundary in the page, such as a cross-origin frame), or `other`. All four are facts about **Reticle's** ability to observe, not about what it observed. This field is how we found that Reticle was refusing to answer over windows that were completely intact.
|
|
54
|
+
|
|
47
55
|
### Bugs Reticle finds
|
|
48
56
|
|
|
49
57
|
When Reticle catches a defect in the app it is verifying, it records **that a class of defect was found** — never what it was found in. The event carries the kind (`signal-contradicted`, `duplicate-request`, `console-error`, …) and how it surfaced, and nothing else: no selector, no URL, no element, no description of your app or its behaviour.
|
package/docs/token-efficiency.md
CHANGED
|
@@ -44,10 +44,10 @@ Measured live, all servers in one run, same tokenizer (`bench/harness/schema-tax
|
|
|
44
44
|
|
|
45
45
|
| MCP server | tools | schema tokens |
|
|
46
46
|
| ----------------------------------------- | ----: | ------------: |
|
|
47
|
-
| **Reticle — the tool surface** |
|
|
47
|
+
| **Reticle — the tool surface** | 18 | **~4,930** |
|
|
48
48
|
| Playwright MCP | 23 | 3,725 |
|
|
49
49
|
| Chrome DevTools MCP | 29 | 5,116 |
|
|
50
|
-
| Reticle — `RETICLE_ADVERTISE_ALL_TOOLS=1` | 48 | ~
|
|
50
|
+
| Reticle — `RETICLE_ADVERTISE_ALL_TOOLS=1` | 48 | ~30,200 |
|
|
51
51
|
|
|
52
52
|
There is one tool surface: the verify loop advertised directly, plus two meta-tools (`reticle_tools`, `reticle_run`) that reach every other tool on demand. Nothing is unreachable; the cold tail simply is not re-sent every turn.
|
|
53
53
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@reticlehq/server",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.7.0",
|
|
4
4
|
"description": "Reticle bridge + MCP server. Hosts the browser WS endpoint and exposes MCP tools to the coding agent.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
"@modelcontextprotocol/sdk": "^1.13.0",
|
|
23
23
|
"ws": "^8.18.0",
|
|
24
24
|
"zod": "^3.24.1",
|
|
25
|
-
"@reticlehq/core": "2.
|
|
25
|
+
"@reticlehq/core": "2.7.0"
|
|
26
26
|
},
|
|
27
27
|
"devDependencies": {
|
|
28
28
|
"@types/pngjs": "^6.0.5",
|