@reticlehq/server 2.8.0 → 2.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (292) hide show
  1. package/SKILL.md +130 -813
  2. package/dist/bridge/bridge.d.ts +23 -0
  3. package/dist/bridge/bridge.js +80 -2
  4. package/dist/bridge/bridge.test-harness.d.ts +5 -0
  5. package/dist/bridge/bridge.test-harness.js +6 -4
  6. package/dist/capsule/causal-summary.d.ts +16 -1
  7. package/dist/capsule/causal-summary.js +2 -1
  8. package/dist/cli/cli-doctor.js +41 -13
  9. package/dist/cli/cli-flow-commands.js +8 -8
  10. package/dist/cli/cli-launch.d.ts +0 -17
  11. package/dist/cli/cli-launch.js +0 -28
  12. package/dist/cli/cli-port.d.ts +24 -0
  13. package/dist/cli/cli-port.js +66 -35
  14. package/dist/cli/cli-verify.d.ts +21 -0
  15. package/dist/cli/cli-verify.js +45 -1
  16. package/dist/cli/config-discovery.d.ts +35 -0
  17. package/dist/cli/config-discovery.js +163 -0
  18. package/dist/cli/doctor-daemon-line.js +4 -1
  19. package/dist/cli/doctor-project-line.d.ts +37 -0
  20. package/dist/cli/doctor-project-line.js +35 -0
  21. package/dist/cli/doctor-rows.d.ts +51 -0
  22. package/dist/cli/doctor-rows.js +54 -0
  23. package/dist/cli/doctor-sessions-line.js +6 -3
  24. package/dist/cli/drive-attach.d.ts +99 -0
  25. package/dist/cli/drive-attach.js +203 -0
  26. package/dist/cli/drive-command.d.ts +13 -0
  27. package/dist/cli/drive-command.js +103 -0
  28. package/dist/cli/open-note.d.ts +20 -0
  29. package/dist/cli/open-note.js +37 -0
  30. package/dist/cli/status-next-action.d.ts +37 -0
  31. package/dist/cli/status-next-action.js +61 -0
  32. package/dist/cli.js +65 -70
  33. package/dist/events/contradictions.d.ts +32 -0
  34. package/dist/events/contradictions.js +9 -5
  35. package/dist/events/declared.d.ts +45 -0
  36. package/dist/events/declared.js +73 -0
  37. package/dist/events/echo-mismatch.d.ts +8 -2
  38. package/dist/events/echo-mismatch.js +33 -5
  39. package/dist/events/event-filters.d.ts +1 -1
  40. package/dist/events/event-filters.js +7 -4
  41. package/dist/events/predicate-eval.d.ts +17 -91
  42. package/dist/events/predicate-eval.js +85 -326
  43. package/dist/events/predicate-parse.js +31 -3
  44. package/dist/events/predicate-route.d.ts +32 -0
  45. package/dist/events/predicate-route.js +102 -0
  46. package/dist/events/predicate-schema.d.ts +99 -0
  47. package/dist/events/predicate-schema.js +309 -0
  48. package/dist/events/predicate.d.ts +8 -0
  49. package/dist/events/predicate.js +127 -5
  50. package/dist/flows/annotate-tools.js +3 -0
  51. package/dist/flows/annotate.js +18 -4
  52. package/dist/flows/coverage.d.ts +38 -5
  53. package/dist/flows/coverage.js +30 -4
  54. package/dist/flows/decision.d.ts +15 -0
  55. package/dist/flows/decision.js +1 -1
  56. package/dist/flows/flow-replay-run.js +8 -1
  57. package/dist/flows/flow-tools.js +9 -1
  58. package/dist/flows/replay.js +3 -0
  59. package/dist/honesty/accepted-write.d.ts +3 -0
  60. package/dist/honesty/accepted-write.js +8 -3
  61. package/dist/honesty/already-true.js +15 -0
  62. package/dist/honesty/blind-spots.js +4 -0
  63. package/dist/honesty/honesty.d.ts +11 -0
  64. package/dist/honesty/honesty.js +1 -0
  65. package/dist/honesty/uncaptured-bodies.d.ts +4 -2
  66. package/dist/honesty/uncaptured-bodies.js +9 -2
  67. package/dist/honesty/unread-outcome.d.ts +8 -0
  68. package/dist/honesty/unread-outcome.js +36 -9
  69. package/dist/honesty/unsettled.d.ts +9 -0
  70. package/dist/honesty/unsettled.js +7 -3
  71. package/dist/honesty/verified.d.ts +29 -3
  72. package/dist/honesty/verified.js +51 -8
  73. package/dist/http-server.d.ts +19 -0
  74. package/dist/http-server.js +202 -4
  75. package/dist/impact/impact-recorder.d.ts +23 -0
  76. package/dist/impact/impact-recorder.js +76 -0
  77. package/dist/impact/impact-store.d.ts +47 -0
  78. package/dist/impact/impact-store.js +171 -0
  79. package/dist/index.d.ts +7 -0
  80. package/dist/index.js +44 -10
  81. package/dist/init/agent-rules.d.ts +20 -0
  82. package/dist/init/agent-rules.js +36 -0
  83. package/dist/init/astro-patch.js +27 -1
  84. package/dist/init/confirm.d.ts +21 -3
  85. package/dist/init/confirm.js +32 -5
  86. package/dist/init/connect-steps.d.ts +2 -0
  87. package/dist/init/connect-steps.js +50 -0
  88. package/dist/init/csp-check.d.ts +38 -0
  89. package/dist/init/csp-check.js +92 -0
  90. package/dist/init/csp-doctor.d.ts +25 -0
  91. package/dist/init/csp-doctor.js +49 -0
  92. package/dist/init/cursor.js +6 -9
  93. package/dist/init/detect.d.ts +8 -0
  94. package/dist/init/detect.js +33 -3
  95. package/dist/init/mcp-clients.d.ts +1 -0
  96. package/dist/init/mcp-clients.js +51 -7
  97. package/dist/init/mcp.d.ts +15 -0
  98. package/dist/init/mcp.js +19 -0
  99. package/dist/init/plan-framework.d.ts +10 -0
  100. package/dist/init/plan-framework.js +25 -1
  101. package/dist/init/plan.d.ts +7 -2
  102. package/dist/init/plan.js +66 -30
  103. package/dist/init/run.js +11 -5
  104. package/dist/init/slash-command.d.ts +1 -1
  105. package/dist/init/slash-command.js +12 -4
  106. package/dist/init/snippets.d.ts +1 -1
  107. package/dist/init/snippets.js +35 -7
  108. package/dist/init/vite-config.js +24 -2
  109. package/dist/input/network-mock-tools.js +8 -1
  110. package/dist/input/scroll-find.d.ts +6 -0
  111. package/dist/input/scroll-find.js +17 -2
  112. package/dist/input/scroll-tools.js +4 -0
  113. package/dist/journal/attach-journal.d.ts +9 -1
  114. package/dist/journal/attach-journal.js +13 -2
  115. package/dist/journal/journal-query.d.ts +13 -0
  116. package/dist/journal/journal-query.js +28 -3
  117. package/dist/journal/workspace-gitignore.d.ts +10 -0
  118. package/dist/journal/workspace-gitignore.js +59 -0
  119. package/dist/mcp/mcp-proxy.d.ts +5 -66
  120. package/dist/mcp/mcp-proxy.js +180 -163
  121. package/dist/mcp/mcp.d.ts +10 -1
  122. package/dist/mcp/mcp.js +47 -27
  123. package/dist/mcp/proxy-handshake.d.ts +82 -1
  124. package/dist/mcp/proxy-handshake.js +109 -2
  125. package/dist/mcp/proxy-lifecycle.d.ts +10 -1
  126. package/dist/mcp/proxy-lifecycle.js +11 -2
  127. package/dist/mcp/proxy-log.d.ts +63 -0
  128. package/dist/mcp/proxy-log.js +132 -0
  129. package/dist/mcp/server-instructions.d.ts +17 -0
  130. package/dist/mcp/server-instructions.js +56 -0
  131. package/dist/mcp/shared-params.d.ts +27 -0
  132. package/dist/mcp/shared-params.js +36 -0
  133. package/dist/project/reticle-dir.d.ts +2 -0
  134. package/dist/project/reticle-dir.js +1 -0
  135. package/dist/session/connection-memory.d.ts +59 -0
  136. package/dist/session/connection-memory.js +108 -0
  137. package/dist/session/dev-command.d.ts +26 -0
  138. package/dist/session/dev-command.js +89 -0
  139. package/dist/session/dev-server-probe.d.ts +68 -0
  140. package/dist/session/dev-server-probe.js +102 -7
  141. package/dist/session/last-act.d.ts +26 -0
  142. package/dist/session/last-act.js +35 -1
  143. package/dist/session/live-control-tools.js +42 -5
  144. package/dist/session/no-session-diagnosis.d.ts +48 -2
  145. package/dist/session/no-session-diagnosis.js +176 -20
  146. package/dist/session/no-session-next-action.d.ts +46 -0
  147. package/dist/session/no-session-next-action.js +85 -0
  148. package/dist/session/no-session-watch.d.ts +40 -1
  149. package/dist/session/no-session-watch.js +165 -11
  150. package/dist/session/pending-commands.d.ts +12 -0
  151. package/dist/session/pending-commands.js +13 -1
  152. package/dist/session/session-manager.d.ts +4 -0
  153. package/dist/session/session-manager.js +35 -5
  154. package/dist/session/session.d.ts +10 -1
  155. package/dist/session/session.js +146 -5
  156. package/dist/telemetry/app-instrumented.d.ts +8 -0
  157. package/dist/telemetry/app-instrumented.js +10 -0
  158. package/dist/telemetry/bug-found.d.ts +18 -0
  159. package/dist/telemetry/bug-found.js +14 -0
  160. package/dist/telemetry/daemon-telemetry.js +24 -0
  161. package/dist/telemetry/install-source.d.ts +20 -0
  162. package/dist/telemetry/install-source.js +34 -0
  163. package/dist/telemetry/instrumentation-stall.d.ts +50 -0
  164. package/dist/telemetry/instrumentation-stall.js +90 -0
  165. package/dist/telemetry/session-metrics.d.ts +8 -0
  166. package/dist/telemetry/session-metrics.js +10 -0
  167. package/dist/telemetry/telemetry.d.ts +3 -1
  168. package/dist/telemetry/telemetry.js +13 -2
  169. package/dist/tools/act-tools.js +166 -29
  170. package/dist/tools/assert-verdict.js +32 -7
  171. package/dist/tools/browser-tools.js +10 -0
  172. package/dist/tools/dynamic-tools.js +16 -7
  173. package/dist/tools/error-recovery.js +9 -5
  174. package/dist/tools/invoke-tool.js +20 -3
  175. package/dist/tools/lease-hint.d.ts +30 -8
  176. package/dist/tools/lease-hint.js +65 -14
  177. package/dist/tools/lease-tools.d.ts +11 -0
  178. package/dist/tools/lease-tools.js +141 -96
  179. package/dist/tools/observe-tools.js +9 -3
  180. package/dist/tools/react-grace.d.ts +4 -0
  181. package/dist/tools/react-grace.js +6 -3
  182. package/dist/tools/read-tools.js +47 -1
  183. package/dist/tools/resolve-target.d.ts +33 -0
  184. package/dist/tools/resolve-target.js +53 -0
  185. package/dist/tools/settle-in-flight.d.ts +15 -0
  186. package/dist/tools/settle-in-flight.js +42 -2
  187. package/dist/tools/snapshot-delta.d.ts +3 -0
  188. package/dist/tools/snapshot-delta.js +67 -6
  189. package/dist/tools/tool-kit.d.ts +8 -0
  190. package/dist/tools/tool-surface.d.ts +60 -0
  191. package/dist/tools/tool-surface.js +85 -19
  192. package/dist/tools/tools.js +45 -4
  193. package/dist/tools/unadvertised-help.js +9 -6
  194. package/docs/README.md +1 -2
  195. package/docs/actions.mdx +41 -8
  196. package/docs/agent-cheatsheet.md +4 -2
  197. package/docs/architecture.md +6 -0
  198. package/docs/benchmarks.md +60 -12
  199. package/docs/best-practices.mdx +11 -6
  200. package/docs/capabilities.mdx +201 -54
  201. package/docs/cli/affected.mdx +6 -4
  202. package/docs/cli/capsules.mdx +9 -8
  203. package/docs/cli/cloud.mdx +30 -4
  204. package/docs/cli/doctor.mdx +41 -8
  205. package/docs/cli/drive.mdx +34 -15
  206. package/docs/cli/gate.mdx +6 -6
  207. package/docs/cli/help.mdx +8 -6
  208. package/docs/cli/hunt.mdx +18 -2
  209. package/docs/cli/identify.mdx +16 -2
  210. package/docs/cli/init.mdx +35 -8
  211. package/docs/cli/kill.mdx +5 -5
  212. package/docs/cli/license.mdx +1 -1
  213. package/docs/cli/open.mdx +6 -0
  214. package/docs/cli/restart.mdx +6 -6
  215. package/docs/cli/serve.mdx +10 -3
  216. package/docs/cli/status.mdx +29 -5
  217. package/docs/cli/stop.mdx +8 -0
  218. package/docs/cli/verify.mdx +20 -1
  219. package/docs/cli/version.mdx +1 -1
  220. package/docs/cli/watch.mdx +9 -2
  221. package/docs/cli.mdx +44 -22
  222. package/docs/debugging.md +3 -1
  223. package/docs/deploy-checks.md +30 -1
  224. package/docs/desktop.mdx +276 -80
  225. package/docs/docs.json +13 -12
  226. package/docs/enterprise.md +2 -0
  227. package/docs/faq.mdx +49 -35
  228. package/docs/fixtures.md +2 -0
  229. package/docs/flows.md +3 -1
  230. package/docs/for-agents.md +3 -3
  231. package/docs/frameworks.mdx +46 -7
  232. package/docs/gate-plan.md +4 -2
  233. package/docs/gates.md +7 -5
  234. package/docs/getting-started.md +53 -12
  235. package/docs/human-control.md +2 -0
  236. package/docs/index.mdx +43 -1
  237. package/docs/install-agentic.mdx +13 -4
  238. package/docs/install-manual.mdx +6 -4
  239. package/docs/instrumentation.mdx +7 -1
  240. package/docs/integration-patterns.md +3 -1
  241. package/docs/local-registry.md +4 -2
  242. package/docs/multi-agent-testing.md +3 -1
  243. package/docs/packages/babel-plugin.mdx +15 -6
  244. package/docs/packages/browser.mdx +2 -2
  245. package/docs/packages/core.mdx +26 -16
  246. package/docs/packages/electron.mdx +5 -7
  247. package/docs/packages/eslint-plugin.mdx +5 -3
  248. package/docs/packages/next.mdx +3 -5
  249. package/docs/packages/react.mdx +3 -3
  250. package/docs/packages/server.mdx +2 -2
  251. package/docs/packages/tauri.mdx +6 -6
  252. package/docs/packages/test.mdx +18 -7
  253. package/docs/packages/vite-plugin.mdx +4 -6
  254. package/docs/packages.mdx +14 -13
  255. package/docs/platform-integration.md +2 -0
  256. package/docs/predicates.mdx +76 -7
  257. package/docs/quickstart.mdx +51 -5
  258. package/docs/recipes.mdx +62 -22
  259. package/docs/skill-file.mdx +3 -1
  260. package/docs/state-management.mdx +2 -0
  261. package/docs/system-map.md +3 -1
  262. package/docs/telemetry-contract.md +16 -5
  263. package/docs/telemetry-events.mdx +63 -18
  264. package/docs/telemetry.md +8 -5
  265. package/docs/testing.md +35 -1
  266. package/docs/token-efficiency.md +8 -2
  267. package/docs/tools-act-and-wait.mdx +64 -14
  268. package/docs/tools-act-sequence.mdx +34 -17
  269. package/docs/tools-act.mdx +24 -11
  270. package/docs/tools-assert.mdx +69 -10
  271. package/docs/tools-console.mdx +11 -7
  272. package/docs/tools-inspect.mdx +11 -8
  273. package/docs/tools-navigate.mdx +41 -5
  274. package/docs/tools-network.mdx +42 -9
  275. package/docs/tools-observe.mdx +66 -10
  276. package/docs/tools-overview.mdx +26 -23
  277. package/docs/tools-query.mdx +52 -8
  278. package/docs/tools-session-and-feedback.mdx +30 -9
  279. package/docs/tools-sessions.mdx +39 -10
  280. package/docs/tools-snapshot.mdx +57 -3
  281. package/docs/tools-state.mdx +24 -1
  282. package/docs/tools-tools-and-run.mdx +40 -9
  283. package/docs/tools-wait-for.mdx +34 -15
  284. package/docs/troubleshooting.mdx +80 -10
  285. package/docs/usage.md +43 -21
  286. package/docs/vs-chrome-devtools-mcp.mdx +41 -0
  287. package/docs/vs-playwright-mcp.mdx +42 -2
  288. package/docs/vs-screenshots.mdx +42 -1
  289. package/docs/what-is-reticle.mdx +1 -1
  290. package/docs/why-reticle.mdx +3 -1
  291. package/package.json +5 -5
  292. package/docs/desktop-apps.md +0 -265
@@ -4,6 +4,8 @@ description: 'Adopt Reticle on your own codebase, or embed it inside an AI app-b
4
4
  icon: plug
5
5
  ---
6
6
 
7
+ Two ways in. **A team** runs `npx @reticlehq/server init` once and points its coding agent at the MCP server. **A platform** adds the dev-only SDK to its generated-app template, runs `npx @reticlehq/server serve --http --drive <preview-url>` beside each preview, and `POST`s to `/verify` after every generate or edit, gating the ship on `run.verdict.status === 'pass'` and feeding `run.repair.failurePackets[]` back to a fixer agent.
8
+
7
9
  > The one guide for adopting Reticle, both for a team using a coding agent on its own app and for an AI app-builder platform (Lovable / Emergent / Bolt) embedding Reticle in its generation pipeline. Reticle reads the program from _inside_ a running app and returns a **verdict with evidence** ("did it actually work?"), not a screenshot. Enterprise/premium access lives in [`enterprise.md`](./enterprise.md).
8
10
 
9
11
  ## The loop
@@ -1,11 +1,13 @@
1
1
  ---
2
- title: Predicate reference: the grammar of green
2
+ title: 'Predicate reference: the grammar of green'
3
3
  sidebarTitle: Predicates
4
4
  description: The grammar for declaring what should be true. Every kind, every field, and which ones actually prove something.
5
5
  icon: filter
6
6
  ---
7
7
 
8
- A predicate declares what should be true. It is the argument to `until` on [`reticle_act_and_wait`](/tools-act-and-wait), and to `predicate` on [`reticle_assert`](/tools-assert) and [`reticle_wait_for`](/tools-wait-for).
8
+ A predicate declares what should be true, as `{ kind, ...fields }`. Reticle has twelve kinds: `signal`, `state`, `net`, `route`, `element`, `text`, `console`, `animation`, `settled`, and the combinators `allOf`, `anyOf` and `not`. Which one you pick decides whether a green means anything.
9
+
10
+ It is the argument to `until` on [`reticle_act_and_wait`](/tools-act-and-wait), and to `predicate` on [`reticle_assert`](/tools-assert) and [`reticle_wait_for`](/tools-wait-for).
9
11
 
10
12
  Every predicate is `{ kind, ...fields }`.
11
13
 
@@ -31,6 +33,8 @@ Take the advice. `element` and `text` are convenient and weak; `signal`, `state`
31
33
  | `route` | medium | Navigation happened |
32
34
  | `console` | medium | Something logged, or nothing did |
33
35
  | `element` / `text` | weak | Something rendered. A mock renders too |
36
+ | `animation` | weak | An animation ran or finished |
37
+ | `settled` | timing | The page went quiet. A precondition, not a proof |
34
38
 
35
39
  ## The leaves
36
40
 
@@ -54,7 +58,7 @@ Add `"absent": true` to assert something is **gone**. The predicate for a remova
54
58
  { "kind": "text", "contains": "Saved successfully", "visible": true }
55
59
  ```
56
60
 
57
- Both `contains` and `value` are accepted. `absent: true` asserts the text is not present.
61
+ `contains` is the field the published schema declares. `value` and `text` are accepted as aliases and rewritten to it. `absent: true` asserts the text is not present, and `visible: true` requires it to be on screen rather than merely in the DOM.
58
62
 
59
63
  ### net
60
64
 
@@ -76,7 +80,19 @@ The one that catches the expensive bug:
76
80
  { "kind": "route", "pathname": "/success" }
77
81
  ```
78
82
 
79
- `contains` also works.
83
+ `pathname` matches the path exactly. `contains` matches anywhere in the whole route, path, query and fragment together. `path` is accepted as an alias for `pathname`, and `url` and `urlContains` as aliases for `contains`, but the canonical spellings are the ones above.
84
+
85
+ A passing route verdict quotes the transition itself:
86
+
87
+ ```json
88
+ "evidence": {
89
+ "from": "http://localhost:4310/",
90
+ "to": "http://localhost:4310/deployments",
91
+ "pathname": "/deployments",
92
+ "search": "",
93
+ "hash": ""
94
+ }
95
+ ```
80
96
 
81
97
  <Warning>
82
98
  `route` asserts the route **changed**. Asserting the route you are already on always fails, and
@@ -115,7 +131,35 @@ Walks a dot-path, with numeric array indices. `equals` takes a literal, or an op
115
131
  | `$contains` | substring or array membership |
116
132
  | `$length` | collection size |
117
133
 
118
- Omit `equals` entirely to assert presence.
134
+ Omit `equals` entirely to assert presence. A real `$length` check against this fixture:
135
+
136
+ ```json
137
+ { "kind": "state", "store": "app", "path": "deployments", "equals": { "$length": 40 } }
138
+ ```
139
+
140
+ ```json
141
+ {
142
+ "verified": "yes",
143
+ "because": "assertion held at state grade over a clean capture with no channel disagreeing",
144
+ "pass": true,
145
+ "evidence": { "store": "app", "path": "deployments", "value": ["{…11 keys}", "…"] }
146
+ }
147
+ ```
148
+
149
+ Note `grade: state` in the `because` line, and that the evidence collapses each row to a size marker rather than returning forty objects.
150
+
151
+ A miss quotes the real value back at you:
152
+
153
+ ```json
154
+ {
155
+ "pass": false,
156
+ "failureReason": "state 'deployments.0.status' is \"queued\", expected \"live\"",
157
+ "observed": "deployments.0.status = \"queued\"",
158
+ "expected": "deployments.0.status = \"live\"",
159
+ "assertion": "state.equals",
160
+ "evidence": { "store": "app", "path": "deployments.0.status", "value": "queued" }
161
+ }
162
+ ```
119
163
 
120
164
  `store` is optional. Leave it out and the path picks the store: if exactly one registered store exposes it, that one is read. Registering several is normal, so this is the common case rather than a shortcut. Name a store when two of them genuinely expose the same path, which is the only situation Reticle cannot resolve on its own. If no store exposes it, that is a failure rather than a question, and the miss names the stores it searched.
121
165
 
@@ -127,6 +171,16 @@ This is the predicate that catches a UI-versus-store desync. A deploy that only
127
171
  { "kind": "animation", "name": "dialog-in", "completed": true }
128
172
  ```
129
173
 
174
+ `target` narrows it to one element. `completed: false` matches an animation that is still running.
175
+
176
+ ### settled
177
+
178
+ ```json
179
+ { "kind": "settled", "quietMs": 500 }
180
+ ```
181
+
182
+ Network and DOM idle. This is the deterministic replacement for a fixed sleep, not a proof of anything: combine it with a real consequence rather than asserting it alone. Omitting `until` on [`reticle_act_and_wait`](/tools-act-and-wait) waits for exactly this.
183
+
130
184
  ## Combinators
131
185
 
132
186
  ```json
@@ -149,13 +203,13 @@ This is the predicate that catches a UI-versus-store desync. A deploy that only
149
203
  A combinator needs its own `kind`, and the children go in `predicates`. A bare
150
204
  `{ "allOf": [ … ] }` does not parse, and neither does `{ "kind": "allOf", "allOf": [ … ] }`.
151
205
 
152
- Reticle refuses the call rather than running half of it: _"Nothing ran, the predicate was not evaluated, so no verdict was produced."_ Better to be told than to get a green from an assertion that never executed.
206
+ Reticle refuses the call rather than running half of it. The real refusal names the field, the fix and the consequence: _"predicates: Required; unknown field allOf. Nothing ran, the predicate was not evaluated, so no verdict was produced. allOf accepts: predicates."_ Better to be told than to get a green from an assertion that never executed.
153
207
 
154
208
  </Warning>
155
209
 
156
210
  `allOf` is the workhorse. "The signal fired **and** exactly one request went out **and** the console stayed clean" is a genuinely strong check, and it is one call.
157
211
 
158
- Each child reports its own evidence:
212
+ Each child reports its own evidence, in order. That is a real verdict for exactly the `allOf` above:
159
213
 
160
214
  ```json
161
215
  "evidence": [
@@ -165,6 +219,21 @@ Each child reports its own evidence:
165
219
  ]
166
220
  ```
167
221
 
222
+ On a failure each child reports separately too, so you can see which one broke:
223
+
224
+ ```json
225
+ "evidence": [
226
+ {
227
+ "pass": false,
228
+ "expected": "an element matching {\"role\":\"dialog\"} in state 'visible'",
229
+ "observed": "no matching element on the page",
230
+ "assertion": "element.present",
231
+ "evidence": { "presentTestids": ["deploy-table", "filter-search", "env-filter"] }
232
+ },
233
+ { "pass": true, "evidence": { "absent": true } }
234
+ ]
235
+ ```
236
+
168
237
  ## Timing and scoping
169
238
 
170
239
  | Field | Where | What it does |
@@ -1,10 +1,12 @@
1
1
  ---
2
- title: Quickstart: proof in five minutes
2
+ title: 'Quickstart: proof in five minutes'
3
3
  sidebarTitle: Quickstart
4
4
  description: Install Reticle, connect your coding agent, and get a real verdict with evidence in about five minutes.
5
5
  icon: rocket
6
6
  ---
7
7
 
8
+ **Run `npx @reticlehq/server init` in your project, restart your dev server and your agent, then ask your agent to drive a flow.** That is the whole install. Reticle is a verification layer for AI coding agents: it embeds a dev-only SDK in your running web app so an agent can prove a change works instead of guessing.
9
+
8
10
  By the end of this page your agent will click a button in your app and prove what happened: the request that fired, the state that changed, and the file to open if it didn't.
9
11
 
10
12
  <Note>
@@ -81,10 +83,10 @@ npx @reticlehq/server open
81
83
  This reuses the tab you already have open, or opens one if you don't. The SDK connects to the local daemon over a WebSocket, and your agent can now see the app.
82
84
 
83
85
  <Warning>
84
- If the browser console says it "could not reach the bridge", your app and the daemon disagree
85
- about the port. Set it explicitly: `VITE_RETICLE_WS_URL=ws://localhost:4400/reticle` for Vite, or
86
- `reticle.connect({url})`. This is the single most common setup problem, and the console message
87
- tells you the exact port it tried.
86
+ If the browser console says `[Reticle] this page could not open a websocket to …`, your app and
87
+ the daemon disagree about the port. Set it explicitly:
88
+ `VITE_RETICLE_WS_URL=ws://localhost:4400/reticle` for Vite, or `reticle.connect({url})`. This is
89
+ the single most common setup problem, and the console message names the exact address it tried.
88
90
  </Warning>
89
91
 
90
92
  ## Step 4. Look at the page
@@ -193,6 +195,50 @@ Read what that actually proves:
193
195
 
194
196
  That third state is the one that matters. Most tools have two outcomes and quietly file "I couldn't tell" under "fine". A verification layer that cannot admit uncertainty is just a very confident random number generator.
195
197
 
198
+ ## Frequently asked questions
199
+
200
+ <AccordionGroup>
201
+ <Accordion title="How long does it actually take to install Reticle?">
202
+ One command and two restarts. `npx @reticlehq/server init` registers the MCP server, installs
203
+ the SDK, wires your build config and writes the agent rules. Then restart your dev server, so it
204
+ picks up the new build plugin, and restart your agent, so it re-reads its MCP server list. The
205
+ MCP registration is global, so every later project starts with the tools already there.
206
+ </Accordion>
207
+
208
+ <Accordion title="Do I need to write tests or learn a query syntax?">
209
+ No. You talk to your agent in plain language and it calls the tools. There is no spec file, no
210
+ selector language and no test runner involved in the loop on this page. If you later want the same
211
+ flow to block a bad merge, you can save it and run it in CI, but that is a separate step.
212
+ </Accordion>
213
+
214
+ <Accordion title="Will any of this end up in my production build?">
215
+ No. The SDK is dev-only. The Vite plugin declares `apply: 'serve'`, so it is dropped from `vite
216
+ build` entirely, and `connect()` self-disables when the build reports `NODE_ENV=production`. The
217
+ recommended wiring also guards the call behind `import.meta.env.DEV`, so there are two independent
218
+ locks.
219
+ </Accordion>
220
+
221
+ <Accordion title="Does my code or my app's data leave my machine?">
222
+ No. The daemon runs on your machine and the SDK runs in your browser. No application code, DOM,
223
+ network traffic or console output leaves it. Anonymous product analytics are sent unless you opt
224
+ out with `npx @reticlehq/server telemetry disable`, and they never include your code or your app's
225
+ data.
226
+ </Accordion>
227
+
228
+ <Accordion title="What if my framework is not React or Next.js?">
229
+ The core is framework-neutral, so DOM, network, console, routing and source mapping work anywhere
230
+ JavaScript runs. What varies is how `connect()` gets into your app and whether you get component
231
+ identity on top. See [Frameworks](/frameworks) for the exact wiring per stack and an honest table
232
+ of what is gated in CI versus wired but unverified.
233
+ </Accordion>
234
+
235
+ <Accordion title="Nothing connected. What is the first thing to check?">
236
+ Run `npx @reticlehq/server doctor`. If it shows the daemon running but `sessions` at zero, your
237
+ app is not reaching the bridge, and the usual cause is a port mismatch. The bridge port is not
238
+ your dev-server port. See [Troubleshooting](/troubleshooting).
239
+ </Accordion>
240
+ </AccordionGroup>
241
+
196
242
  ## Where to go next
197
243
 
198
244
  <CardGroup cols={2}>
package/docs/recipes.mdx CHANGED
@@ -1,11 +1,11 @@
1
1
  ---
2
- title: Recipes: copy, paste, verify
2
+ title: 'Recipes: copy, paste, verify'
3
3
  sidebarTitle: Recipes
4
4
  description: Worked examples for the situations you are actually in, with the real response each one returned.
5
5
  icon: book-open
6
6
  ---
7
7
 
8
- Each recipe is a situation, the call that verifies it, and what came back. Every response on this page was captured from a running app.
8
+ Each recipe is a situation, the call that verifies it, and what came back. Every response on this page was captured from a running app on 2026-08-16. Refs like `e605` are session-specific: take your own from [`reticle_query`](/tools-query) before copying a call.
9
9
 
10
10
  ## "Did the login actually work?"
11
11
 
@@ -13,7 +13,7 @@ One call, three conditions. The signal fired, exactly one request went out, and
13
13
 
14
14
  ```json
15
15
  {
16
- "ref": "e5",
16
+ "ref": "e605",
17
17
  "action": "click",
18
18
  "until": {
19
19
  "kind": "allOf",
@@ -51,7 +51,7 @@ Each child predicate reports its own evidence, in order. `count: 1` is the part
51
51
  ## "Did navigating actually change the view?"
52
52
 
53
53
  ```json
54
- { "ref": "e17", "action": "click", "until": { "kind": "signal", "name": "nav:changed" } }
54
+ { "ref": "e618", "action": "click", "until": { "kind": "signal", "name": "nav:changed" } }
55
55
  ```
56
56
 
57
57
  ```json
@@ -62,9 +62,11 @@ Each child predicate reports its own evidence, in order. `count: 1` is the part
62
62
  "evidence": { "name": "nav:changed", "data": { "view": "deployments" } }
63
63
  },
64
64
  "summary": {
65
- "stateDiffs": [{ "path": "view", "from": "overview", "to": "deployments" }],
66
- "route": "/deployments"
67
- }
65
+ "statePathsChanged": ["app"],
66
+ "stateDiffs": [{ "path": "view", "from": "overview", "to": "deployments", "atMs": 49084 }],
67
+ "signals": ["nav:changed"]
68
+ },
69
+ "honesty": { "grade": "signal", "coverage": { "pct": 100, "partial": false } }
68
70
  }
69
71
  ```
70
72
 
@@ -76,8 +78,9 @@ Here is a real failure, kept because it teaches more than another pass.
76
78
 
77
79
  ```json
78
80
  {
79
- "ref": "e23",
81
+ "ref": "e623",
80
82
  "action": "click",
83
+ "args": { "confirmDangerous": true },
81
84
  "until": {
82
85
  "kind": "allOf",
83
86
  "predicates": [
@@ -98,19 +101,23 @@ Here is a real failure, kept because it teaches more than another pass.
98
101
  "pass": false,
99
102
  "expected": "an element matching {\"role\":\"dialog\"} in state 'visible'",
100
103
  "observed": "no matching element on the page",
101
- "evidence": { "presentTestids": ["deploy-table", "filter-search", "env-filter", "…"] }
104
+ "assertion": "element.present",
105
+ "evidence": {
106
+ "presentTestids": ["deploy-table", "filter-search", "env-filter", "sign-out", "…"]
107
+ }
102
108
  },
103
109
  { "pass": true, "evidence": { "absent": true } }
104
110
  ]
105
111
  },
106
112
  "summary": {
107
- "stateDiffs": [{ "path": "newDeployOpen", "from": false, "to": true }],
113
+ "stateDiffs": [{ "path": "newDeployOpen", "from": false, "to": true, "atMs": 60862 }],
108
114
  "signals": ["modal:opened"]
109
- }
115
+ },
116
+ "capsule": { "blastRadius": ["state app", "signal modal:opened"], "firstDivergence": null }
110
117
  }
111
118
  ```
112
119
 
113
- Read the `summary`. The modal **did** open. `newDeployOpen` went `false → true` and the app fired `modal:opened`.
120
+ Read the `summary`. The modal **did** open. `newDeployOpen` went `false` to `true` and the app fired `modal:opened`. `capsule.blastRadius` says the same thing as a list: the click moved the `app` store and fired `modal:opened`, and nothing else.
114
121
 
115
122
  The assertion was wrong, not the app. That modal is a `div` with no `role="dialog"`, so a role query could never match it. (Which is also an accessibility bug worth fixing, found for free.)
116
123
 
@@ -135,14 +142,16 @@ An empty `reticle_query` result tells you what _is_ there:
135
142
  "count": 0,
136
143
  "elements": [],
137
144
  "hint": {
138
- "route": "/",
145
+ "route": "/hostile",
139
146
  "knownEmptyState": true,
140
- "presentTestids": ["nav-overview", "nav-deployments", "kpi-deploys", "…"]
141
- }
147
+ "presentTestids": ["nav-overview", "nav-deployments", "hostile-view", "hostile-ticker", "…"],
148
+ "presentRegions": [{ "role": "list", "childCount": 0, "name": "hostile-feed", "sample": [] }]
149
+ },
150
+ "cost": { "bytes": 402, "tokens": 101 }
142
151
  }
143
152
  ```
144
153
 
145
- In this case the testid existed, on another route. The hint saved a snapshot and a guess.
154
+ The testid existed, on another route. `hint.route` says where you actually are and `presentTestids` says what is here instead, for 101 tokens and no snapshot. `presentRegions` is the other half: a list with `childCount: 0` is an empty state, which is a different bug from a list that failed to render at all.
146
155
 
147
156
  ## "Is this button destructive?"
148
157
 
@@ -158,7 +167,7 @@ Reticle refuses controls that look destructive, before acting:
158
167
  Confirm it explicitly when you mean it:
159
168
 
160
169
  ```json
161
- { "ref": "e23", "action": "click", "args": { "confirmDangerous": true }, "until": { … } }
170
+ { "ref": "e623", "action": "click", "args": { "confirmDangerous": true }, "until": { … } }
162
171
  ```
163
172
 
164
173
  <Note>
@@ -215,10 +224,20 @@ Then drive the flow and assert the error UI appears. Pass `clear: true` to turn
215
224
 
216
225
  <Warning>
217
226
  This one needs a **Reticle-driven browser**, because mocks are applied through CDP and the always-on
218
- SDK cannot do that. A pooled lease is **not** enough. Verified: `reticle_lease` still returns
219
- `{ "ok": false, "reason": "no-cdp-provider" }`.
227
+ SDK cannot do that. A pooled lease is **not** enough. Re-verified on 2026-08-16: calling it against a
228
+ tab from `reticle_lease` returns
220
229
 
221
- Your route is `RETICLE_CDP_URL` pointed at a Chrome started with `--remote-debugging-port`. The same applies to `reticle_screenshot`, `reticle_visual_diff` and `reticle_viewport`.
230
+ ```json
231
+ {
232
+ "applied": false,
233
+ "count": 0,
234
+ "ok": false,
235
+ "reason": "no-cdp-provider",
236
+ "recommendation": "this needs a Reticle-driven browser (it is applied through CDP, which the always-on SDK cannot do), start with `reticle drive <url>` or set RETICLE_CDP_URL"
237
+ }
238
+ ```
239
+
240
+ Under a driven browser the same call answers `{ "applied": true, "count": 1 }`. The same rule applies to `reticle_screenshot`, `reticle_visual_diff` and `reticle_viewport`, which report `no-visual-provider` instead.
222
241
 
223
242
  </Warning>
224
243
 
@@ -227,10 +246,31 @@ Your route is `RETICLE_CDP_URL` pointed at a Chrome started with `--remote-debug
227
246
  Record it once, replay it forever:
228
247
 
229
248
  ```json
230
- { "tool": "reticle_flow_verify" }
249
+ { "tool": "reticle_flow_verify", "args": { "names": ["docs-check"] } }
250
+ ```
251
+
252
+ Omit `names` to replay every saved flow. Either way you get one consolidated verdict, and only the failures carry detail:
253
+
254
+ ```json
255
+ {
256
+ "status": "fail",
257
+ "total": 1,
258
+ "passed": 0,
259
+ "failed": 1,
260
+ "summary": "0/1 flows pass, 1 need attention: docs-check",
261
+ "failures": [
262
+ {
263
+ "flow": "docs-check",
264
+ "verdict": "drift",
265
+ "whatChanged": "testid \"login-submit\" not found",
266
+ "whereInSource": "src/components/Login.tsx:81",
267
+ "nextAction": "rebind the anchor to \"sign-out\" (closest survivor), or update the flow if the change was intended."
268
+ }
269
+ ]
270
+ }
231
271
  ```
232
272
 
233
- Replays every saved flow and returns one consolidated verdict.
273
+ It also returns `flaky`, the flows it has seen both pass and fail on unchanged code. Treat that list as a bug in the flow, not in the app.
234
274
 
235
275
  <CardGroup cols={2}>
236
276
  <Card title="The predicate grammar" icon="filter" href="/predicates">
@@ -1,10 +1,12 @@
1
1
  ---
2
- title: The SKILL file: paste and go
2
+ title: 'The SKILL file: paste and go'
3
3
  sidebarTitle: SKILL file
4
4
  description: One URL you paste into any coding agent to teach it Reticle. How it works, what it contains, and when you need it at all.
5
5
  icon: graduation-cap
6
6
  ---
7
7
 
8
+ Paste `https://raw.githubusercontent.com/reticlehq/reticle/main/SKILL.md` into any coding agent and say "read this and set up Reticle". The document installs Reticle, wires it into your app, and then drives a proper verification loop, choosing setup mode or test mode itself based on whether `.reticle.json` already exists. You often do not need it: `npx @reticlehq/server init` configures Claude Code and Cursor directly.
9
+
8
10
  `SKILL.md` is a single document you hand to a coding agent. It teaches the agent to install Reticle, wire it into your app, and then drive a verification loop properly. Without you explaining any of it.
9
11
 
10
12
  ```
@@ -4,6 +4,8 @@ description: Register any store so Reticle can read what your app believes. Zust
4
4
  icon: database
5
5
  ---
6
6
 
7
+ **Reticle reads any store that has `getState()` and `subscribe()`, which covers Zustand and Redux with no adapter at all. TanStack Query, Pinia, Svelte stores, Jotai, Valtio, MobX, XState and Recoil each have a one-line adapter, and `pushStore` covers anything else.** Register one with `registerStore('app', myStore)` in your dev-only `src/reticle-dev.ts`.
8
+
7
9
  Reading application state is what separates a verdict from a screenshot. A `200` proves the server was reachable. A rendered row proves React ran. Only state proves your app accepted the change.
8
10
 
9
11
  Reticle needs one thing from your store: a way to read it, and a way to know when it changed.
@@ -4,6 +4,8 @@ description: 'How a tool call travels from a coding agent to your app and back,
4
4
  icon: map
5
5
  ---
6
6
 
7
+ A tool call crosses five processes and four hops: coding agent → `reticle mcp` proxy (stdio) → daemon on `:4400` (HTTP POST out, SSE back) → browser SDK (WebSocket) → your app. Each hop has its own failure vocabulary and its own way of lying, and the ones marked **silent** below fail with no error reaching either the agent or you.
8
+
7
9
  > How a tool call gets from a coding agent to your app and back, what each hop can do wrong, and which of those failures are **silent**. Written for anyone touching the transport, the daemon, the bridge, or a gate, and for contributors trying to work out where their change lives.
8
10
  >
9
11
  > Companion pages: [`harness-rules.md`](../apps/e2e/harness-rules.md) (what a gate must do about all this) and [`telemetry-contract.md`](./telemetry-contract.md) (the rules for anything that emits).
@@ -56,7 +58,7 @@ Three different counts, all measured, and the difference between them matters:
56
58
  | --- | --- |
57
59
  | **68** | name constants in `ReticleTool` |
58
60
  | **48** | tools advertised under `RETICLE_ADVERTISE_ALL_TOOLS=1`, i.e. what `tool-surface-sweep-test` drives (51 calls) |
59
- | **17** | advertised by default; the rest are reached through `reticle_run` |
61
+ | **19** | advertised by default; the rest are reached through `reticle_run` |
60
62
 
61
63
  68 → 48 is **family folding**: `reticle_baseline`, `reticle_session`, `reticle_record`, `reticle_flow` and `reticle_lease` each absorb their members behind an `action` parameter. So "every tool is callable" is asserted over 48 surfaces, not 68 behaviours; a family member reachable only through an `action` value the sweep never passes is not covered by it. Worth closing when the per-tool budget lands (see [`gate-plan.md`](./gate-plan.md), Phase 4).
62
64
 
@@ -103,6 +103,7 @@ The single exception is `daemon_stopped`, which is **awaited**, because the proc
103
103
  | `identified` | `reticle identify` | joins anonymous machine ids to a person who volunteered one |
104
104
  | `mcp_client_connected` | an MCP client attached | how many sessions are agent-driven at all |
105
105
  | `app_instrumented` | the first app carrying the SDK reached this daemon | **the funnel step everything turns on**; see below |
106
+ | `instrumentation_stalled` | the daemon waited and no app ever arrived | the same funnel step, seen from the failure side; see below |
106
107
  | `mcp_connection_lost` | the proxy lost its daemon | **the transport-stability metric.** The disconnect that makes a user reopen `/mcp` is invisible without it |
107
108
  | `init_completed` | `reticle init` finished | does install actually work, outside the fixtures gate |
108
109
  | `bug_found` | a defect was detected in the app under test | the value delivered, as opposed to the work done |
@@ -116,7 +117,17 @@ Nothing measured the second. `daemon_started` and `mcp_client_connected` describ
116
117
 
117
118
  `app_instrumented` fires **once per daemon run**, on the first session-ready only, so `daemon_started` → `app_instrumented` is a rate rather than an inference and a reloading page cannot inflate it. It carries `initialized` (had `init` run here), `agentAttached` (was an agent already waiting), and `msToFirstApp` (how long the daemon sat with nothing wired). It deliberately carries no stack and no framework: `project_profiled` already reports both for the same run, and the two join on `sessionId`.
118
119
 
119
- What it still cannot see is **why** an app never connected. Every cause for that is page-side (the non-localhost gate, a port mismatch, a stale build, a dev server never restarted), where the daemon has no visibility. That needs the SDK to report its own refusals, and it is the next thing to build.
120
+ ### The counterpart: `instrumentation_stalled`
121
+
122
+ `app_instrumented` fires on success and only on success, so the larger population, runs where no app ever arrives, exists in the data as an **absence**. An absence is the one thing telemetry cannot interpret: it cannot separate "nothing was ever wired" from "the process was killed before it could say so" from "we never sent the event". Since the funnel breaks at this step and nowhere else, this is the one silence worth converting into a signal.
123
+
124
+ `instrumentation_stalled` fires **once per daemon run**, only after the daemon has been up past a generous threshold, and **never once an app has connected**. A run that starts slowly and then works reports nothing at all. It carries the same three facts as its success counterpart so the two are directly comparable, with `msWaited` in place of `msToFirstApp`.
125
+
126
+ Read it against `agentAttached`, which is what makes it actionable rather than merely sad. A stalled daemon with no agent attached is somebody who installed and walked away, and is not a defect. A stalled daemon **with** an agent attached is the failure the product actually has: the tools are loaded, something is asking for them, and there is nothing on the other end to drive.
127
+
128
+ It deliberately carries no diagnosis. Whether a dev server is listening is a live probe, and running one on a timer to enrich a metric would be collecting for the metric's sake. The diagnosis belongs where a human or an agent asks for it.
129
+
130
+ What neither event can see is **why** an app never connected. Every cause for that is page-side (the non-localhost gate, a port mismatch, a stale build, a dev server never restarted), where the daemon has no visibility. That needs the SDK to report its own refusals, and it is the next thing to build.
120
131
 
121
132
  ## Sessions: `daemon_stopped` vs `session_progress`
122
133
 
@@ -124,9 +135,9 @@ Count sessions with **`daemon_stopped`** (`final: true`). It fires once, at a cl
124
135
 
125
136
  A running daemon rolls its window up every 5 minutes as **`session_progress`** (`final: false`), same payload shape. Sum work across both; count sessions with neither summed nor doubled.
126
137
 
127
- This split exists because the flush used to be emitted AS `daemon_stopped`, an event named for an exit, fired while the process was alive. One day's export: 98 `daemon_stopped` events were 73 exits + 25 flushes, so a session count was 34% high. Worse, the two populations are opposites: every one of the 25 flushes had tool calls and **not one of the 73 exits did**, because a daemon that has served a tool never idle-exits and so never reaches a clean shutdown. A funnel over the raw event describes active sessions at one end and abandoned ones at the other.
138
+ This split exists because the flush used to be emitted AS `daemon_stopped`, an event named for an exit, fired while the process was alive. Anything counting sessions over the raw event therefore over-stated, and the two populations it merged are opposites: a flush comes from a daemon that has served tool calls, while a clean exit comes from one that has not, because a daemon that has served a tool never idle-exits and so never reaches a clean shutdown. A funnel over the raw event describes active sessions at one end and abandoned ones at the other.
128
139
 
129
- The flush interval is also the **bound on what is lost**: nothing calls shutdown when a working daemon is finally killed, so its last partial window dies with it. At 30 minutes against a median 28-minute session that was most of the session. Only non-empty windows emit, so a short interval costs nothing on the daemons that never serve a tool.
140
+ The flush interval is also the **bound on what is lost**: nothing calls shutdown when a working daemon is finally killed, so its last partial window dies with it. The old 30-minute interval was long enough to lose most of a typical session that way. Only non-empty windows emit, so a short interval costs nothing on the daemons that never serve a tool.
130
141
 
131
142
  ## The session summary's newer fields
132
143
 
@@ -140,7 +151,7 @@ Four counters and one flag were added because the data could not answer question
140
151
  | `endedWithVerdict` | session summary (final only) | did this session ever produce a verdict. The headline metric, and previously the only thing in the payload that had to be COMPUTED, from lifetime counters sitting next to windowed ones, which is a subtraction that gets read wrong. Sent as `false` rather than omitted: a session that drove an app and never asked whether it worked is the finding. |
141
152
  | `verification.browser` | `verification_completed` | `headless` \| `headed` \| `attached`: who DROVE the browser. `attached` (Reticle launched nothing, the SDK connected from a browser somebody else opened) is the common case in production, so on its own this is mostly "somebody's own browser". |
142
153
  | `verification.brand` | `verification_completed` | WHICH browser it was: `chrome` \| `edge` \| `arc` \| `dia` \| `brave` \| `opera` \| `firefox` \| `safari` \| `other`, the closed `BrowserBrand` list in core. The axis `engine` cannot answer, since Chrome, Edge, Arc, Dia and Brave are all `blink`. The SDK reads `navigator.userAgentData.brands` (and the UA string on Firefox/Safari, which expose no `userAgentData`) and normalises IN THE PAGE: a raw brand or UA string is unbounded and fingerprintable and never leaves. Anything unrecognised is `other`. **Omitted rather than `"unknown"`** when the page did not say. A desktop webview has no brand and an older SDK does not report one, and a guess is indistinguishable from a measurement on a dashboard. |
143
- | `verification.reason` | `verification_completed` | WHICH clause of `decideVerified` produced the verdict, from core's closed `VerifiedReason`: `inconclusive` \| `observation_lost` \| `assertion_failed` \| `contradicted` \| `already_true` \| `unclean_capture` \| `vacuous_grade` \| `outcome_pending` \| `outcome_unread` \| `unsettled` \| `proved`. See below. |
154
+ | `verification.reason` | `verification_completed` | WHICH clause of `decideVerified` produced the verdict, from core's closed `VerifiedReason`: `inconclusive` \| `observation_lost` \| `assertion_failed` \| `contradicted` \| `already_true` \| `unclean_capture` \| `vacuous_grade` \| `outcome_pending` \| `outcome_unread` \| `unsettled` (the page never went idle and no consequence was declared) \| `evidence_incomplete` (the assertion held, but a channel's outcome had not arrived when the window closed) \| `proved`. See below. |
144
155
  | `verification.uncleanLoss` | `verification_completed` | WHAT was lost when `reason` is `unclean_capture`, from core's closed `CaptureLoss`: `buffer_loss` (our server ring buffer evicted evidence from the window) \| `transport_gap` (our browser queue overflowed) \| `blind_spot` (a boundary in the page, such as a cross-origin frame or a closed shadow root) \| `other`. Three owners, three fixes, and one bar on a dashboard until this existed. ONE value, not a list: a multi-value property is not something a breakdown can group by, so the first is sent, ours before the page's. **Absent whenever the capture was clean**, so its presence is itself the signal. Reported as `other` rather than omitted when the block says dirty and names nothing: a gap there would read as "no unclean verdicts happened". |
145
156
  | `bug.attribution` | `bug_found` | `app` \| `request` \| `reticle`: whose fault the defect was. **Absent means unclassified**, never `app`. See below. |
146
157
  | `outage.stage` / `outage.reason` / `outage.attempts` | `mcp_connection_lost` | which stage of the outage, why the stream went away (closed `OutageReason`, `other` for anything unnamed), and how many reconnects had been tried. See below. |
@@ -184,7 +195,7 @@ So `plugin` is the only route detectable without anybody typing anything (the pl
184
195
 
185
196
  Captured against the real classifier: `verified: 'unknown'` covered "the agent malformed the call", "the consequence was already true", "the app answered 202", "a 2xx body went unread", "the capture was not clean", "nothing was asserted at a real grade" and "the page never settled": **seven causes, two wire payloads**. They belong to three different owners (the agent, the app, Reticle) and need opposite responses: teach the agent, wait and re-check, or ship a fix. On a dashboard they were one bar. `verified: 'no'` collapsed the same way: "channels disagree" (Reticle earning its keep) and "the agent's predicate failed" were the same string.
186
197
 
187
- `VerifiedReason` lives in `@reticlehq/core` and is the **single list**. `decideVerified` returns a member from every clause, so a new clause cannot compile without one; `verified.test.ts` drives all ten and fails if a member exists that no clause produces. `verification-of.ts` narrows the result field against `Object.values(VerifiedReason)`, and anything else is dropped rather than forwarded, because a string nobody can group by is worse than a gap. **Nothing re-lists these**, including the battery spec, which imports the enum from core's build.
198
+ `VerifiedReason` lives in `@reticlehq/core` and is the **single list**. `decideVerified` returns a member from every clause, so a new clause cannot compile without one; `verified.test.ts` drives every member and fails if a member exists that no clause produces. `verification-of.ts` narrows the result field against `Object.values(VerifiedReason)`, and anything else is dropped rather than forwarded, because a string nobody can group by is worse than a gap. **Nothing re-lists these**, including the battery spec, which imports the enum from core's build.
188
199
 
189
200
  Optional on purpose: a suite verdict (`flow_verify`) is a pass/fail with no clause behind it, and an older sender has none. Absent means unclassified.
190
201