@reticlehq/server 2.8.0 → 2.10.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 (318) 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/capsule-store.d.ts +2 -2
  7. package/dist/capsule/causal-summary.d.ts +16 -1
  8. package/dist/capsule/causal-summary.js +2 -1
  9. package/dist/cli/cli-doctor.js +41 -13
  10. package/dist/cli/cli-flow-commands.js +8 -8
  11. package/dist/cli/cli-launch.d.ts +0 -17
  12. package/dist/cli/cli-launch.js +0 -28
  13. package/dist/cli/cli-parse.d.ts +1 -1
  14. package/dist/cli/cli-parse.js +1 -1
  15. package/dist/cli/cli-port.d.ts +24 -0
  16. package/dist/cli/cli-port.js +66 -35
  17. package/dist/cli/cli-verify.d.ts +21 -0
  18. package/dist/cli/cli-verify.js +45 -1
  19. package/dist/cli/config-discovery.d.ts +35 -0
  20. package/dist/cli/config-discovery.js +163 -0
  21. package/dist/cli/doctor-daemon-line.js +4 -1
  22. package/dist/cli/doctor-project-line.d.ts +37 -0
  23. package/dist/cli/doctor-project-line.js +35 -0
  24. package/dist/cli/doctor-rows.d.ts +51 -0
  25. package/dist/cli/doctor-rows.js +54 -0
  26. package/dist/cli/doctor-sessions-line.js +6 -3
  27. package/dist/cli/drive-attach.d.ts +99 -0
  28. package/dist/cli/drive-attach.js +203 -0
  29. package/dist/cli/drive-command.d.ts +13 -0
  30. package/dist/cli/drive-command.js +103 -0
  31. package/dist/cli/open-note.d.ts +20 -0
  32. package/dist/cli/open-note.js +37 -0
  33. package/dist/cli/status-next-action.d.ts +37 -0
  34. package/dist/cli/status-next-action.js +61 -0
  35. package/dist/cli.js +65 -70
  36. package/dist/crawl/crawl.d.ts +7 -2
  37. package/dist/crawl/crawl.js +52 -9
  38. package/dist/ee/audit-log.js +2 -2
  39. package/dist/events/body-failures.d.ts +2 -2
  40. package/dist/events/contradiction-folds.d.ts +45 -0
  41. package/dist/events/contradiction-folds.js +42 -0
  42. package/dist/events/contradictions.d.ts +51 -1
  43. package/dist/events/contradictions.js +26 -7
  44. package/dist/events/declared.d.ts +45 -0
  45. package/dist/events/declared.js +73 -0
  46. package/dist/events/echo-mismatch.d.ts +9 -3
  47. package/dist/events/echo-mismatch.js +33 -5
  48. package/dist/events/event-filters.d.ts +1 -1
  49. package/dist/events/event-filters.js +7 -4
  50. package/dist/events/predicate-eval.d.ts +17 -91
  51. package/dist/events/predicate-eval.js +85 -326
  52. package/dist/events/predicate-parse.js +31 -3
  53. package/dist/events/predicate-route.d.ts +32 -0
  54. package/dist/events/predicate-route.js +102 -0
  55. package/dist/events/predicate-schema.d.ts +99 -0
  56. package/dist/events/predicate-schema.js +309 -0
  57. package/dist/events/predicate.d.ts +8 -0
  58. package/dist/events/predicate.js +127 -5
  59. package/dist/events/stale-response.d.ts +2 -3
  60. package/dist/events/stale-response.js +49 -2
  61. package/dist/events/unit-mismatch.d.ts +2 -2
  62. package/dist/flows/annotate-tools.js +3 -0
  63. package/dist/flows/annotate.js +18 -4
  64. package/dist/flows/coverage.d.ts +38 -5
  65. package/dist/flows/coverage.js +30 -4
  66. package/dist/flows/decision.d.ts +15 -0
  67. package/dist/flows/decision.js +1 -1
  68. package/dist/flows/flow-replay-run.js +8 -1
  69. package/dist/flows/flow-tools.js +9 -1
  70. package/dist/flows/replay.js +3 -0
  71. package/dist/flows/verify-change-tools.js +1 -1
  72. package/dist/honesty/accepted-write.d.ts +3 -0
  73. package/dist/honesty/accepted-write.js +8 -3
  74. package/dist/honesty/already-true.js +15 -0
  75. package/dist/honesty/blind-spots.js +4 -0
  76. package/dist/honesty/honesty.d.ts +11 -0
  77. package/dist/honesty/honesty.js +1 -0
  78. package/dist/honesty/uncaptured-bodies.d.ts +4 -2
  79. package/dist/honesty/uncaptured-bodies.js +9 -2
  80. package/dist/honesty/unread-outcome.d.ts +8 -0
  81. package/dist/honesty/unread-outcome.js +36 -9
  82. package/dist/honesty/unsettled.d.ts +9 -0
  83. package/dist/honesty/unsettled.js +7 -3
  84. package/dist/honesty/verified.d.ts +29 -3
  85. package/dist/honesty/verified.js +67 -8
  86. package/dist/http-server.d.ts +19 -0
  87. package/dist/http-server.js +202 -4
  88. package/dist/impact/impact-recorder.d.ts +23 -0
  89. package/dist/impact/impact-recorder.js +76 -0
  90. package/dist/impact/impact-store.d.ts +47 -0
  91. package/dist/impact/impact-store.js +171 -0
  92. package/dist/index.d.ts +25 -0
  93. package/dist/index.js +60 -10
  94. package/dist/init/agent-rules.d.ts +20 -0
  95. package/dist/init/agent-rules.js +36 -0
  96. package/dist/init/astro-patch.js +27 -1
  97. package/dist/init/confirm.d.ts +21 -3
  98. package/dist/init/confirm.js +32 -5
  99. package/dist/init/connect-steps.d.ts +2 -0
  100. package/dist/init/connect-steps.js +50 -0
  101. package/dist/init/csp-check.d.ts +38 -0
  102. package/dist/init/csp-check.js +92 -0
  103. package/dist/init/csp-doctor.d.ts +25 -0
  104. package/dist/init/csp-doctor.js +49 -0
  105. package/dist/init/cursor.js +6 -9
  106. package/dist/init/detect.d.ts +8 -0
  107. package/dist/init/detect.js +33 -3
  108. package/dist/init/mcp-clients.d.ts +1 -0
  109. package/dist/init/mcp-clients.js +51 -7
  110. package/dist/init/mcp.d.ts +16 -2
  111. package/dist/init/mcp.js +23 -1
  112. package/dist/init/plan-framework.d.ts +10 -0
  113. package/dist/init/plan-framework.js +25 -1
  114. package/dist/init/plan.d.ts +7 -2
  115. package/dist/init/plan.js +66 -30
  116. package/dist/init/run.js +11 -5
  117. package/dist/init/slash-command.d.ts +1 -1
  118. package/dist/init/slash-command.js +12 -4
  119. package/dist/init/snippets.d.ts +1 -1
  120. package/dist/init/snippets.js +35 -7
  121. package/dist/init/vite-config.js +24 -2
  122. package/dist/input/network-mock-tools.js +8 -1
  123. package/dist/input/scroll-find.d.ts +6 -0
  124. package/dist/input/scroll-find.js +17 -2
  125. package/dist/input/scroll-tools.js +4 -0
  126. package/dist/journal/attach-journal.d.ts +9 -1
  127. package/dist/journal/attach-journal.js +13 -2
  128. package/dist/journal/journal-query.d.ts +13 -0
  129. package/dist/journal/journal-query.js +28 -3
  130. package/dist/journal/workspace-gitignore.d.ts +10 -0
  131. package/dist/journal/workspace-gitignore.js +59 -0
  132. package/dist/license/license.d.ts +69 -4
  133. package/dist/license/license.js +84 -15
  134. package/dist/mcp/mcp-proxy.d.ts +7 -93
  135. package/dist/mcp/mcp-proxy.js +183 -250
  136. package/dist/mcp/mcp.d.ts +27 -2
  137. package/dist/mcp/mcp.js +77 -36
  138. package/dist/mcp/proxy-daemon-probe.d.ts +8 -0
  139. package/dist/mcp/proxy-daemon-probe.js +48 -0
  140. package/dist/mcp/proxy-handshake.d.ts +82 -1
  141. package/dist/mcp/proxy-handshake.js +110 -3
  142. package/dist/mcp/proxy-lifecycle.d.ts +10 -1
  143. package/dist/mcp/proxy-lifecycle.js +11 -2
  144. package/dist/mcp/proxy-log.d.ts +63 -0
  145. package/dist/mcp/proxy-log.js +132 -0
  146. package/dist/mcp/server-instructions.d.ts +17 -0
  147. package/dist/mcp/server-instructions.js +56 -0
  148. package/dist/mcp/shared-params.d.ts +27 -0
  149. package/dist/mcp/shared-params.js +36 -0
  150. package/dist/mcp/sse-frame-parser.d.ts +19 -0
  151. package/dist/mcp/sse-frame-parser.js +41 -0
  152. package/dist/project/reticle-dir.d.ts +2 -0
  153. package/dist/project/reticle-dir.js +1 -0
  154. package/dist/session/connection-memory.d.ts +59 -0
  155. package/dist/session/connection-memory.js +108 -0
  156. package/dist/session/dev-command.d.ts +26 -0
  157. package/dist/session/dev-command.js +89 -0
  158. package/dist/session/dev-server-probe.d.ts +68 -0
  159. package/dist/session/dev-server-probe.js +102 -7
  160. package/dist/session/last-act.d.ts +26 -0
  161. package/dist/session/last-act.js +35 -1
  162. package/dist/session/live-control-tools.js +42 -5
  163. package/dist/session/no-session-diagnosis.d.ts +55 -2
  164. package/dist/session/no-session-diagnosis.js +199 -22
  165. package/dist/session/no-session-next-action.d.ts +46 -0
  166. package/dist/session/no-session-next-action.js +85 -0
  167. package/dist/session/no-session-watch.d.ts +40 -1
  168. package/dist/session/no-session-watch.js +172 -11
  169. package/dist/session/pending-commands.d.ts +12 -0
  170. package/dist/session/pending-commands.js +13 -1
  171. package/dist/session/session-manager.d.ts +4 -0
  172. package/dist/session/session-manager.js +35 -5
  173. package/dist/session/session-readiness.js +1 -1
  174. package/dist/session/session.d.ts +10 -1
  175. package/dist/session/session.js +146 -5
  176. package/dist/telemetry/app-instrumented.d.ts +8 -0
  177. package/dist/telemetry/app-instrumented.js +10 -0
  178. package/dist/telemetry/bug-found.d.ts +18 -0
  179. package/dist/telemetry/bug-found.js +14 -0
  180. package/dist/telemetry/daemon-telemetry.js +29 -0
  181. package/dist/telemetry/install-source.d.ts +20 -0
  182. package/dist/telemetry/install-source.js +34 -0
  183. package/dist/telemetry/instrumentation-stall.d.ts +57 -0
  184. package/dist/telemetry/instrumentation-stall.js +103 -0
  185. package/dist/telemetry/license-activation.d.ts +48 -0
  186. package/dist/telemetry/license-activation.js +57 -0
  187. package/dist/telemetry/session-metrics.d.ts +8 -0
  188. package/dist/telemetry/session-metrics.js +10 -0
  189. package/dist/telemetry/telemetry.d.ts +3 -1
  190. package/dist/telemetry/telemetry.js +23 -3
  191. package/dist/tools/act-tools.js +166 -29
  192. package/dist/tools/assert-verdict.js +32 -7
  193. package/dist/tools/browser-tools.js +10 -0
  194. package/dist/tools/dynamic-tools.js +17 -8
  195. package/dist/tools/error-recovery.js +10 -6
  196. package/dist/tools/feedback-tools.js +31 -6
  197. package/dist/tools/invoke-tool.d.ts +1 -1
  198. package/dist/tools/invoke-tool.js +30 -8
  199. package/dist/tools/lease-hint.d.ts +30 -8
  200. package/dist/tools/lease-hint.js +65 -14
  201. package/dist/tools/lease-tools.d.ts +11 -0
  202. package/dist/tools/lease-tools.js +141 -96
  203. package/dist/tools/observe-tools.js +9 -3
  204. package/dist/tools/react-grace.d.ts +4 -0
  205. package/dist/tools/react-grace.js +6 -3
  206. package/dist/tools/read-tools.js +47 -1
  207. package/dist/tools/resolve-target.d.ts +33 -0
  208. package/dist/tools/resolve-target.js +53 -0
  209. package/dist/tools/settle-in-flight.d.ts +15 -0
  210. package/dist/tools/settle-in-flight.js +42 -2
  211. package/dist/tools/snapshot-delta.d.ts +3 -0
  212. package/dist/tools/snapshot-delta.js +67 -6
  213. package/dist/tools/tool-kit.d.ts +38 -3
  214. package/dist/tools/tool-names.d.ts +2 -0
  215. package/dist/tools/tool-names.js +2 -0
  216. package/dist/tools/tool-surface.d.ts +60 -0
  217. package/dist/tools/tool-surface.js +84 -19
  218. package/dist/tools/tools.js +61 -5
  219. package/dist/tools/unadvertised-help.js +9 -6
  220. package/docs/README.md +1 -2
  221. package/docs/actions.mdx +41 -8
  222. package/docs/agent-cheatsheet.md +6 -4
  223. package/docs/architecture.md +6 -0
  224. package/docs/benchmarks.md +60 -12
  225. package/docs/best-practices.mdx +11 -6
  226. package/docs/capabilities.mdx +204 -57
  227. package/docs/cli/affected.mdx +6 -4
  228. package/docs/cli/capsules.mdx +9 -8
  229. package/docs/cli/cloud.mdx +30 -4
  230. package/docs/cli/doctor.mdx +41 -8
  231. package/docs/cli/drive.mdx +34 -15
  232. package/docs/cli/gate.mdx +6 -6
  233. package/docs/cli/help.mdx +8 -6
  234. package/docs/cli/hunt.mdx +20 -4
  235. package/docs/cli/identify.mdx +16 -2
  236. package/docs/cli/init.mdx +35 -8
  237. package/docs/cli/kill.mdx +5 -5
  238. package/docs/cli/license.mdx +1 -1
  239. package/docs/cli/open.mdx +6 -0
  240. package/docs/cli/restart.mdx +6 -6
  241. package/docs/cli/serve.mdx +10 -3
  242. package/docs/cli/status.mdx +29 -5
  243. package/docs/cli/stop.mdx +8 -0
  244. package/docs/cli/verify.mdx +20 -1
  245. package/docs/cli/version.mdx +1 -1
  246. package/docs/cli/watch.mdx +9 -2
  247. package/docs/cli.mdx +44 -22
  248. package/docs/debugging.md +3 -1
  249. package/docs/deploy-checks.md +31 -2
  250. package/docs/desktop.mdx +276 -80
  251. package/docs/docs.json +13 -12
  252. package/docs/enterprise.md +9 -3
  253. package/docs/faq.mdx +49 -35
  254. package/docs/fixtures.md +2 -0
  255. package/docs/flows.md +7 -5
  256. package/docs/for-agents.md +3 -3
  257. package/docs/frameworks.mdx +46 -7
  258. package/docs/gate-plan.md +4 -2
  259. package/docs/gates.md +8 -6
  260. package/docs/getting-started.md +53 -12
  261. package/docs/human-control.md +2 -0
  262. package/docs/index.mdx +43 -1
  263. package/docs/install-agentic.mdx +13 -4
  264. package/docs/install-manual.mdx +6 -4
  265. package/docs/instrumentation.mdx +7 -1
  266. package/docs/integration-patterns.md +3 -1
  267. package/docs/local-registry.md +4 -2
  268. package/docs/multi-agent-testing.md +3 -1
  269. package/docs/packages/babel-plugin.mdx +15 -6
  270. package/docs/packages/browser.mdx +2 -2
  271. package/docs/packages/core.mdx +26 -16
  272. package/docs/packages/electron.mdx +5 -7
  273. package/docs/packages/eslint-plugin.mdx +5 -3
  274. package/docs/packages/next.mdx +3 -5
  275. package/docs/packages/react.mdx +3 -3
  276. package/docs/packages/server.mdx +2 -2
  277. package/docs/packages/tauri.mdx +6 -6
  278. package/docs/packages/test.mdx +18 -7
  279. package/docs/packages/vite-plugin.mdx +4 -6
  280. package/docs/packages.mdx +14 -13
  281. package/docs/platform-integration.md +2 -0
  282. package/docs/predicates.mdx +76 -7
  283. package/docs/quickstart.mdx +51 -5
  284. package/docs/recipes.mdx +63 -23
  285. package/docs/skill-file.mdx +3 -1
  286. package/docs/state-management.mdx +2 -0
  287. package/docs/system-map.md +3 -1
  288. package/docs/telemetry-contract.md +33 -5
  289. package/docs/telemetry-events.mdx +63 -18
  290. package/docs/telemetry.md +8 -5
  291. package/docs/testing.md +35 -1
  292. package/docs/token-efficiency.md +8 -2
  293. package/docs/tools-act-and-wait.mdx +64 -14
  294. package/docs/tools-act-sequence.mdx +34 -17
  295. package/docs/tools-act.mdx +24 -11
  296. package/docs/tools-assert.mdx +69 -10
  297. package/docs/tools-console.mdx +11 -7
  298. package/docs/tools-inspect.mdx +11 -8
  299. package/docs/tools-navigate.mdx +41 -5
  300. package/docs/tools-network.mdx +42 -9
  301. package/docs/tools-observe.mdx +66 -10
  302. package/docs/tools-overview.mdx +26 -23
  303. package/docs/tools-query.mdx +52 -8
  304. package/docs/tools-session-and-feedback.mdx +35 -9
  305. package/docs/tools-sessions.mdx +39 -10
  306. package/docs/tools-snapshot.mdx +57 -3
  307. package/docs/tools-state.mdx +24 -1
  308. package/docs/tools-tools-and-run.mdx +55 -24
  309. package/docs/tools-wait-for.mdx +34 -15
  310. package/docs/troubleshooting.mdx +80 -10
  311. package/docs/usage.md +46 -24
  312. package/docs/vs-chrome-devtools-mcp.mdx +41 -0
  313. package/docs/vs-playwright-mcp.mdx +42 -2
  314. package/docs/vs-screenshots.mdx +42 -1
  315. package/docs/what-is-reticle.mdx +1 -1
  316. package/docs/why-reticle.mdx +3 -1
  317. package/package.json +5 -5
  318. package/docs/desktop-apps.md +0 -265
@@ -262,7 +262,7 @@ export const OBSERVE_TOOLS = [
262
262
  // undocumented here, including `route` — and "did submitting the login form navigate away"
263
263
  // is the most common thing an agent wants to assert. A field report reached us from an agent
264
264
  // that guessed `urlContains` on route (net's spelling) and got unrecognized_keys.
265
- 'Predicate to evaluate. Kinds: { signal, name } { net, urlContains|method|status|count } ' +
265
+ 'Predicate to evaluate. Kinds: { signal, name } { net, urlContains|method|status|count|bodyContains } ' +
266
266
  '{ state, path|equals } { route, pathname (exact) | contains (path+query+hash) } ' +
267
267
  '{ element, testid|role|text } { text } { console, level|absent } { animation, name } ' +
268
268
  '{ settled } — combine with { allOf | anyOf | not }. Prefer a signal/net/state consequence ' +
@@ -386,6 +386,10 @@ export const OBSERVE_TOOLS = [
386
386
  .nonnegative()
387
387
  .optional()
388
388
  .describe('Keep only the most recent N matching calls (older are dropped and counted in droppedOldest) — cuts tokens on a wide window. Defaults to 200 when omitted; pass a higher number for more, or scope with since/until.'),
389
+ bodies: z
390
+ .boolean()
391
+ .optional()
392
+ .describe('Include request/response bodies (default true). Pass false for a body-free listing — method, url, status, timing only — for the common "did POST /x return 200?" read. Bodies dominate the payload, so this cuts the cheap case by a large factor.'),
389
393
  ...sessionIdShape,
390
394
  },
391
395
  outputSchema: {
@@ -411,6 +415,8 @@ export const OBSERVE_TOOLS = [
411
415
  const status = asNumber(args['status']);
412
416
  const ok = 'boolean' === typeof args['ok'] ? args['ok'] : undefined;
413
417
  const limit = asNumber(args['limit']);
418
+ // Default true keeps the current shape; `bodies: false` returns the body-free listing (#401).
419
+ const bodies = 'boolean' === typeof args['bodies'] ? args['bodies'] : true;
414
420
  const buffer = bufferEnvelope(session);
415
421
  // Completed calls + unresolved in-flight requests (a hung request shows as pending).
416
422
  const allNet = reconcileNet(await session.queryEvents({
@@ -425,14 +431,14 @@ export const OBSERVE_TOOLS = [
425
431
  }
426
432
  // Default the cap so an omitted `limit` can't dump a whole flooded session (since defaults to 0).
427
433
  const { events: budgeted, droppedOldest } = applyEventBudget(matched, limit ?? DEFAULT_QUERY_LIMIT);
428
- const calls = budgeted.map(projectNetCall);
434
+ const calls = budgeted.map((e) => projectNetCall(e, bodies));
429
435
  // A zero-match FILTER already reports what did fire (netEmptyHint above). Zero calls at all
430
436
  // fell through as a bare `[]`, which is indistinguishable from an observer that is not
431
437
  // recording — and those need opposite responses. Say the look happened.
432
438
  return withSizeCost(noteEmptyRead({
433
439
  calls,
434
440
  ...(droppedOldest > 0 ? { total: matched.length, droppedOldest } : {}),
435
- ...bodiesNotCaptured(calls),
441
+ ...(bodies ? bodiesNotCaptured(calls) : {}),
436
442
  ...buffer,
437
443
  }, 'calls', { noun: 'network calls' }));
438
444
  },
@@ -30,6 +30,10 @@ import type { SettleSource } from './settle-in-flight.js';
30
30
  * True only for the accusable shape: a SUCCESSFUL MUTATING request settled, and nothing has moved. A
31
31
  * read that changed nothing is a prefetch, a failed write is a different finding entirely, and an
32
32
  * action that already moved the UI has nothing to wait for — none of them buy a grace period.
33
+ *
34
+ * "Mutating" is read from the same constant the accuser uses rather than restated here. Restating it
35
+ * is how `IPC` came to be absent from this list and present in that one, so on a desktop app every
36
+ * successful write skipped the grace and a correct app was told it had ignored its own response.
33
37
  */
34
38
  export declare function awaitsReaction(events: readonly {
35
39
  type: string;
@@ -23,7 +23,7 @@
23
23
  * Deliberately short. This covers a task-boundary re-render, not a slow app: if nothing has moved
24
24
  * within the grace, "ignored" has become an honest description and the finding should stand.
25
25
  */
26
- import { EventType } from '@reticlehq/core';
26
+ import { EventType, MUTATING_METHODS } from '@reticlehq/core';
27
27
  /**
28
28
  * How long to let the app react. A React commit after a resolved fetch is one or two task hops; this
29
29
  * is generous against that and still far too short to paper over an app that genuinely dropped the
@@ -32,7 +32,6 @@ import { EventType } from '@reticlehq/core';
32
32
  const REACTION_GRACE_MS = 300;
33
33
  /** Poll cadence — the same as the settle wait, for the same reason. */
34
34
  const POLL_MS = 50;
35
- const MUTATING = ['POST', 'PUT', 'PATCH', 'DELETE'];
36
35
  /** Movement that would clear `response-ignored`; mirrors `uiAdvanced` in the detector. */
37
36
  const MOVEMENT = new Set([
38
37
  EventType.DOM_ADDED,
@@ -51,6 +50,10 @@ function moved(events) {
51
50
  * True only for the accusable shape: a SUCCESSFUL MUTATING request settled, and nothing has moved. A
52
51
  * read that changed nothing is a prefetch, a failed write is a different finding entirely, and an
53
52
  * action that already moved the UI has nothing to wait for — none of them buy a grace period.
53
+ *
54
+ * "Mutating" is read from the same constant the accuser uses rather than restated here. Restating it
55
+ * is how `IPC` came to be absent from this list and present in that one, so on a desktop app every
56
+ * successful write skipped the grace and a correct app was told it had ignored its own response.
54
57
  */
55
58
  export function awaitsReaction(events) {
56
59
  if (moved(events))
@@ -60,7 +63,7 @@ export function awaitsReaction(events) {
60
63
  return false;
61
64
  const method = 'string' === typeof e.data['method'] ? e.data['method'].toUpperCase() : '';
62
65
  const ok = e.data['ok'];
63
- return MUTATING.includes(method) && true === ok;
66
+ return MUTATING_METHODS.includes(method) && true === ok;
64
67
  });
65
68
  }
66
69
  /**
@@ -39,6 +39,15 @@ function unanchoredWarning(count) {
39
39
  const HUD_LEVELS = ['info', 'warn', 'error'];
40
40
  const HUD_LEVEL_LIST = HUD_LEVELS.join(' | ');
41
41
  const hudLevelEnum = z.enum(HUD_LEVELS);
42
+ /**
43
+ * How deep an unscoped `reticle_state` read goes before collapsing to size markers.
44
+ *
45
+ * 3 keeps every store name and every top-level key visible — enough to see what exists and to name a
46
+ * `path` — while collapsing the collections that make an unscoped read enormous. Measured on the
47
+ * bench fixture: unbounded 10,119 B, depth 4 1,323 B, depth 3 596 B, depth 2 322 B. Two loses the
48
+ * per-store keys, which is the orientation the next call needs.
49
+ */
50
+ const DEFAULT_STATE_DEPTH = 1;
42
51
  export const READ_TOOLS = [
43
52
  {
44
53
  name: ReticleTool.BASELINE_SAVE,
@@ -395,7 +404,44 @@ export const READ_TOOLS = [
395
404
  return { ...root, storeNames: names };
396
405
  }
397
406
  if (path === undefined && depth === undefined) {
398
- return { ...root, storeNames: names }; // unchanged shape, safe storeNames
407
+ // An unscoped read used to hand back the WHOLE store tree: 10,119 bytes on this repo's own
408
+ // fixture. A tool result is not paid once — it stays in the conversation and is re-sent on
409
+ // every later turn, so one such read cost roughly 34,000 tokens across a 16-turn run.
410
+ //
411
+ // The VALUES are bounded; the ENVELOPE is not touched. `{ stores, storeNames, component }`
412
+ // is the documented shape and callers depend on it — a first attempt applied the bound by
413
+ // routing through the path selector, which silently changed the reply to `{ found, value }`
414
+ // and took the component projection and the bridge round-trip with it. Capping in place
415
+ // keeps every store name and the component read exactly where they were; only collections
416
+ // deeper than the bound collapse to a marker like "[Array(40)]", which still says what is
417
+ // there and how much of it.
418
+ //
419
+ // A default, not a cap: any explicit `depth` is honoured at any value, and `path` reads a
420
+ // sub-tree at full fidelity. Nothing became unreachable, only cheaper by default.
421
+ const stores = root.stores;
422
+ const bounded = 'object' === typeof stores && null !== stores
423
+ ? Object.fromEntries(Object.entries(stores).map(([k, v]) => [k, capDepth(v, DEFAULT_STATE_DEPTH)]))
424
+ : undefined;
425
+ // Say that it was bounded, and only when it actually was. A read that quietly hands back
426
+ // less than it was asked for is the shape of a false green: the agent sees
427
+ // `deployments: "[Array(40)]"`, concludes it has read the store, and asserts over a summary.
428
+ // The marker alone is suggestive; this makes it explicit and names the way to the real value.
429
+ const trimmed = bounded !== undefined && JSON.stringify(bounded) !== JSON.stringify(stores);
430
+ return {
431
+ ...root,
432
+ ...(bounded === undefined ? {} : { stores: bounded }),
433
+ storeNames: names,
434
+ ...(trimmed
435
+ ? {
436
+ truncation: {
437
+ note: `bounded to depth ${String(DEFAULT_STATE_DEPTH)} per store — this is NOT the ` +
438
+ 'whole store. Values shown as "[Array(n)]" or "{…n keys}" were collapsed; read ' +
439
+ 'one with { store, path } (e.g. { store:"app", path:"deployments.0" }) or pass ' +
440
+ 'an explicit `depth` for more.',
441
+ },
442
+ }
443
+ : {}),
444
+ };
399
445
  }
400
446
  // Back-compat: an older browser returned the whole store; scope it here (may already be
401
447
  // size-truncated in transit for a very large store — that is the limitation this fix removes
@@ -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
- export interface ToolDeps {
18
+ import type { ChromiumProbe } from '../cli/chromium-hint.js';
19
+ export interface ToolDeps<Ext = unknown> {
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/). */
@@ -51,8 +59,23 @@ export interface ToolDeps {
51
59
  * ToolDeps keeps working; absent falls back to the default port.
52
60
  */
53
61
  bridgePort?: number;
62
+ /**
63
+ * A consumer's OWN dependencies, carried through untouched.
64
+ *
65
+ * Nothing in this package reads it. It exists so a consumer embedding the engine can add tools of
66
+ * its own — answering out of a store this package has never heard of — without either widening the
67
+ * fields above with a stranger's vocabulary or forking to get a handle through.
68
+ *
69
+ * The type parameter, rather than `unknown`, is what keeps the seam typed: a consumer writes
70
+ * `ToolDef<TheirExt>` once and their handlers see their own type. Casting out of `unknown` in every
71
+ * handler is how a typed seam quietly becomes an untyped one.
72
+ *
73
+ * Optional, and defaulted to `unknown`, so every existing construction of this bag — and every test
74
+ * that builds one — is unchanged.
75
+ */
76
+ ext?: Ext;
54
77
  }
55
- export interface ToolDef {
78
+ export interface ToolDef<Ext = unknown> {
56
79
  name: string;
57
80
  description: string;
58
81
  inputSchema: z.ZodRawShape;
@@ -78,7 +101,19 @@ export interface ToolDef {
78
101
  * example that does not validate is worse than none at all.
79
102
  */
80
103
  example?: Record<string, unknown>;
81
- handler: (deps: ToolDeps, args: Record<string, unknown>) => Promise<unknown>;
104
+ /**
105
+ * Declared method-style, not as a property, and that is load-bearing rather than stylistic.
106
+ *
107
+ * Under `strictFunctionTypes` a property holding a function has CONTRAVARIANT parameters, so
108
+ * `ToolDef<TheirExt>` and `ToolDef<unknown>` become mutually unassignable and a consumer composing
109
+ * `[...TOOLS, ...theirTools]` needs a cast — as does every internal helper that then touches the
110
+ * array. Method-style parameters are checked bivariantly, which collapses all of that.
111
+ *
112
+ * The unsoundness bivariance admits is that a handler written against `ToolDeps<TheirExt>` could be
113
+ * handed a bag carrying no `ext` at all. That costs nothing here, because `ext` is optional by
114
+ * design: any handler reading it already has to survive `undefined`.
115
+ */
116
+ handler(deps: ToolDeps<Ext>, args: Record<string, unknown>): Promise<unknown>;
82
117
  }
83
118
  export declare const sessionIdShape: {
84
119
  sessionId: z.ZodOptional<z.ZodString>;
@@ -46,6 +46,8 @@ export declare const ReticleTool: {
46
46
  readonly AFFECTED: "reticle_affected";
47
47
  readonly COVERAGE: "reticle_coverage";
48
48
  readonly VERIFY_CHANGE: "reticle_verify_change";
49
+ /** Merged: change/flows/affected/coverage/crawl — "what is proved, and what is not". */
50
+ readonly VERIFY: "reticle_verify";
49
51
  /** read cross-run history (.reticle/project.json) + diff-vs-last for a name. */
50
52
  readonly PROJECT: "reticle_project";
51
53
  /** explicitly record a run outcome (the manual companion to auto-recording). */
@@ -47,6 +47,8 @@ export const ReticleTool = {
47
47
  AFFECTED: 'reticle_affected',
48
48
  COVERAGE: 'reticle_coverage',
49
49
  VERIFY_CHANGE: 'reticle_verify_change',
50
+ /** Merged: change/flows/affected/coverage/crawl — "what is proved, and what is not". */
51
+ VERIFY: 'reticle_verify',
50
52
  /** read cross-run history (.reticle/project.json) + diff-vs-last for a name. */
51
53
  PROJECT: 'reticle_project',
52
54
  /** explicitly record a run outcome (the manual companion to auto-recording). */
@@ -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
  *