@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
@@ -1,3 +1,5 @@
1
+ import { normalizeQueryArgs } from './query-shape.js';
2
+ import { resolveTargetRef } from './resolve-target.js';
1
3
  /**
2
4
  * Action tools — reticle_act, reticle_act_sequence, reticle_act_and_wait. Split out of tools.ts to keep
3
5
  * that file under the line cap and assembled back into the tool list there via ...ACT_TOOLS; the
@@ -14,18 +16,19 @@ import { buildReactionReport, summarizeReaction } from '../events/reaction.js';
14
16
  import { parsePredicate } from '../events/predicate-parse.js';
15
17
  import { causalSummary } from '../capsule/causal-summary.js';
16
18
  import { findContradictions } from '../events/contradictions.js';
17
- import { inFlightRequestLabels, waitForInFlight } from './settle-in-flight.js';
19
+ import { inFlightRequestLabels, repeatedRequestLabels, waitForInFlight, } from './settle-in-flight.js';
18
20
  import { waitForReaction } from './react-grace.js';
19
21
  import { decideVerified } from '../honesty/verified.js';
22
+ import { declaredExpectations } from '../events/declared.js';
20
23
  import { readsDomState } from '../honesty/already-true.js';
21
24
  import { describeWaitTarget } from '../honesty/unsettled.js';
22
25
  import { saveFailedAssertCapsule } from './act-capsule.js';
23
26
  import { buildDivergenceCapsule } from '../capsule/capsule.js';
24
27
  import { predicateToExpectedLinks } from '../capsule/predicate-to-links.js';
25
28
  import { buildHonestyBlock } from '../honesty/honesty.js';
26
- import { buildCoverageStatement, blindSpotsFromState, transportGapNote, Coverage, impeachesCapture, } from '../honesty/blind-spots.js';
29
+ import { BlindSpotKind, buildCoverageStatement, blindSpotsFromState, transportGapNote, Coverage, impeachesCapture, } from '../honesty/blind-spots.js';
27
30
  import { hasAcceptedWrite } from '../honesty/accepted-write.js';
28
- import { hasUnreadWriteOutcome } from '../honesty/unread-outcome.js';
31
+ import { unreadWriteLabels } from '../honesty/unread-outcome.js';
29
32
  import { evaluatePredicate, waitForPredicate, provenExpectedLinks, PredicateSchema, } from '../events/predicate.js';
30
33
  import { healthEnvelope, refuseIfThrottled } from '../session/session-health.js';
31
34
  import { pausedShortCircuit, pausedOutputShape, withControl, PAUSED_NO_VERDICT, } from '../session/control-envelope.js';
@@ -54,20 +57,59 @@ import { tryRealInput } from './real-input-attempt.js';
54
57
  const ACTION_TYPE_VALUES = Object.values(ActionType);
55
58
  const ACTION_TYPE_LIST = ACTION_TYPE_VALUES.join(' | ');
56
59
  const actionTypeEnum = z.enum(ACTION_TYPE_VALUES);
60
+ /**
61
+ * Resolve an action's element: an explicit `ref`, or a `target` query resolved in the SAME call.
62
+ *
63
+ * Requiring a ref meant every verification paid a `reticle_query` turn first just to learn one
64
+ * string, and the advertised tool surface is re-sent on every turn — measured on the wire, a
65
+ * two-turn verification spent 10,756 of 11,235 tokens on schema and 479 on the actual answers. The
66
+ * lookup still happens; it just stops costing a round trip through the model.
67
+ *
68
+ * `ref` wins when both are given, because it is the more specific instruction and silently
69
+ * preferring the query would act on something the caller did not name.
70
+ */
71
+ async function resolveActTarget(session, args) {
72
+ const ref = asString(args['ref']);
73
+ if (ref !== undefined && ref.length > 0)
74
+ return { kind: 'ref', ref };
75
+ const target = args['target'];
76
+ if (target === undefined) {
77
+ return {
78
+ kind: 'error',
79
+ message: 'pass `ref` (from reticle_query/reticle_snapshot) or `target` (e.g. { testid } or { role, name }).',
80
+ };
81
+ }
82
+ const q = normalizeQueryArgs(asRecord(target));
83
+ const out = await session.command(ReticleCommand.QUERY, {
84
+ by: q['by'],
85
+ value: q['value'],
86
+ name: q['name'],
87
+ scope: q['scope'],
88
+ });
89
+ if (!out.ok)
90
+ return { kind: 'error', message: out.error ?? 'target query failed' };
91
+ const elements = asRecord(out.result)['elements'];
92
+ return resolveTargetRef(Array.isArray(elements) ? elements : []);
93
+ }
57
94
  export const ACT_TOOLS = [
58
95
  {
59
96
  name: ReticleTool.ACT,
60
97
  example: { ref: 'e42', action: 'fill', args: { value: 'hello' } },
61
- description: 'Act WITHOUT checking the result — if the action is supposed to cause something, use reticle_act_and_wait { until } instead and get the verdict in the same call. This tool returns immediately with a `since` cursor; observe the reaction yourself with reticle_observe. Use it for actions with no observable consequence to assert (focus, hover, scrollIntoView, a fill you are about to submit). One action against a ref: click|dblclick|hover|focus|fill|type|clear|select|check|uncheck|submit|press|scrollIntoView. Carries effect:{dispatched,targetMatched,visible,enabled,focusMoved,valueChanged,domMutatedWithin,occluded,occludedBy,scrolledIntoView} to tell "action missed" from "app didn\'t react"; dispatched=landed, settled=a real frame flushed, and a settle timeout never fails the tool. Fields at their uninformative default are OMITTED so a clean action collapses to its consequence: an absent dispatched/targetMatched/visible/enabled means true, an absent occluded/scrolledIntoView/valueChanged/defaultPrevented means false, an absent focusMoved/occludedBy means null. occluded=true means the click point is covered by another element (a real user could not click it) — synthetic dispatch still delivered the event; scrolledIntoView=true means an off-viewport target was scrolled in first. inputMode is "real" (native CDP, no synthetic effect block) or "synthetic"; clicks default to the occlusion-honest synthetic path even when CDP is configured — pass args.native:true to force a trusted native click (file pickers, clipboard). args.holdMs keeps the pointer DOWN for that long between mousedown and mouseup — the only way to drive a hold-to-confirm control (hold-to-delete, hold-to-record, long-press); effect.heldMs reports the hold actually achieved, which a throttled background tab can stretch. args.confirmDangerous is a permission gate and NOT a duration: it allows a destructive control, it does not hold one. inputModeReason explains any real→synthetic choice so it is never silent. Full model (real-input, throttled tabs, `reticle drive`): node_modules/@reticlehq/server/docs/usage.md §18.',
98
+ description: 'Act WITHOUT checking the result — if the action is supposed to cause something, use reticle_act_and_wait { until } instead and get the verdict in the same call. This tool returns immediately with a `since` cursor; observe the reaction yourself with reticle_observe. Use it for actions with no observable consequence to assert (focus, hover, scrollIntoView, a fill you are about to submit). One action against a ref: click|dblclick|hover|focus|fill|type|clear|select|check|uncheck|submit|press|scrollIntoView. Carries effect:{dispatched,targetMatched,visible,enabled,focusMoved,valueChanged,domMutatedWithin,occluded,occludedBy,scrolledIntoView} to tell "action missed" from "app didn\'t react"; dispatched=landed, settled=a real frame flushed, and a settle timeout never fails the tool. Fields at their uninformative default are OMITTED so a clean action collapses to its consequence: an absent dispatched/targetMatched/visible/enabled means true, an absent occluded/scrolledIntoView/valueChanged/defaultPrevented means false, an absent focusMoved/occludedBy means null. occluded=true means the click point is covered by another element (a real user could not click it) — synthetic dispatch still delivered the event; scrolledIntoView=true means an off-viewport target was scrolled in first. effect.alreadyAtValue=true (check/uncheck only) means the box already read as the requested state, so NOTHING was dispatched and the app was never told — the DOM property is not evidence the application holds that value, so assert the app\'s own state (a signal, a request, a derived control), not the box. inputMode is "real" (native CDP, no synthetic effect block) or "synthetic"; clicks default to the occlusion-honest synthetic path even when CDP is configured — pass args.native:true to force a trusted native click (file pickers, clipboard). args.holdMs keeps the pointer DOWN for that long between mousedown and mouseup — the only way to drive a hold-to-confirm control (hold-to-delete, hold-to-record, long-press); effect.heldMs reports the hold actually achieved, which a throttled background tab can stretch. args.confirmDangerous is a permission gate and NOT a duration: it allows a destructive control, it does not hold one. inputModeReason explains any real→synthetic choice so it is never silent. Full model (real-input, throttled tabs, `reticle drive`): node_modules/@reticlehq/server/docs/usage.md §18.',
62
99
  inputSchema: {
63
100
  ref: z
64
101
  .string()
65
- .describe(`Element ref (e.g. 'e42') from reticle_snapshot/reticle_query — stable until the element leaves the DOM, so no re-snapshot between actions.`),
102
+ .optional()
103
+ .describe(`Element ref (e.g. 'e42') from reticle_snapshot/reticle_query — stable until the element leaves the DOM, so no re-snapshot between actions. Give this OR \`target\`.`),
104
+ target: z
105
+ .record(z.unknown())
106
+ .optional()
107
+ .describe('Find the element and act on it in ONE call, instead of a reticle_query round trip first: { testid } | { text } | { role, name } | { label }. Refuses if it matches more than one, rather than guessing.'),
66
108
  action: actionTypeEnum.describe(`Action to perform: ${ACTION_TYPE_LIST}`),
67
109
  args: z
68
110
  .record(z.unknown())
69
111
  .optional()
70
- .describe('Action-specific arguments: { value } for fill/select, { text } for type/press (the key NAME, e.g. Escape or Tab), { toRef } for drag (the ref to drop ON — without it the drag lands nowhere), { native: true } to force a trusted native click, { holdMs: N } to keep the pointer DOWN for N ms (hold-to-confirm controls; effect.heldMs reports what was achieved), { confirmDangerous: true } to allow a potentially destructive control — a permission gate, NOT a duration.'),
112
+ .describe('Action-specific arguments: { value } for fill/select, { text } for type/press (the key NAME, e.g. Escape or Tab), { modifiers: ["Meta","Shift"] } for a press shortcut (Meta/Control/Shift/Alt — a Cmd+K), { toRef } for drag (the ref to drop ON — without it the drag lands nowhere), { native: true } to force a trusted native click, { holdMs: N } to keep the pointer DOWN for N ms (hold-to-confirm controls; effect.heldMs reports what was achieved), { confirmDangerous: true } to allow a potentially destructive control — a permission gate, NOT a duration.'),
71
113
  refuseWhenThrottled: z
72
114
  .boolean()
73
115
  .optional()
@@ -107,10 +149,15 @@ export const ACT_TOOLS = [
107
149
  if (paused !== undefined)
108
150
  return paused;
109
151
  refuseIfThrottled(session, args['refuseWhenThrottled']);
152
+ // Resolve `target` to a ref BEFORE the action window opens, so the lookup is not attributed to
153
+ // the act and cannot be mistaken for something the action caused.
154
+ const targetRef = await resolveActTarget(session, args);
155
+ if ('error' === targetRef.kind)
156
+ throw new Error(targetRef.message);
110
157
  const since = session.elapsed();
111
158
  // The act cursor + effect are marked only once the action has actually DISPATCHED (below, on
112
159
  // each success path) — a refused act must leave nothing behind for the next observe to judge.
113
- const ref = asString(args['ref']) ?? '';
160
+ const ref = targetRef.ref;
114
161
  // Open the journal's action-attribution window BEFORE dispatching, so it covers the native path
115
162
  // too. It used to open only on the synthetic path, and the native path returned above it — so a
116
163
  // hover, a drag or any native:true click produced events with no actionId. Session.pushEvent
@@ -140,7 +187,7 @@ export const ACT_TOOLS = [
140
187
  });
141
188
  }
142
189
  const result = await session.command(ReticleCommand.ACT, {
143
- ref: args['ref'],
190
+ ref: targetRef.ref,
144
191
  action: args['action'],
145
192
  args: args['args'] ?? {},
146
193
  });
@@ -192,11 +239,18 @@ export const ACT_TOOLS = [
192
239
  steps: z
193
240
  .array(z.record(z.unknown()))
194
241
  .describe('Ordered list of { ref, action, args? } objects. Each step is equivalent to one reticle_act call; put confirmDangerous:true in a destructive step args object.'),
242
+ timeout_ms: z
243
+ .number()
244
+ .optional()
245
+ .describe('Per-step timeout in milliseconds. Default: 8000. Each step gets this budget independently.'),
195
246
  ...sessionIdShape,
196
247
  },
197
248
  outputSchema: {
198
249
  since: z.number(),
199
250
  dispatched: z.boolean(),
251
+ completed: z.number(),
252
+ stalled_at: z.number().optional(),
253
+ steps: z.array(z.record(z.unknown())).optional(),
200
254
  result: z.unknown().optional(),
201
255
  session: z
202
256
  .object({ lastSeenMs: z.number(), throttled: z.boolean(), focused: z.boolean() })
@@ -206,33 +260,84 @@ export const ACT_TOOLS = [
206
260
  },
207
261
  handler: async (deps, args) => {
208
262
  const session = deps.sessions.resolve(asString(args['sessionId']));
209
- // Live-control: refuse to drive the page while the human has paused us (before any work).
210
263
  const paused = pausedShortCircuit(session);
211
264
  if (paused !== undefined)
212
265
  return paused;
213
266
  const since = session.elapsed();
214
267
  session.beginAction(ReticleTool.ACT_SEQUENCE, asRecord(args));
215
268
  try {
216
- const result = await session.command(ReticleCommand.ACT_SEQUENCE, { steps: args['steps'] });
217
- if (!result.ok)
218
- throw new Error(result.error ?? 'act_sequence failed');
219
- // Marked only once the sequence ran: a refused sequence must leave no cursor behind for a
220
- // later observe to judge. No single action and no in-target measurement here — per-step
221
- // effects live in steps[] — so the effect is cleared rather than inherited from an earlier act.
222
- session.lastAct.markActed(since, undefined, undefined);
223
- if (deps.recordings.active().length > 0) {
224
- deps.recordings.capture(compileSequenceStep(args, result.result));
269
+ const inputSteps = Array.isArray(args['steps']) ? args['steps'] : [];
270
+ const perStepTimeout = 'number' === typeof args['timeout_ms'] ? args['timeout_ms'] : 8000;
271
+ const stepResults = [];
272
+ let stalledAt;
273
+ // DIVERGENCE: live sends N individual ACT commands (for per-step timeout + progress);
274
+ // replay sends one batched ACT_SEQUENCE command (flows/replay.ts:294). A bug in either is
275
+ // invisible from the other — cover both when changing sequence semantics.
276
+ for (let i = 0; i < inputSteps.length; i++) {
277
+ const step = asRecord(inputSteps[i]);
278
+ try {
279
+ const result = await session.command(ReticleCommand.ACT, { ref: step['ref'], action: step['action'], args: step['args'] ?? {} }, perStepTimeout);
280
+ if (!result.ok) {
281
+ stalledAt = i;
282
+ stepResults.push({
283
+ ref: step['ref'],
284
+ action: step['action'],
285
+ dispatched: false,
286
+ error: result.error ?? 'step failed',
287
+ });
288
+ break;
289
+ }
290
+ const r = asRecord(result.result);
291
+ const stepResult = {
292
+ ref: r['ref'] ?? step['ref'],
293
+ action: r['action'] ?? step['action'],
294
+ dispatched: r['dispatched'] ?? true,
295
+ settled: r['settled'] ?? null,
296
+ settleReason: r['settleReason'] ?? null,
297
+ };
298
+ if (r['testid'] !== undefined)
299
+ stepResult['testid'] = r['testid'];
300
+ if (r['component'] !== undefined)
301
+ stepResult['component'] = r['component'];
302
+ if (r['role'] !== undefined)
303
+ stepResult['role'] = r['role'];
304
+ if (r['name'] !== undefined)
305
+ stepResult['name'] = r['name'];
306
+ if (r['source'] !== undefined)
307
+ stepResult['source'] = r['source'];
308
+ if (r['warning'] !== undefined)
309
+ stepResult['warning'] = r['warning'];
310
+ stepResults.push(stepResult);
311
+ }
312
+ catch (err) {
313
+ stalledAt = i;
314
+ stepResults.push({
315
+ ref: step['ref'],
316
+ action: step['action'],
317
+ dispatched: null,
318
+ timedOut: true,
319
+ error: err instanceof Error ? err.message : 'step timed out',
320
+ });
321
+ break;
322
+ }
323
+ }
324
+ const completed = stalledAt ?? inputSteps.length;
325
+ if (completed > 0) {
326
+ session.lastAct.markActed(since, undefined, undefined);
327
+ }
328
+ if (deps.recordings.active().length > 0 && stalledAt === undefined) {
329
+ deps.recordings.capture(compileSequenceStep(args, { count: inputSteps.length, steps: stepResults }));
225
330
  }
226
- const r = asRecord(result.result); // per-step settle status lives in result.steps[]
227
331
  return withControl(session, {
228
332
  since,
229
- dispatched: r['count'] !== undefined,
230
- result: leanActResult(result.result),
333
+ dispatched: completed > 0,
334
+ completed,
335
+ ...(stalledAt !== undefined ? { stalled_at: stalledAt } : {}),
336
+ steps: stepResults,
231
337
  ...healthEnvelope(session),
232
338
  });
233
339
  }
234
340
  finally {
235
- // Per-step settle lives in steps[]; the sequence action records without a single settle bool.
236
341
  session.finishAction();
237
342
  }
238
343
  },
@@ -253,12 +358,17 @@ export const ACT_TOOLS = [
253
358
  inputSchema: {
254
359
  ref: z
255
360
  .string()
256
- .describe(`Element ref (e.g. 'e42') from reticle_snapshot/reticle_query — stable until the element leaves the DOM, so no re-snapshot between actions.`),
361
+ .optional()
362
+ .describe(`Element ref (e.g. 'e42') from reticle_snapshot/reticle_query — stable until the element leaves the DOM, so no re-snapshot between actions. Give this OR \`target\`.`),
363
+ target: z
364
+ .record(z.unknown())
365
+ .optional()
366
+ .describe('Find the element and act on it in ONE call, instead of a reticle_query round trip first: { testid } | { text } | { role, name } | { label }. Refuses if it matches more than one, rather than guessing.'),
257
367
  action: actionTypeEnum.describe(`Action to perform: ${ACTION_TYPE_LIST}`),
258
368
  args: z
259
369
  .record(z.unknown())
260
370
  .optional()
261
- .describe('Action-specific arguments: { value } for fill/select, { text } for type/press (the key NAME, e.g. Escape or Tab), { toRef } for drag (the ref to drop ON — without it the drag lands nowhere), { confirmDangerous: true } for a potentially destructive control.'),
371
+ .describe('Action-specific arguments: { value } for fill/select, { text } for type/press (the key NAME, e.g. Escape or Tab), { modifiers: ["Meta","Shift"] } for a press shortcut (Meta/Control/Shift/Alt), { toRef } for drag (the ref to drop ON — without it the drag lands nowhere), { confirmDangerous: true } for a potentially destructive control.'),
262
372
  predicate: PredicateSchema.optional().describe('Alias for `until` (the name reticle_assert / reticle_wait_for use).'),
263
373
  until: PredicateSchema.optional().describe('Predicate to wait for after the action completes (same shape as reticle_assert). OMIT to wait for the page to SETTLE — network + DOM idle — the deterministic default instead of a sleep. To assert a consequence AND settle, allOf them: { kind: "allOf", predicates: [<your predicate>, { kind: "settled" }] }.'),
264
374
  timeout_ms: z
@@ -298,7 +408,7 @@ export const ACT_TOOLS = [
298
408
  .describe('Reaction digest: { window_ms, summary } of what the app did (DOM/network/route/console/signal counts). The full per-event timeline is one reticle_observe { since } away.'),
299
409
  summary: z
300
410
  .unknown()
301
- .describe('Bounded causal summary: net {total,errors,headline}, consoleErrors, statePathsChanged, storageKeysChanged, stateDiffs [{path,from,to}], storageDiffs [{key,from,to}], route, signals, layoutShift, longTasks — real before→after diffs (capped), not just readings. Every list is length-capped so the verdict can never be the field a client truncates; `elided` says which lists lost entries and how many.'),
411
+ .describe('Bounded causal summary: net {total,errors,headline}, consoleErrors, statePathsChanged, storageKeysChanged, stateDiffs [{path,from,to}], storageDiffs [{key,from,to}], route, signals, layoutShift, longTasks — real before→after diffs (capped), not just readings. THIS IS THE ANSWER TO "what did that click do": console, network, storage and state for this action are all here, so do NOT follow an act with reticle_console + reticle_network + reticle_state to find out — call them only to go deeper into something this block already pointed at. Every list is length-capped so the verdict can never be the field a client truncates; `elided` says which lists lost entries and how many. `stateUnwatched: true` means NO subscribable store is registered, so an empty stateDiffs means unwatched, NOT unchanged — no state conclusion is available until one is registered.'),
302
412
  // Promoted out of `effect` on RED only — the file:line the failure came from, the first thing a
303
413
  // repair wants. Undeclared, it was stripped on the validating profile exactly like the structured
304
414
  // cause above was, losing the highest-value pointer on the one path (a failed verdict) that has it.
@@ -357,6 +467,11 @@ export const ACT_TOOLS = [
357
467
  // Before anything is driven: this path cannot honour a native-input request, and taking the
358
468
  // argument and ignoring it told the agent its trusted click had happened. See act-danger.
359
469
  assertNativeInputSupported(asRecord(args['args']));
470
+ // Resolve `target` to a ref BEFORE the action window opens, so the lookup is not attributed to
471
+ // the act and cannot be mistaken for something the action caused.
472
+ const resolved = await resolveActTarget(session, args);
473
+ if ('error' === resolved.kind)
474
+ throw new Error(resolved.message);
360
475
  const since = session.elapsed();
361
476
  // The cursor + effect are marked after the act dispatches (below) — a refused act leaves none.
362
477
  // The attribution window stays open across the settle wait below, so post-dispatch async events
@@ -372,7 +487,7 @@ export const ACT_TOOLS = [
372
487
  : false;
373
488
  try {
374
489
  const actResult = await session.command(ReticleCommand.ACT, {
375
- ref: args['ref'],
490
+ ref: resolved.ref,
376
491
  action: args['action'],
377
492
  args: args['args'] ?? {},
378
493
  });
@@ -444,6 +559,9 @@ export const ACT_TOOLS = [
444
559
  // verdict didn't see everything — say so, never imply full coverage.
445
560
  const spots = blindSpotsFromState(session.blindSpots());
446
561
  const coverage = buildCoverageStatement(spots);
562
+ // Nothing subscribed ⇒ the state channel is dark, and the summary must say so rather than
563
+ // report an empty diff list that reads like a fact about the app. See CausalSummary.
564
+ const stateUnwatched = spots.some((s) => s.kind === BlindSpotKind.UNWATCHED_STATE && s.count > 0);
447
565
  // Only a spot that IMPEACHES the capture belongs in integrity — see impeachesCapture. A
448
566
  // structural boundary (virtualized rows, a cross-origin frame) is reported as coverage and
449
567
  // must not downgrade a verdict about what WAS observed.
@@ -475,6 +593,9 @@ export const ACT_TOOLS = [
475
593
  // evictions that happened outside the window they impeached.
476
594
  truncated: bufferLost,
477
595
  coveragePartial: Coverage.PARTIAL === coverage.coverage,
596
+ // Settlement no longer vetoes a declared consequence that held, so the fact is carried
597
+ // here instead of only in the verdict it used to decide. Omitted when never measured.
598
+ ...(settledOutcome === undefined ? {} : { settled: settledOutcome }),
478
599
  ...(0 === impeachingNotes.length ? {} : { blindSpots: impeachingNotes }),
479
600
  losses,
480
601
  });
@@ -503,18 +624,30 @@ export const ACT_TOOLS = [
503
624
  //
504
625
  // `actionSince` is the window's own floor here, which is the tightest attribution there is —
505
626
  // and it is what lets `duplicate-request` claim "one user action was performed" and mean it.
627
+ // What the caller DECLARED before acting, handed to the detector that would otherwise judge
628
+ // the window as if nobody had said what they expected. See events/declared.ts.
629
+ const declared = declaredExpectations(until);
506
630
  const contradictions = findContradictions(windowEvents, {
507
631
  action: acted,
508
632
  prior,
509
633
  actionSince: since,
634
+ expectedFailures: declared.netFailures,
635
+ // A consequence that was already true before the action proves nothing about it, so it is
636
+ // not evidence the destination rendered either — `alreadyTrue` decides that, once.
637
+ renderProved: verdict.pass && !alreadyTrue && declared.rendersContent,
510
638
  ...session.lastAct.effect(),
511
639
  });
512
640
  // The single field an agent reads. Everything below it is the evidence it was derived from;
513
641
  // this is the only one that has to be interpreted, and now it interprets itself.
514
642
  const outcomePending = hasAcceptedWrite(windowEvents);
515
- const outcomeUnread = hasUnreadWriteOutcome(windowEvents);
643
+ const outcomeUnread = unreadWriteLabels(windowEvents);
516
644
  const decision = decideVerified({
517
645
  pass: verdict.pass,
646
+ // The caller NAMED the consequence rather than defaulting to "wait for idle". A
647
+ // declaration made before the action is what this tool sells, and idle-settlement was
648
+ // overriding it — see `declaredConsequence`. An explicit `{ kind: "settled" }` is not a
649
+ // declaration about the app's behaviour, it IS the idle wait, so it does not count.
650
+ declaredConsequence: until.kind !== PredicateKind.SETTLED,
518
651
  ...(alreadyTrue ? { alreadyTrue } : {}),
519
652
  // An assertion nobody could evaluate must not be reported as one the app failed.
520
653
  ...(verdict.inconclusive === undefined ? {} : { inconclusive: verdict.inconclusive }),
@@ -525,7 +658,7 @@ export const ACT_TOOLS = [
525
658
  honesty,
526
659
  contradictions,
527
660
  ...(outcomePending ? { outcomePending } : {}),
528
- ...(outcomeUnread ? { outcomeUnread } : {}),
661
+ ...(outcomeUnread.length > 0 ? { outcomeUnread } : {}),
529
662
  // `settled` is genuinely optional: a wait that declared no predicate never measured it, and
530
663
  // passing `false` there would report "never settled" about something never asked to settle.
531
664
  ...(settledOutcome === undefined ? {} : { settled: settledOutcome }),
@@ -535,6 +668,8 @@ export const ACT_TOOLS = [
535
668
  unsettled: {
536
669
  waitedFor: describeWaitTarget(until),
537
670
  stillInFlight: inFlightRequestLabels(windowEvents),
671
+ // The retry loop that leaves nothing outstanding — see repeatedRequestLabels.
672
+ repeated: repeatedRequestLabels(windowEvents),
538
673
  },
539
674
  });
540
675
  return withControl(session, {
@@ -549,7 +684,9 @@ export const ACT_TOOLS = [
549
684
  : { source: `${actedSource.file}:${String(actedSource.line)}` }),
550
685
  trace,
551
686
  ...(capsuleSaved === undefined ? {} : { capsuleSaved }),
552
- summary: causalSummary(windowEvents),
687
+ // The window cannot say whether anything was WATCHING state — that is a level fact the
688
+ // session holds. Without it an empty `stateDiffs` reads as "the app changed nothing".
689
+ summary: causalSummary(windowEvents, { stateUnwatched }),
553
690
  // Cross-channel disagreement, reported WITH the action that caused it.
554
691
  //
555
692
  // This is the one finding here a human structurally cannot make — they watch one channel,
@@ -1,13 +1,25 @@
1
- import { CaptureLoss } from '@reticlehq/core';
1
+ import { BlindSpotKind, CaptureLoss, PredicateKind } from '@reticlehq/core';
2
2
  import { findContradictions } from '../events/contradictions.js';
3
+ import { declaredExpectations } from '../events/declared.js';
3
4
  import { blindSpotsFromState, buildCoverageStatement, Coverage, impeachesCapture, transportGapNote, } from '../honesty/blind-spots.js';
4
5
  import { buildHonestyBlock } from '../honesty/honesty.js';
5
6
  import { hasAcceptedWrite } from '../honesty/accepted-write.js';
6
- import { hasUnreadWriteOutcome } from '../honesty/unread-outcome.js';
7
+ import { unreadWriteLabels } from '../honesty/unread-outcome.js';
7
8
  import { decideVerified } from '../honesty/verified.js';
8
9
  import { describeWaitTarget } from '../honesty/unsettled.js';
9
- import { inFlightRequestLabels } from './settle-in-flight.js';
10
+ import { inFlightRequestLabels, repeatedRequestLabels } from './settle-in-flight.js';
10
11
  import { gradeOfPredicate } from './assert-grade.js';
12
+ function absenceBlindSpotNote(predicate, spots) {
13
+ if (predicate.kind !== 'element' || predicate.absent !== true)
14
+ return undefined;
15
+ const relevant = spots.filter((spot) => spot.count > 0 &&
16
+ spot.kind === BlindSpotKind.CROSS_ORIGIN_IFRAME &&
17
+ undefined !== predicate.query.scope);
18
+ if (0 === relevant.length)
19
+ return undefined;
20
+ const statement = buildCoverageStatement(relevant);
21
+ return `the absence assertion targeted a region Reticle could not observe (${statement.note ?? 'partial coverage'}), so a passing DOM check cannot prove absence`;
22
+ }
11
23
  /**
12
24
  * The honesty verdict for a plain `reticle_assert`.
13
25
  *
@@ -37,6 +49,7 @@ observationLost) {
37
49
  // that implies coverage it never had, and a needless caveat costs the agent a sentence.
38
50
  const spots = blindSpotsFromState(session.blindSpots());
39
51
  const statement = buildCoverageStatement(spots);
52
+ const absenceBlindSpot = absenceBlindSpotNote(predicate, spots);
40
53
  // Omitted entirely when coverage is full, so an intact page pays nothing and the field's PRESENCE
41
54
  // is the warning.
42
55
  const coverage = Coverage.PARTIAL === statement.coverage
@@ -57,12 +70,18 @@ observationLost) {
57
70
  // could show it did. An assert taken over a window that predates the act attributes nothing, and the
58
71
  // rule then stays quiet.
59
72
  const actCursor = session.lastAct.cursor();
73
+ // The same declaration the act path reads. `reticle_assert` is the other half of the verdict
74
+ // surface, and an error path declared here deserves the same answer it gets there — a fix that
75
+ // lived only on the act path would leave the sibling caller broken.
76
+ const declared = declaredExpectations(predicate);
60
77
  const contradictions = findContradictions(windowEvents, {
61
78
  prior,
79
+ expectedFailures: declared.netFailures,
80
+ renderProved: pass && declared.rendersContent,
62
81
  ...(actCursor !== undefined && actCursor >= since ? { actionSince: actCursor } : {}),
63
82
  });
64
- // Only a spot that IMPEACHES the capture downgrades a verdict. A structural boundary (virtualized
65
- // rows, a cross-origin frame) is reported as coverage and must not impugn what WAS observed.
83
+ // Only a spot that IMPEACHES the capture downgrades a general verdict. Structural boundaries are
84
+ // reported as coverage; the narrower absence exception is computed separately above.
66
85
  const impeaching = buildCoverageStatement(spots.filter((sp) => impeachesCapture(sp.kind)));
67
86
  // A gap in the WINDOW, as opposed to a standing limit of the page. Both mean the same thing to the
68
87
  // rule — part of what happened was not seen — so both belong in `blindSpots`, which is the only
@@ -70,11 +89,16 @@ observationLost) {
70
89
  const gap = transportGapNote(windowEvents);
71
90
  const impeachingNotes = [impeaching.note, gap].filter((n) => n !== undefined);
72
91
  const outcomePending = hasAcceptedWrite(windowEvents);
73
- const outcomeUnread = hasUnreadWriteOutcome(windowEvents);
92
+ const outcomeUnread = unreadWriteLabels(windowEvents);
74
93
  const decision = decideVerified({
75
94
  pass,
95
+ // Same rule as the act path: the caller named a consequence, so a settlement-only finding must
96
+ // not override it. A fix that lived on one half of the verdict surface would leave the other
97
+ // half broken, and this is the tool agents call most.
98
+ declaredConsequence: predicate.kind !== PredicateKind.SETTLED,
76
99
  ...(inconclusive === undefined ? {} : { inconclusive }),
77
100
  ...(true === observationLost ? { observationLost: true } : {}),
101
+ ...(absenceBlindSpot === undefined ? {} : { absenceBlindSpot }),
78
102
  honesty: buildHonestyBlock({
79
103
  grade: gradeOfPredicate(predicate),
80
104
  attribution: 'window',
@@ -89,12 +113,13 @@ observationLost) {
89
113
  }),
90
114
  contradictions,
91
115
  ...(outcomePending ? { outcomePending } : {}),
92
- ...(outcomeUnread ? { outcomeUnread } : {}),
116
+ ...(outcomeUnread.length > 0 ? { outcomeUnread } : {}),
93
117
  // Same detail the act path supplies: this route reaches UNSETTLED through an absence-derived
94
118
  // contradiction, and "the window closed before the app finished" is no more actionable here.
95
119
  unsettled: {
96
120
  waitedFor: describeWaitTarget(predicate),
97
121
  stillInFlight: inFlightRequestLabels(windowEvents),
122
+ repeated: repeatedRequestLabels(windowEvents),
98
123
  },
99
124
  });
100
125
  return { decision: decision, contradictions, coverage };
@@ -39,6 +39,12 @@ export const BROWSER_TOOLS = [
39
39
  // reload:true is the absorbed reticle_refresh — same command, one fewer advertised tool.
40
40
  if (true === args['reload']) {
41
41
  const before = deps.sessions.resolve(asString(args['sessionId']));
42
+ // The verdict floor moves HERE, before the page goes away. A reload destroys the document,
43
+ // so every request that failed under the old one belongs to a page that no longer exists —
44
+ // and a later assert defaulting its window to "the last thing that happened" must not reach
45
+ // back past this point. Reported from the field as an assertion whose clauses all passed
46
+ // coming back `contradicted` by hundreds of 500s against resources that were already gone.
47
+ before.lastAct.markNavigated(before.elapsed());
42
48
  await commandOrThrow(deps, asString(args['sessionId']), ReticleCommand.REFRESH, {
43
49
  hard: true === args['hard'],
44
50
  });
@@ -65,6 +71,10 @@ export const BROWSER_TOOLS = [
65
71
  // reconnects), but the action record itself — "navigated to X" — is the causal fact worth keeping.
66
72
  const session = deps.sessions.resolve(asString(args['sessionId']));
67
73
  session.beginAction(ReticleTool.NAVIGATE, { url });
74
+ // Same floor as the reload path above, for the same reason: going to a new URL replaces the
75
+ // document just as thoroughly. `beginAction` attributes events to this action; it does not move
76
+ // the window a later assert judges over, and those are two different jobs.
77
+ session.lastAct.markNavigated(session.elapsed());
68
78
  try {
69
79
  const result = (await commandOrThrow(deps, asString(args['sessionId']), ReticleCommand.NAVIGATE, { url }));
70
80
  // `ok` is the browser accepting the instruction, not the page arriving — see navigate-result.
@@ -65,16 +65,17 @@ export function buildDynamicTools(allTools, profile) {
65
65
  : {
66
66
  profile: {
67
67
  ...profile,
68
- // NOT "every tool is reachable via reticle_run under any profile" — measured false:
69
- // `full` advertises all 46 tools DIRECTLY and carries no meta-tools at all, so
70
- // reticle_run does not exist there. Saying otherwise sends the agent to a tool the
71
- // profile does not have.
72
- note: `The profile is read once at daemon startup: change ${TOOL_PROFILE_ENV} and restart the daemon, or it has no effect. Under this profile every unadvertised tool is reachable through reticle_run; under \`full\` there are no meta-tools because every tool is advertised directly.`,
68
+ // This used to say `full` carries no meta-tools because it advertised everything
69
+ // directly. That stopped being true when the advertised surface was capped: no surface
70
+ // advertises the whole registry any more, so BOTH meta-tools are on every surface and
71
+ // reticle_run is always the way to the tail. The old wording would now send an agent
72
+ // away from the only tool that can reach half the registry.
73
+ note: `The surface is read once at daemon startup: change ${TOOL_PROFILE_ENV} and restart the daemon, or it has no effect. No surface advertises every tool — the advertised count is capped because editors budget tools across all connected MCP servers. Every tool listed here is callable through reticle_run { tool, args } whether or not it is advertised.`,
73
74
  },
74
75
  };
75
76
  const reticleTools = {
76
77
  name: ReticleTool.TOOLS,
77
- description: 'Discover Reticle tools on demand. Call with no arguments to list every tool (name + one-line summary); call with names:["reticle_network", …] to load full descriptions and parameters for specific tools. Then invoke them with reticle_run. This avoids paying for every tool definition on every turn. To make a verification REUSABLE (record once, replay free forever), the flow workflow lives here: reticle_record{action:"start"} → act → reticle_flow_save → reticle_flow_verify (and reticle_flow_heal on drift). Load those names when you want to save or re-run a flow.',
78
+ description: 'Discover Reticle tools on demand. Call with no arguments to list every tool (name + one-line summary); call with names:["reticle_network", …] to load full descriptions and parameters for specific tools. Then invoke them with reticle_run. This avoids paying for every tool definition on every turn. To make a verification REUSABLE (record once, replay free forever), the flow workflow lives here: reticle_record{action:"start"} → act → reticle_flow_save → reticle_verify{action:"flows"} (and reticle_flow_heal on drift). Load those names when you want to save or re-run a flow.',
78
79
  inputSchema: {
79
80
  names: z
80
81
  .array(z.string())
@@ -86,10 +87,18 @@ export function buildDynamicTools(allTools, profile) {
86
87
  ? args['names'].filter((n) => 'string' === typeof n)
87
88
  : undefined;
88
89
  if (names === undefined || 0 === names.length) {
90
+ // The COUNT is stated, not just implied by the array length. An agent that can see 18 tools
91
+ // has no way to know whether that is all of them; being told the registry holds more is what
92
+ // turns "these are the tools" into "these are the tools I was shown".
93
+ const catalog = allTools.map((t) => ({
94
+ name: t.name,
95
+ summary: firstSentence(t.description),
96
+ }));
89
97
  return Promise.resolve({
90
- tools: allTools.map((t) => ({ name: t.name, summary: firstSentence(t.description) })),
98
+ total: catalog.length,
99
+ tools: catalog,
91
100
  ...profileBlock,
92
- next: 'Load params with reticle_tools { names:[…] }, then call reticle_run { tool, args }.',
101
+ next: `All ${catalog.length} tools above are callable, advertised or not. Load full params with reticle_tools { names:[…] }, then call reticle_run { tool, args }.`,
93
102
  });
94
103
  }
95
104
  return Promise.resolve({
@@ -56,10 +56,12 @@ function isSelfRecovering(message) {
56
56
  }
57
57
  /** The recovery hints, named so they are not free strings and can be asserted in tests. */
58
58
  export const RECOVERY = {
59
- NO_SESSION: 'No app is connected to Reticle. Ask the human to start their app in dev with @reticlehq/browser ' +
60
- 'enabled, then run `reticle status` to confirm a session appears. If the app is running but no ' +
61
- 'session shows, the SDK is not reaching the bridge — check the dev server is up and using the ' +
62
- 'configured Reticle port.',
59
+ NO_SESSION: "No app is connected to Reticle. If nothing is serving the app, start the project's own dev " +
60
+ 'script (from its package.json) in the BACKGROUND yourself and tell the human in one line that ' +
61
+ 'it is running and how to stop it — never a second server, never a guessed command, never kill ' +
62
+ 'anything. Then run `reticle status` to confirm a session appears. If the app IS running and no ' +
63
+ 'session shows, the SDK is not reaching the bridge — check @reticlehq/browser is loaded in the ' +
64
+ 'page and that both sides use the configured Reticle port.',
63
65
  MULTIPLE_SESSIONS: 'Several tabs are connected. Call reticle_sessions to list them, then pass an explicit sessionId to ' +
64
66
  'target the one you mean.',
65
67
  UNKNOWN_SESSION: 'That sessionId is not connected. Call reticle_sessions for the current ids and retry with a valid one.',
@@ -121,11 +123,13 @@ export const RECOVERY = {
121
123
  COMMAND_TIMEOUT: 'The page did not answer within the command window. That is a fact about the page, not a Reticle ' +
122
124
  'failure: check reticle_sessions for `throttled`/`stale` on this session — a backgrounded tab is ' +
123
125
  'throttled and may never answer, so ask the human to bring it to the front or drive your own ' +
124
- 'browser with reticle_lease. If the tab is in front, the page is busy or blocked (a long ' +
126
+ 'browser with reticle_run { tool: "reticle_lease", action: "acquire", url } — reticle_lease is ' +
127
+ 'not advertised under the default profile, so it is reached through reticle_run, not called ' +
128
+ 'directly. If the tab is in front, the page is busy or blocked (a long ' +
125
129
  'synchronous task, an alert/confirm dialog); reticle_console usually shows what it hit. Retry ' +
126
130
  'once the cause is addressed.',
127
131
  FLOW_STEP_MISSING: 'A recorded step points at an element that is not on the page now. Either the app changed under ' +
128
- 'the flow — run reticle_flow_heal to re-anchor it, then reticle_flow_verify — or an earlier step ' +
132
+ 'the flow — run reticle_flow_heal to re-anchor it, then reticle_verify{action:"flows"} — or an earlier step ' +
129
133
  'did not do what it used to, leaving the run on the wrong page: check reticle_snapshot to see ' +
130
134
  'where it actually is. This is app/flow drift, which is exactly what the flow exists to catch.',
131
135
  NO_POOL: 'Reticle cannot launch its own browser here: the daemon-managed pool is absent, or Playwright ' +