@reticlehq/server 2.8.0 → 2.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (292) hide show
  1. package/SKILL.md +130 -813
  2. package/dist/bridge/bridge.d.ts +23 -0
  3. package/dist/bridge/bridge.js +80 -2
  4. package/dist/bridge/bridge.test-harness.d.ts +5 -0
  5. package/dist/bridge/bridge.test-harness.js +6 -4
  6. package/dist/capsule/causal-summary.d.ts +16 -1
  7. package/dist/capsule/causal-summary.js +2 -1
  8. package/dist/cli/cli-doctor.js +41 -13
  9. package/dist/cli/cli-flow-commands.js +8 -8
  10. package/dist/cli/cli-launch.d.ts +0 -17
  11. package/dist/cli/cli-launch.js +0 -28
  12. package/dist/cli/cli-port.d.ts +24 -0
  13. package/dist/cli/cli-port.js +66 -35
  14. package/dist/cli/cli-verify.d.ts +21 -0
  15. package/dist/cli/cli-verify.js +45 -1
  16. package/dist/cli/config-discovery.d.ts +35 -0
  17. package/dist/cli/config-discovery.js +163 -0
  18. package/dist/cli/doctor-daemon-line.js +4 -1
  19. package/dist/cli/doctor-project-line.d.ts +37 -0
  20. package/dist/cli/doctor-project-line.js +35 -0
  21. package/dist/cli/doctor-rows.d.ts +51 -0
  22. package/dist/cli/doctor-rows.js +54 -0
  23. package/dist/cli/doctor-sessions-line.js +6 -3
  24. package/dist/cli/drive-attach.d.ts +99 -0
  25. package/dist/cli/drive-attach.js +203 -0
  26. package/dist/cli/drive-command.d.ts +13 -0
  27. package/dist/cli/drive-command.js +103 -0
  28. package/dist/cli/open-note.d.ts +20 -0
  29. package/dist/cli/open-note.js +37 -0
  30. package/dist/cli/status-next-action.d.ts +37 -0
  31. package/dist/cli/status-next-action.js +61 -0
  32. package/dist/cli.js +65 -70
  33. package/dist/events/contradictions.d.ts +32 -0
  34. package/dist/events/contradictions.js +9 -5
  35. package/dist/events/declared.d.ts +45 -0
  36. package/dist/events/declared.js +73 -0
  37. package/dist/events/echo-mismatch.d.ts +8 -2
  38. package/dist/events/echo-mismatch.js +33 -5
  39. package/dist/events/event-filters.d.ts +1 -1
  40. package/dist/events/event-filters.js +7 -4
  41. package/dist/events/predicate-eval.d.ts +17 -91
  42. package/dist/events/predicate-eval.js +85 -326
  43. package/dist/events/predicate-parse.js +31 -3
  44. package/dist/events/predicate-route.d.ts +32 -0
  45. package/dist/events/predicate-route.js +102 -0
  46. package/dist/events/predicate-schema.d.ts +99 -0
  47. package/dist/events/predicate-schema.js +309 -0
  48. package/dist/events/predicate.d.ts +8 -0
  49. package/dist/events/predicate.js +127 -5
  50. package/dist/flows/annotate-tools.js +3 -0
  51. package/dist/flows/annotate.js +18 -4
  52. package/dist/flows/coverage.d.ts +38 -5
  53. package/dist/flows/coverage.js +30 -4
  54. package/dist/flows/decision.d.ts +15 -0
  55. package/dist/flows/decision.js +1 -1
  56. package/dist/flows/flow-replay-run.js +8 -1
  57. package/dist/flows/flow-tools.js +9 -1
  58. package/dist/flows/replay.js +3 -0
  59. package/dist/honesty/accepted-write.d.ts +3 -0
  60. package/dist/honesty/accepted-write.js +8 -3
  61. package/dist/honesty/already-true.js +15 -0
  62. package/dist/honesty/blind-spots.js +4 -0
  63. package/dist/honesty/honesty.d.ts +11 -0
  64. package/dist/honesty/honesty.js +1 -0
  65. package/dist/honesty/uncaptured-bodies.d.ts +4 -2
  66. package/dist/honesty/uncaptured-bodies.js +9 -2
  67. package/dist/honesty/unread-outcome.d.ts +8 -0
  68. package/dist/honesty/unread-outcome.js +36 -9
  69. package/dist/honesty/unsettled.d.ts +9 -0
  70. package/dist/honesty/unsettled.js +7 -3
  71. package/dist/honesty/verified.d.ts +29 -3
  72. package/dist/honesty/verified.js +51 -8
  73. package/dist/http-server.d.ts +19 -0
  74. package/dist/http-server.js +202 -4
  75. package/dist/impact/impact-recorder.d.ts +23 -0
  76. package/dist/impact/impact-recorder.js +76 -0
  77. package/dist/impact/impact-store.d.ts +47 -0
  78. package/dist/impact/impact-store.js +171 -0
  79. package/dist/index.d.ts +7 -0
  80. package/dist/index.js +44 -10
  81. package/dist/init/agent-rules.d.ts +20 -0
  82. package/dist/init/agent-rules.js +36 -0
  83. package/dist/init/astro-patch.js +27 -1
  84. package/dist/init/confirm.d.ts +21 -3
  85. package/dist/init/confirm.js +32 -5
  86. package/dist/init/connect-steps.d.ts +2 -0
  87. package/dist/init/connect-steps.js +50 -0
  88. package/dist/init/csp-check.d.ts +38 -0
  89. package/dist/init/csp-check.js +92 -0
  90. package/dist/init/csp-doctor.d.ts +25 -0
  91. package/dist/init/csp-doctor.js +49 -0
  92. package/dist/init/cursor.js +6 -9
  93. package/dist/init/detect.d.ts +8 -0
  94. package/dist/init/detect.js +33 -3
  95. package/dist/init/mcp-clients.d.ts +1 -0
  96. package/dist/init/mcp-clients.js +51 -7
  97. package/dist/init/mcp.d.ts +15 -0
  98. package/dist/init/mcp.js +19 -0
  99. package/dist/init/plan-framework.d.ts +10 -0
  100. package/dist/init/plan-framework.js +25 -1
  101. package/dist/init/plan.d.ts +7 -2
  102. package/dist/init/plan.js +66 -30
  103. package/dist/init/run.js +11 -5
  104. package/dist/init/slash-command.d.ts +1 -1
  105. package/dist/init/slash-command.js +12 -4
  106. package/dist/init/snippets.d.ts +1 -1
  107. package/dist/init/snippets.js +35 -7
  108. package/dist/init/vite-config.js +24 -2
  109. package/dist/input/network-mock-tools.js +8 -1
  110. package/dist/input/scroll-find.d.ts +6 -0
  111. package/dist/input/scroll-find.js +17 -2
  112. package/dist/input/scroll-tools.js +4 -0
  113. package/dist/journal/attach-journal.d.ts +9 -1
  114. package/dist/journal/attach-journal.js +13 -2
  115. package/dist/journal/journal-query.d.ts +13 -0
  116. package/dist/journal/journal-query.js +28 -3
  117. package/dist/journal/workspace-gitignore.d.ts +10 -0
  118. package/dist/journal/workspace-gitignore.js +59 -0
  119. package/dist/mcp/mcp-proxy.d.ts +5 -66
  120. package/dist/mcp/mcp-proxy.js +180 -163
  121. package/dist/mcp/mcp.d.ts +10 -1
  122. package/dist/mcp/mcp.js +47 -27
  123. package/dist/mcp/proxy-handshake.d.ts +82 -1
  124. package/dist/mcp/proxy-handshake.js +109 -2
  125. package/dist/mcp/proxy-lifecycle.d.ts +10 -1
  126. package/dist/mcp/proxy-lifecycle.js +11 -2
  127. package/dist/mcp/proxy-log.d.ts +63 -0
  128. package/dist/mcp/proxy-log.js +132 -0
  129. package/dist/mcp/server-instructions.d.ts +17 -0
  130. package/dist/mcp/server-instructions.js +56 -0
  131. package/dist/mcp/shared-params.d.ts +27 -0
  132. package/dist/mcp/shared-params.js +36 -0
  133. package/dist/project/reticle-dir.d.ts +2 -0
  134. package/dist/project/reticle-dir.js +1 -0
  135. package/dist/session/connection-memory.d.ts +59 -0
  136. package/dist/session/connection-memory.js +108 -0
  137. package/dist/session/dev-command.d.ts +26 -0
  138. package/dist/session/dev-command.js +89 -0
  139. package/dist/session/dev-server-probe.d.ts +68 -0
  140. package/dist/session/dev-server-probe.js +102 -7
  141. package/dist/session/last-act.d.ts +26 -0
  142. package/dist/session/last-act.js +35 -1
  143. package/dist/session/live-control-tools.js +42 -5
  144. package/dist/session/no-session-diagnosis.d.ts +48 -2
  145. package/dist/session/no-session-diagnosis.js +176 -20
  146. package/dist/session/no-session-next-action.d.ts +46 -0
  147. package/dist/session/no-session-next-action.js +85 -0
  148. package/dist/session/no-session-watch.d.ts +40 -1
  149. package/dist/session/no-session-watch.js +165 -11
  150. package/dist/session/pending-commands.d.ts +12 -0
  151. package/dist/session/pending-commands.js +13 -1
  152. package/dist/session/session-manager.d.ts +4 -0
  153. package/dist/session/session-manager.js +35 -5
  154. package/dist/session/session.d.ts +10 -1
  155. package/dist/session/session.js +146 -5
  156. package/dist/telemetry/app-instrumented.d.ts +8 -0
  157. package/dist/telemetry/app-instrumented.js +10 -0
  158. package/dist/telemetry/bug-found.d.ts +18 -0
  159. package/dist/telemetry/bug-found.js +14 -0
  160. package/dist/telemetry/daemon-telemetry.js +24 -0
  161. package/dist/telemetry/install-source.d.ts +20 -0
  162. package/dist/telemetry/install-source.js +34 -0
  163. package/dist/telemetry/instrumentation-stall.d.ts +50 -0
  164. package/dist/telemetry/instrumentation-stall.js +90 -0
  165. package/dist/telemetry/session-metrics.d.ts +8 -0
  166. package/dist/telemetry/session-metrics.js +10 -0
  167. package/dist/telemetry/telemetry.d.ts +3 -1
  168. package/dist/telemetry/telemetry.js +13 -2
  169. package/dist/tools/act-tools.js +166 -29
  170. package/dist/tools/assert-verdict.js +32 -7
  171. package/dist/tools/browser-tools.js +10 -0
  172. package/dist/tools/dynamic-tools.js +16 -7
  173. package/dist/tools/error-recovery.js +9 -5
  174. package/dist/tools/invoke-tool.js +20 -3
  175. package/dist/tools/lease-hint.d.ts +30 -8
  176. package/dist/tools/lease-hint.js +65 -14
  177. package/dist/tools/lease-tools.d.ts +11 -0
  178. package/dist/tools/lease-tools.js +141 -96
  179. package/dist/tools/observe-tools.js +9 -3
  180. package/dist/tools/react-grace.d.ts +4 -0
  181. package/dist/tools/react-grace.js +6 -3
  182. package/dist/tools/read-tools.js +47 -1
  183. package/dist/tools/resolve-target.d.ts +33 -0
  184. package/dist/tools/resolve-target.js +53 -0
  185. package/dist/tools/settle-in-flight.d.ts +15 -0
  186. package/dist/tools/settle-in-flight.js +42 -2
  187. package/dist/tools/snapshot-delta.d.ts +3 -0
  188. package/dist/tools/snapshot-delta.js +67 -6
  189. package/dist/tools/tool-kit.d.ts +8 -0
  190. package/dist/tools/tool-surface.d.ts +60 -0
  191. package/dist/tools/tool-surface.js +85 -19
  192. package/dist/tools/tools.js +45 -4
  193. package/dist/tools/unadvertised-help.js +9 -6
  194. package/docs/README.md +1 -2
  195. package/docs/actions.mdx +41 -8
  196. package/docs/agent-cheatsheet.md +4 -2
  197. package/docs/architecture.md +6 -0
  198. package/docs/benchmarks.md +60 -12
  199. package/docs/best-practices.mdx +11 -6
  200. package/docs/capabilities.mdx +201 -54
  201. package/docs/cli/affected.mdx +6 -4
  202. package/docs/cli/capsules.mdx +9 -8
  203. package/docs/cli/cloud.mdx +30 -4
  204. package/docs/cli/doctor.mdx +41 -8
  205. package/docs/cli/drive.mdx +34 -15
  206. package/docs/cli/gate.mdx +6 -6
  207. package/docs/cli/help.mdx +8 -6
  208. package/docs/cli/hunt.mdx +18 -2
  209. package/docs/cli/identify.mdx +16 -2
  210. package/docs/cli/init.mdx +35 -8
  211. package/docs/cli/kill.mdx +5 -5
  212. package/docs/cli/license.mdx +1 -1
  213. package/docs/cli/open.mdx +6 -0
  214. package/docs/cli/restart.mdx +6 -6
  215. package/docs/cli/serve.mdx +10 -3
  216. package/docs/cli/status.mdx +29 -5
  217. package/docs/cli/stop.mdx +8 -0
  218. package/docs/cli/verify.mdx +20 -1
  219. package/docs/cli/version.mdx +1 -1
  220. package/docs/cli/watch.mdx +9 -2
  221. package/docs/cli.mdx +44 -22
  222. package/docs/debugging.md +3 -1
  223. package/docs/deploy-checks.md +30 -1
  224. package/docs/desktop.mdx +276 -80
  225. package/docs/docs.json +13 -12
  226. package/docs/enterprise.md +2 -0
  227. package/docs/faq.mdx +49 -35
  228. package/docs/fixtures.md +2 -0
  229. package/docs/flows.md +3 -1
  230. package/docs/for-agents.md +3 -3
  231. package/docs/frameworks.mdx +46 -7
  232. package/docs/gate-plan.md +4 -2
  233. package/docs/gates.md +7 -5
  234. package/docs/getting-started.md +53 -12
  235. package/docs/human-control.md +2 -0
  236. package/docs/index.mdx +43 -1
  237. package/docs/install-agentic.mdx +13 -4
  238. package/docs/install-manual.mdx +6 -4
  239. package/docs/instrumentation.mdx +7 -1
  240. package/docs/integration-patterns.md +3 -1
  241. package/docs/local-registry.md +4 -2
  242. package/docs/multi-agent-testing.md +3 -1
  243. package/docs/packages/babel-plugin.mdx +15 -6
  244. package/docs/packages/browser.mdx +2 -2
  245. package/docs/packages/core.mdx +26 -16
  246. package/docs/packages/electron.mdx +5 -7
  247. package/docs/packages/eslint-plugin.mdx +5 -3
  248. package/docs/packages/next.mdx +3 -5
  249. package/docs/packages/react.mdx +3 -3
  250. package/docs/packages/server.mdx +2 -2
  251. package/docs/packages/tauri.mdx +6 -6
  252. package/docs/packages/test.mdx +18 -7
  253. package/docs/packages/vite-plugin.mdx +4 -6
  254. package/docs/packages.mdx +14 -13
  255. package/docs/platform-integration.md +2 -0
  256. package/docs/predicates.mdx +76 -7
  257. package/docs/quickstart.mdx +51 -5
  258. package/docs/recipes.mdx +62 -22
  259. package/docs/skill-file.mdx +3 -1
  260. package/docs/state-management.mdx +2 -0
  261. package/docs/system-map.md +3 -1
  262. package/docs/telemetry-contract.md +16 -5
  263. package/docs/telemetry-events.mdx +63 -18
  264. package/docs/telemetry.md +8 -5
  265. package/docs/testing.md +35 -1
  266. package/docs/token-efficiency.md +8 -2
  267. package/docs/tools-act-and-wait.mdx +64 -14
  268. package/docs/tools-act-sequence.mdx +34 -17
  269. package/docs/tools-act.mdx +24 -11
  270. package/docs/tools-assert.mdx +69 -10
  271. package/docs/tools-console.mdx +11 -7
  272. package/docs/tools-inspect.mdx +11 -8
  273. package/docs/tools-navigate.mdx +41 -5
  274. package/docs/tools-network.mdx +42 -9
  275. package/docs/tools-observe.mdx +66 -10
  276. package/docs/tools-overview.mdx +26 -23
  277. package/docs/tools-query.mdx +52 -8
  278. package/docs/tools-session-and-feedback.mdx +30 -9
  279. package/docs/tools-sessions.mdx +39 -10
  280. package/docs/tools-snapshot.mdx +57 -3
  281. package/docs/tools-state.mdx +24 -1
  282. package/docs/tools-tools-and-run.mdx +40 -9
  283. package/docs/tools-wait-for.mdx +34 -15
  284. package/docs/troubleshooting.mdx +80 -10
  285. package/docs/usage.md +43 -21
  286. package/docs/vs-chrome-devtools-mcp.mdx +41 -0
  287. package/docs/vs-playwright-mcp.mdx +42 -2
  288. package/docs/vs-screenshots.mdx +42 -1
  289. package/docs/what-is-reticle.mdx +1 -1
  290. package/docs/why-reticle.mdx +3 -1
  291. package/package.json +5 -5
  292. package/docs/desktop-apps.md +0 -265
@@ -0,0 +1,33 @@
1
+ /** One candidate the browser returned for a target query. */
2
+ export interface TargetCandidate {
3
+ ref?: unknown;
4
+ role?: unknown;
5
+ name?: unknown;
6
+ visible?: unknown;
7
+ }
8
+ /** What the resolution produced: a ref to act on, or the reason there isn't one. */
9
+ export type TargetResolution = {
10
+ readonly kind: 'ref';
11
+ readonly ref: string;
12
+ } | {
13
+ readonly kind: 'error';
14
+ readonly message: string;
15
+ };
16
+ /**
17
+ * Turn a target QUERY into the single ref an action can be performed on.
18
+ *
19
+ * `act_and_wait` took only a `ref`, so every verification cost a `reticle_query` turn first just to
20
+ * learn one string. The advertised tool surface is re-sent on every turn, so that extra turn was not
21
+ * a small cost: measured on the wire, a two-turn verification spent 10,756 of its 11,235 tokens on
22
+ * schema and 479 on the actual answers. Removing the turn removes half the schema bill.
23
+ *
24
+ * Ambiguity is an ERROR, never a pick. Choosing "the first match" would let a verification act on a
25
+ * different element than the author meant and still report success, which is the false green this
26
+ * product exists to prevent — and the failure is invisible, because the verdict describes the
27
+ * element it DID act on. The message names the candidates so the caller can narrow rather than guess.
28
+ *
29
+ * Invisible matches are filtered before counting: a hidden duplicate is a common way for a
30
+ * legitimately unambiguous query to look ambiguous, and refusing on it would push callers back to
31
+ * the two-turn path for no gain.
32
+ */
33
+ export declare function resolveTargetRef(candidates: readonly unknown[]): TargetResolution;
@@ -0,0 +1,53 @@
1
+ import { asRecord } from './tools-helpers.js';
2
+ /**
3
+ * Turn a target QUERY into the single ref an action can be performed on.
4
+ *
5
+ * `act_and_wait` took only a `ref`, so every verification cost a `reticle_query` turn first just to
6
+ * learn one string. The advertised tool surface is re-sent on every turn, so that extra turn was not
7
+ * a small cost: measured on the wire, a two-turn verification spent 10,756 of its 11,235 tokens on
8
+ * schema and 479 on the actual answers. Removing the turn removes half the schema bill.
9
+ *
10
+ * Ambiguity is an ERROR, never a pick. Choosing "the first match" would let a verification act on a
11
+ * different element than the author meant and still report success, which is the false green this
12
+ * product exists to prevent — and the failure is invisible, because the verdict describes the
13
+ * element it DID act on. The message names the candidates so the caller can narrow rather than guess.
14
+ *
15
+ * Invisible matches are filtered before counting: a hidden duplicate is a common way for a
16
+ * legitimately unambiguous query to look ambiguous, and refusing on it would push callers back to
17
+ * the two-turn path for no gain.
18
+ */
19
+ export function resolveTargetRef(candidates) {
20
+ const all = candidates.map((c) => asRecord(c));
21
+ const visible = all.filter((c) => c.visible !== false);
22
+ const usable = visible.length > 0 ? visible : all;
23
+ if (0 === usable.length) {
24
+ return {
25
+ kind: 'error',
26
+ message: 'target matched no element. Nothing was acted on and no verdict is possible — widen the ' +
27
+ 'query, or take a reticle_snapshot to see what is actually on the page.',
28
+ };
29
+ }
30
+ if (usable.length > 1) {
31
+ const named = usable
32
+ .slice(0, 5)
33
+ .map((c) => {
34
+ const role = 'string' === typeof c.role ? c.role : '?';
35
+ const name = 'string' === typeof c.name ? c.name : '';
36
+ const ref = 'string' === typeof c.ref ? c.ref : '?';
37
+ return name.length > 0 ? `${ref} (${role} "${name}")` : `${ref} (${role})`;
38
+ })
39
+ .join(', ');
40
+ return {
41
+ kind: 'error',
42
+ message: `target matched ${String(usable.length)} elements and an action must not guess between ` +
43
+ `them: ${named}. Narrow the query (add role/name/testid or a scope), or pass an explicit ` +
44
+ '`ref` from reticle_query.',
45
+ };
46
+ }
47
+ const only = usable[0];
48
+ const ref = only === undefined ? undefined : only.ref;
49
+ if ('string' !== typeof ref || 0 === ref.length) {
50
+ return { kind: 'error', message: 'target matched an element with no usable ref.' };
51
+ }
52
+ return { kind: 'ref', ref };
53
+ }
@@ -62,6 +62,21 @@ export declare function inFlightRequestLabels(events: readonly {
62
62
  type: string;
63
63
  data: Record<string, unknown>;
64
64
  }[]): string[];
65
+ /**
66
+ * Endpoints called MORE THAN ONCE in this window, busiest first, as "METHOD url ×N".
67
+ *
68
+ * The other half of an honest `unsettled`. A retrying query against a dead backend leaves nothing in
69
+ * flight — every attempt comes back, then the next one fires — so the verdict fell through to "what
70
+ * kept the page busy was its own churn (a poll, a timer, an animation)", which names three things
71
+ * and points at none of them. Measured in the field: on an app with a background retry loop
72
+ * `act_and_wait` could never return a pass, and nothing in the verdict said which endpoint to look
73
+ * at. This does not judge the repetition — a poll is legitimate — it reports it, which is all an
74
+ * agent needs to tell "my assertion was wrong" from "this app never quiets".
75
+ */
76
+ export declare function repeatedRequestLabels(events: readonly {
77
+ type: string;
78
+ data: Record<string, unknown>;
79
+ }[]): string[];
65
80
  export interface SettleResult {
66
81
  /** True when nothing this window started is still outstanding. */
67
82
  settled: boolean;
@@ -27,7 +27,16 @@
27
27
  * - still in flight when the budget genuinely runs out → `unsettled`, as before, but now an honest
28
28
  * limit instead of an early exit.
29
29
  */
30
- import { EventType } from '@reticlehq/core';
30
+ import { EventType, isDevToolingUrl } from '@reticlehq/core';
31
+ /**
32
+ * The dev toolchain talking about ITSELF is not the app finishing its work.
33
+ *
34
+ * `evalSettled` and `findContradictions` both drop it already; this file read the raw window, so the
35
+ * exclusion was cosmetic exactly where it decides a verdict. Reported from a real drive: a
36
+ * deliberately disabled control — no state change, no storage change, zero application requests —
37
+ * was graded on the strength of a Next webpack hot-update the same payload printed as ignored.
38
+ */
39
+ const isDevTooling = (data) => isDevToolingUrl('string' === typeof data['url'] ? data['url'] : undefined);
31
40
  /** How often to re-read the buffer. Short enough to return promptly, long enough not to spin. */
32
41
  const POLL_MS = 50;
33
42
  const idOf = (data) => 'string' === typeof data['id'] ? data['id'] : undefined;
@@ -49,7 +58,7 @@ export function inFlightRequestIds(events) {
49
58
  }
50
59
  const open = [];
51
60
  for (const e of events) {
52
- if (e.type !== EventType.NET_PENDING)
61
+ if (e.type !== EventType.NET_PENDING || isDevTooling(e.data))
53
62
  continue;
54
63
  const id = idOf(e.data);
55
64
  if (id !== undefined && !settled.has(id) && !open.includes(id))
@@ -81,6 +90,37 @@ export function inFlightRequestLabels(events) {
81
90
  }
82
91
  return labels;
83
92
  }
93
+ /** Enough to recognise the loop; a busy dashboard must not spend the verdict on a URL list. */
94
+ const MAX_REPEATED = 3;
95
+ /** One call is traffic. Two to the same endpoint inside one window is a repeat. */
96
+ const MIN_REPEATS = 2;
97
+ /**
98
+ * Endpoints called MORE THAN ONCE in this window, busiest first, as "METHOD url ×N".
99
+ *
100
+ * The other half of an honest `unsettled`. A retrying query against a dead backend leaves nothing in
101
+ * flight — every attempt comes back, then the next one fires — so the verdict fell through to "what
102
+ * kept the page busy was its own churn (a poll, a timer, an animation)", which names three things
103
+ * and points at none of them. Measured in the field: on an app with a background retry loop
104
+ * `act_and_wait` could never return a pass, and nothing in the verdict said which endpoint to look
105
+ * at. This does not judge the repetition — a poll is legitimate — it reports it, which is all an
106
+ * agent needs to tell "my assertion was wrong" from "this app never quiets".
107
+ */
108
+ export function repeatedRequestLabels(events) {
109
+ const counts = new Map();
110
+ for (const e of events) {
111
+ if (e.type !== EventType.NET_REQUEST || isDevTooling(e.data))
112
+ continue;
113
+ const method = 'string' === typeof e.data['method'] ? e.data['method'] : 'request';
114
+ const url = 'string' === typeof e.data['url'] ? e.data['url'] : 'an unreported url';
115
+ const key = `${method} ${url}`;
116
+ counts.set(key, (counts.get(key) ?? 0) + 1);
117
+ }
118
+ return [...counts.entries()]
119
+ .filter(([, count]) => count >= MIN_REPEATS)
120
+ .sort(([, a], [, b]) => b - a)
121
+ .slice(0, MAX_REPEATED)
122
+ .map(([key, count]) => `${key} ×${String(count)}`);
123
+ }
84
124
  /**
85
125
  * Wait, bounded by `budgetMs`, for the requests this window started to come back.
86
126
  *
@@ -21,6 +21,7 @@ export type SnapshotDeltaMode = (typeof SnapshotDeltaMode)[keyof typeof Snapshot
21
21
  interface SnapshotDelta {
22
22
  added: string[];
23
23
  removed: string[];
24
+ changed: string[];
24
25
  addedCount: number;
25
26
  removedCount: number;
26
27
  }
@@ -45,6 +46,8 @@ export declare function snapshotDelta(prevTree: string | undefined, nextTree: st
45
46
  export declare class SnapshotCache {
46
47
  #private;
47
48
  constructor(max?: number);
49
+ /** True when an entry exists for this key (regardless of route match). */
50
+ has(key: string): boolean;
48
51
  /** Last tree for this key IF the route still matches; undefined when absent or route changed. */
49
52
  recall(key: string, route: string): string | undefined;
50
53
  remember(key: string, route: string, tree: string): void;
@@ -63,6 +63,17 @@ function deltaLines(tree) {
63
63
  * buttons — and the count is the only thing that says how many left. Emitting the ORIGINAL text is
64
64
  * what finally tells them apart: with refs, "which twelve" has an answer.
65
65
  */
66
+ /** Extract the ref marker value from a text line, or undefined if absent. */
67
+ function refOf(text) {
68
+ return REF_MARKER.exec(text)?.[0]?.trim();
69
+ }
70
+ /** Extract the role (first word after the bullet) from a snapshot line — the structural type that must match. */
71
+ function roleOf(text) {
72
+ return (text
73
+ .trimStart()
74
+ .replace(/^-\s*/, '')
75
+ .split(/[\s"(]/)[0] ?? '');
76
+ }
66
77
  function diffKeyed(prev, next) {
67
78
  const bucket = (lines) => {
68
79
  const map = new Map();
@@ -96,6 +107,39 @@ function diffKeyed(prev, next) {
96
107
  }
97
108
  return { removed, added };
98
109
  }
110
+ /**
111
+ * Same ref AND same role in both added and removed = value change, not a structural
112
+ * arrival/departure. Refs alone are not stable identities — they are re-minted on re-render — so a
113
+ * removed button and an added textbox sharing a ref must stay in their respective lists.
114
+ */
115
+ function extractChanged(added, removed) {
116
+ const addedByRef = new Map();
117
+ for (const [i, text] of added.entries()) {
118
+ const ref = refOf(text);
119
+ if (ref !== undefined)
120
+ addedByRef.set(ref, { idx: i, text, role: roleOf(text) });
121
+ }
122
+ const changedIndicesInAdded = new Set();
123
+ const changedIndicesInRemoved = new Set();
124
+ const changed = [];
125
+ for (const [i, text] of removed.entries()) {
126
+ const ref = refOf(text);
127
+ if (ref === undefined)
128
+ continue;
129
+ const match = addedByRef.get(ref);
130
+ if (match !== undefined && match.role === roleOf(text)) {
131
+ changed.push(match.text);
132
+ changedIndicesInAdded.add(match.idx);
133
+ changedIndicesInRemoved.add(i);
134
+ addedByRef.delete(ref);
135
+ }
136
+ }
137
+ return {
138
+ added: added.filter((_, i) => !changedIndicesInAdded.has(i)),
139
+ removed: removed.filter((_, i) => !changedIndicesInRemoved.has(i)),
140
+ changed,
141
+ };
142
+ }
99
143
  /**
100
144
  * Where focus sits now: the line to show, plus the identity to compare it BY.
101
145
  *
@@ -123,7 +167,8 @@ export const SnapshotDeltaMode = {
123
167
  export function snapshotDelta(prevTree, nextTree) {
124
168
  if (prevTree === undefined)
125
169
  return { mode: SnapshotDeltaMode.FULL };
126
- const { added, removed } = diffKeyed(deltaLines(prevTree), deltaLines(nextTree));
170
+ const raw = diffKeyed(deltaLines(prevTree), deltaLines(nextTree));
171
+ const { added, removed, changed } = extractChanged(raw.added, raw.removed);
127
172
  const before = focusedLine(prevTree);
128
173
  const now = focusedLine(nextTree);
129
174
  // Reported only on a MOVE. Repeating "focus is still here" every turn is the noise the delta exists
@@ -135,12 +180,18 @@ export function snapshotDelta(prevTree, nextTree) {
135
180
  ...(now === undefined ? {} : { to: now.text }),
136
181
  };
137
182
  const moved = focusChanged === undefined ? {} : { focusChanged };
138
- if (0 === added.length && 0 === removed.length) {
183
+ if (0 === added.length && 0 === removed.length && 0 === changed.length) {
139
184
  return { mode: SnapshotDeltaMode.UNCHANGED, ...moved };
140
185
  }
141
186
  return {
142
187
  mode: SnapshotDeltaMode.DELTA,
143
- delta: { added, removed, addedCount: added.length, removedCount: removed.length },
188
+ delta: {
189
+ added,
190
+ removed,
191
+ changed,
192
+ addedCount: added.length,
193
+ removedCount: removed.length,
194
+ },
144
195
  ...moved,
145
196
  };
146
197
  }
@@ -152,6 +203,10 @@ export class SnapshotCache {
152
203
  constructor(max = DEFAULT_MAX_ENTRIES) {
153
204
  this.#max = max;
154
205
  }
206
+ /** True when an entry exists for this key (regardless of route match). */
207
+ has(key) {
208
+ return this.#map.has(key);
209
+ }
155
210
  /** Last tree for this key IF the route still matches; undefined when absent or route changed. */
156
211
  recall(key, route) {
157
212
  const entry = this.#map.get(key);
@@ -210,10 +265,13 @@ export function applySnapshotDelta(raw, opts, cache) {
210
265
  return raw;
211
266
  }
212
267
  const prev = cache.recall(key, route);
268
+ const hadEntry = cache.has(key);
213
269
  cache.remember(key, route, tree);
214
270
  const decision = snapshotDelta(prev, tree);
215
- if (decision.mode === SnapshotDeltaMode.FULL)
216
- return raw;
271
+ if (decision.mode === SnapshotDeltaMode.FULL) {
272
+ const reason = hadEntry ? 'route changed' : 'first snapshot for this route';
273
+ return { ...r, mode: SnapshotDeltaMode.FULL, reason };
274
+ }
217
275
  // The walk stops at a node cap and returns a DOCUMENT-ORDER PREFIX, so two capped snapshots of a
218
276
  // large page are identical whenever the change happened past the cap — and "unchanged" is then a
219
277
  // statement about the cap, not about the page. Carried through on both branches: a delta computed
@@ -226,9 +284,12 @@ export function applySnapshotDelta(raw, opts, cache) {
226
284
  if (decision.mode === SnapshotDeltaMode.UNCHANGED) {
227
285
  return { mode: SnapshotDeltaMode.UNCHANGED, status: r['status'], ...moved, ...capped };
228
286
  }
287
+ const { changed, ...structuralDelta } = decision.delta;
288
+ const changedField = changed.length > 0 ? { changed, changedCount: changed.length } : {};
229
289
  return {
230
290
  mode: SnapshotDeltaMode.DELTA,
231
- delta: decision.delta,
291
+ delta: structuralDelta,
292
+ ...changedField,
232
293
  status: r['status'],
233
294
  ...moved,
234
295
  ...capped,
@@ -15,10 +15,18 @@ import type { FlowStore } from '../flows/flows.js';
15
15
  import type { ProjectStore } from '../project/project-store.js';
16
16
  import type { AnnotationStore } from '../flows/annotation-store.js';
17
17
  import type { BrowserPool } from '../pool/browser-pool.js';
18
+ import type { ChromiumProbe } from '../cli/chromium-hint.js';
18
19
  export interface ToolDeps {
19
20
  sessions: SessionManager;
20
21
  /** shared one-browser/N-context pool for headless leases. undefined ⇒ lease tools report unavailable. */
21
22
  pool?: BrowserPool;
23
+ /**
24
+ * Probe for a launchable Chromium, so the lease path can refuse at the FIRST call with the install
25
+ * fix rather than failing several calls in with a message that reads as "the app is not running".
26
+ * Optional: absent ⇒ no preflight — every existing test construction of ToolDeps keeps working, and
27
+ * the fake-pool lease tests are deliberately runnable without a real Chromium.
28
+ */
29
+ browserProbe?: () => Promise<ChromiumProbe>;
22
30
  baselines: BaselineStore;
23
31
  recordings: RecordingStore;
24
32
  /** on-disk anchored-flow store (.reticle/flows/). */
@@ -40,6 +40,36 @@ export declare const TOOL_SURFACE: {
40
40
  readonly DEFAULT: "default";
41
41
  /** Every tool advertised directly, WITH output schemas. A verification switch — see above. */
42
42
  readonly ALL: "all";
43
+ /**
44
+ * The smallest surface that can still produce a VERDICT. Not a profile — a cost switch.
45
+ *
46
+ * Measured on the wire: a verification that names its own target costs one `act_and_wait` call,
47
+ * and 5,480 of its 5,909 tokens are the advertised surface re-sent for that single turn. The
48
+ * answers cost 430. So on the path where the caller already knows what to assert, almost the
49
+ * entire bill is the menu, and the menu is the thing to cut.
50
+ *
51
+ * Deliberately NOT the default, and now MEASURED rather than inherited. Run over the same 30-bug
52
+ * set that the default surface scores 23/27 detection and 2/29 false alarms on:
53
+ *
54
+ * detection 24/28 (held)
55
+ * FALSE ALARMS 7/30 (was 2/29 — TRIPLED)
56
+ * tokens 113,599 per run (was 179,959 — 37% cheaper)
57
+ *
58
+ * The five new false alarms are not scattered. They are `mutation-leak` and
59
+ * `generate-blast-filter` (state), `kpi-deploys-tamper` (business-logic), `debounce-broken`
60
+ * (timing) and `route-stuck-deployments` (routing) — precisely the classes whose evidence lives in
61
+ * `reticle_state`, `reticle_network` and `reticle_observe`, which this surface does not advertise.
62
+ * Strip the observation tools and the model stops observing: it reaches for the verdict without
63
+ * the evidence and calls a healthy build broken.
64
+ *
65
+ * So the retired `dynamic` finding is confirmed, with a sharper mechanism than "accuracy drops".
66
+ * 37% of the tokens is not worth trading for the one metric this product actually wins on, and
67
+ * this must not become the default on the strength of the token number alone.
68
+ *
69
+ * It remains correct for a caller who has ALREADY decided what to assert — there the assertion
70
+ * supplies the evidence the surface would otherwise have to go and find.
71
+ */
72
+ readonly VERIFY: "verify";
43
73
  };
44
74
  export type ToolSurface = (typeof TOOL_SURFACE)[keyof typeof TOOL_SURFACE];
45
75
  /**
@@ -49,6 +79,8 @@ export type ToolSurface = (typeof TOOL_SURFACE)[keyof typeof TOOL_SURFACE];
49
79
  * users to shop among alternatives that did not meaningfully differ.
50
80
  */
51
81
  export declare const ADVERTISE_ALL_ENV = "RETICLE_ADVERTISE_ALL_TOOLS";
82
+ /** Opt into the smallest verdict-capable surface. Read by the DAEMON at startup, like the others. */
83
+ export declare const VERIFY_SURFACE_ENV = "RETICLE_VERIFY_SURFACE";
52
84
  /**
53
85
  * The retired setting, still read so nobody's shell profile breaks.
54
86
  *
@@ -58,6 +90,34 @@ export declare const ADVERTISE_ALL_ENV = "RETICLE_ADVERTISE_ALL_TOOLS";
58
90
  */
59
91
  export declare const TOOL_PROFILE_ENV = "RETICLE_TOOL_PROFILE";
60
92
  export declare const CORE_TOOL_NAMES: ReadonlySet<string>;
93
+ /**
94
+ * The extended surface: what `all` advertises BEYOND the default set.
95
+ *
96
+ * `all` used to mean "every tool in the registry", and that is no longer allowed to be true. Cursor
97
+ * enforces a limit of 40 tools across every connected MCP server COMBINED, so a server advertising
98
+ * 48 by itself can push a user's other servers out or be dropped wholesale. The budget is a count,
99
+ * so no amount of trimming parameter prose buys anything back — only advertising fewer names does.
100
+ *
101
+ * Everything omitted stays in the registry, stays catalogued by `reticle_tools`, and stays callable
102
+ * by name through `reticle_run { tool, args }`. The cost is one discovery hop on the cold tail, which
103
+ * is the same trade the default surface has always made, applied one level further out.
104
+ *
105
+ * What it costs that is NOT free: `all` is the only surface that carries `outputSchema`, which is
106
+ * what makes the MCP layer validate tool OUTPUT — the check that once caught `reticle_verify_change`
107
+ * returning a payload its own schema rejected. The tools left off this list no longer get that
108
+ * validation from the wire. That defect class is now uncovered for them, and it is written down here
109
+ * rather than discovered later.
110
+ *
111
+ * Chosen as the capabilities an agent plausibly reaches for once the verify loop is not enough:
112
+ * orientation, the record/replay flow loop, visual evidence, and the three fault-injection controls.
113
+ */
114
+ export declare const EXTENDED_TOOL_NAMES: ReadonlySet<string>;
115
+ /**
116
+ * The verify surface: one acting tool that returns a verdict, plus the two meta-tools that reach
117
+ * everything else. `act_and_wait` can resolve its own target, so no query tool is needed to name an
118
+ * element — that round trip was half the token cost of a verification.
119
+ */
120
+ export declare const VERIFY_TOOL_NAMES: ReadonlySet<string>;
61
121
  /**
62
122
  * Which surface is live.
63
123
  *
@@ -40,6 +40,36 @@ export const TOOL_SURFACE = {
40
40
  DEFAULT: 'default',
41
41
  /** Every tool advertised directly, WITH output schemas. A verification switch — see above. */
42
42
  ALL: 'all',
43
+ /**
44
+ * The smallest surface that can still produce a VERDICT. Not a profile — a cost switch.
45
+ *
46
+ * Measured on the wire: a verification that names its own target costs one `act_and_wait` call,
47
+ * and 5,480 of its 5,909 tokens are the advertised surface re-sent for that single turn. The
48
+ * answers cost 430. So on the path where the caller already knows what to assert, almost the
49
+ * entire bill is the menu, and the menu is the thing to cut.
50
+ *
51
+ * Deliberately NOT the default, and now MEASURED rather than inherited. Run over the same 30-bug
52
+ * set that the default surface scores 23/27 detection and 2/29 false alarms on:
53
+ *
54
+ * detection 24/28 (held)
55
+ * FALSE ALARMS 7/30 (was 2/29 — TRIPLED)
56
+ * tokens 113,599 per run (was 179,959 — 37% cheaper)
57
+ *
58
+ * The five new false alarms are not scattered. They are `mutation-leak` and
59
+ * `generate-blast-filter` (state), `kpi-deploys-tamper` (business-logic), `debounce-broken`
60
+ * (timing) and `route-stuck-deployments` (routing) — precisely the classes whose evidence lives in
61
+ * `reticle_state`, `reticle_network` and `reticle_observe`, which this surface does not advertise.
62
+ * Strip the observation tools and the model stops observing: it reaches for the verdict without
63
+ * the evidence and calls a healthy build broken.
64
+ *
65
+ * So the retired `dynamic` finding is confirmed, with a sharper mechanism than "accuracy drops".
66
+ * 37% of the tokens is not worth trading for the one metric this product actually wins on, and
67
+ * this must not become the default on the strength of the token number alone.
68
+ *
69
+ * It remains correct for a caller who has ALREADY decided what to assert — there the assertion
70
+ * supplies the evidence the surface would otherwise have to go and find.
71
+ */
72
+ VERIFY: 'verify',
43
73
  };
44
74
  /**
45
75
  * The switch that turns on the full, output-schema-carrying surface. A boolean, not a menu.
@@ -48,6 +78,8 @@ export const TOOL_SURFACE = {
48
78
  * users to shop among alternatives that did not meaningfully differ.
49
79
  */
50
80
  export const ADVERTISE_ALL_ENV = 'RETICLE_ADVERTISE_ALL_TOOLS';
81
+ /** Opt into the smallest verdict-capable surface. Read by the DAEMON at startup, like the others. */
82
+ export const VERIFY_SURFACE_ENV = 'RETICLE_VERIFY_SURFACE';
51
83
  /**
52
84
  * The retired setting, still read so nobody's shell profile breaks.
53
85
  *
@@ -144,26 +176,52 @@ export const CORE_TOOL_NAMES = new Set([
144
176
  // deleting the instruction from the lease and the pause hint — deletes the handback protocol,
145
177
  // because there is nowhere else those two calls are ever named.
146
178
  ReticleTool.SESSION,
147
- // CAPABILITIES is the highest orientation-per-token tool on the surface: one call returns the app's
148
- // whole testable surface — its testids, signals, stores and named flows — and `{fromDisk:true}`
149
- // returns the same from a checked-in contract with NO browser attached, so an agent with zero
150
- // context starts oriented instead of exploring.
151
- //
152
- // Same argument as the four above, and it is the fifth time this file has made it: a tool an agent
153
- // must already know about, and reach through reticle_run, is a tool that never gets called.
154
- //
155
- // The cost is real and is NOT the bargain #262 claimed. That issue argued the marginal cost sits
156
- // "well below the 274-token average" because the input schema is small; measured, its description
157
- // plus schema is closer to 325 tokens, which is above the average rather than below it. This is
158
- // also a straight +1 rather than a swap: the tools that write-up proposed demoting (`project`,
159
- // `domain`) were never in the default set to begin with.
160
- //
161
- // So it is a deliberate bet, taken with that correction on the table: that orientation replaces
162
- // exploratory snapshots, which are among the most expensive calls an agent makes. The measurement
163
- // that settles it is whether sessions calling this early take fewer and cheaper snapshots than
164
- // those that do not. If that does not show up, this is the entry to revert.
179
+ ]);
180
+ /**
181
+ * The extended surface: what `all` advertises BEYOND the default set.
182
+ *
183
+ * `all` used to mean "every tool in the registry", and that is no longer allowed to be true. Cursor
184
+ * enforces a limit of 40 tools across every connected MCP server COMBINED, so a server advertising
185
+ * 48 by itself can push a user's other servers out or be dropped wholesale. The budget is a count,
186
+ * so no amount of trimming parameter prose buys anything back — only advertising fewer names does.
187
+ *
188
+ * Everything omitted stays in the registry, stays catalogued by `reticle_tools`, and stays callable
189
+ * by name through `reticle_run { tool, args }`. The cost is one discovery hop on the cold tail, which
190
+ * is the same trade the default surface has always made, applied one level further out.
191
+ *
192
+ * What it costs that is NOT free: `all` is the only surface that carries `outputSchema`, which is
193
+ * what makes the MCP layer validate tool OUTPUT — the check that once caught `reticle_verify_change`
194
+ * returning a payload its own schema rejected. The tools left off this list no longer get that
195
+ * validation from the wire. That defect class is now uncovered for them, and it is written down here
196
+ * rather than discovered later.
197
+ *
198
+ * Chosen as the capabilities an agent plausibly reaches for once the verify loop is not enough:
199
+ * orientation, the record/replay flow loop, visual evidence, and the three fault-injection controls.
200
+ */
201
+ export const EXTENDED_TOOL_NAMES = new Set([
202
+ // Demoted from the default set to make room under the cap. It was added as an explicit bet that
203
+ // orientation replaces exploratory snapshots, and the measurement that would have settled that
204
+ // bet was never run — so when the budget became a hard count, the unproven entry is the one that
205
+ // gives way. Still one `reticle_run` hop from any agent that wants it.
165
206
  ReticleTool.CAPABILITIES,
207
+ ReticleTool.FLOW_SAVE,
208
+ ReticleTool.FLOW_REPLAY,
209
+ ReticleTool.FLOW_VERIFY,
210
+ ReticleTool.RECORD,
211
+ ReticleTool.SCREENSHOT,
212
+ ReticleTool.VISUAL_DIFF,
213
+ ReticleTool.CLOCK,
214
+ ReticleTool.NETWORK_MOCK,
215
+ ReticleTool.STORAGE,
216
+ ReticleTool.VERIFY_CHANGE,
217
+ ReticleTool.CRAWL,
166
218
  ]);
219
+ /**
220
+ * The verify surface: one acting tool that returns a verdict, plus the two meta-tools that reach
221
+ * everything else. `act_and_wait` can resolve its own target, so no query tool is needed to name an
222
+ * element — that round trip was half the token cost of a verification.
223
+ */
224
+ export const VERIFY_TOOL_NAMES = new Set([ReticleTool.ACT_AND_WAIT]);
167
225
  /** Is the truthy form of a boolean env var set? `1`, `true`, `yes` — anything else is off. */
168
226
  function envFlagOn(raw) {
169
227
  if (raw === undefined)
@@ -178,6 +236,8 @@ function envFlagOn(raw) {
178
236
  * switch decides, and the retired setting is honoured last so an old shell profile still works.
179
237
  */
180
238
  export function resolveToolSurface(explicit) {
239
+ if (explicit === TOOL_SURFACE.VERIFY)
240
+ return TOOL_SURFACE.VERIFY;
181
241
  if (explicit === TOOL_SURFACE.ALL)
182
242
  return TOOL_SURFACE.ALL;
183
243
  if (explicit === TOOL_SURFACE.DEFAULT)
@@ -185,6 +245,8 @@ export function resolveToolSurface(explicit) {
185
245
  const retiredExplicit = explicit === undefined ? undefined : RETIRED_PROFILE_VALUES[explicit];
186
246
  if (retiredExplicit !== undefined)
187
247
  return retiredExplicit;
248
+ if (envFlagOn(process.env[VERIFY_SURFACE_ENV]))
249
+ return TOOL_SURFACE.VERIFY;
188
250
  if (envFlagOn(process.env[ADVERTISE_ALL_ENV]))
189
251
  return TOOL_SURFACE.ALL;
190
252
  const retiredEnv = RETIRED_PROFILE_VALUES[process.env[TOOL_PROFILE_ENV] ?? ''];
@@ -230,7 +292,11 @@ export function describeToolSurface(active, requested) {
230
292
  export function filterTools(tools, surface) {
231
293
  // CORE_TOOL_NAMES is what it always really was: the set advertised directly. It was never the
232
294
  // interesting thing about the `core` PROFILE, whose only distinction was a second name for this.
295
+ if (surface === TOOL_SURFACE.VERIFY)
296
+ return tools.filter((t) => VERIFY_TOOL_NAMES.has(t.name));
233
297
  if (surface === TOOL_SURFACE.DEFAULT)
234
298
  return tools.filter((t) => CORE_TOOL_NAMES.has(t.name));
235
- return tools;
299
+ // `all` is the extended surface, not the whole registry — the cap is a hard budget shared with
300
+ // every other MCP server the user has connected. surface-sizes.test.ts enforces it.
301
+ return tools.filter((t) => CORE_TOOL_NAMES.has(t.name) || EXTENDED_TOOL_NAMES.has(t.name));
236
302
  }