@reticlehq/server 2.5.0 → 2.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (182) hide show
  1. package/SKILL.md +226 -26
  2. package/dist/bridge/bridge.d.ts +18 -1
  3. package/dist/bridge/bridge.js +59 -6
  4. package/dist/bridge/bridge.test-harness.d.ts +8 -0
  5. package/dist/bridge/bridge.test-harness.js +9 -1
  6. package/dist/cli/chromium-hint.d.ts +40 -0
  7. package/dist/cli/chromium-hint.js +75 -0
  8. package/dist/cli/cli-doctor.d.ts +0 -5
  9. package/dist/cli/cli-doctor.js +74 -14
  10. package/dist/cli/cli-launch.d.ts +1 -0
  11. package/dist/cli/cli-launch.js +4 -1
  12. package/dist/cli/cli-parse-feedback.d.ts +31 -0
  13. package/dist/cli/cli-parse-feedback.js +56 -0
  14. package/dist/cli/cli-parse.d.ts +5 -9
  15. package/dist/cli/cli-parse.js +7 -23
  16. package/dist/cli/cli-port.js +20 -1
  17. package/dist/cli/doctor-daemon-line.d.ts +37 -0
  18. package/dist/cli/doctor-daemon-line.js +44 -0
  19. package/dist/cli/port-holder.d.ts +43 -0
  20. package/dist/cli/port-holder.js +99 -0
  21. package/dist/cli.js +170 -19
  22. package/dist/daemon/daemon-resilience.d.ts +24 -0
  23. package/dist/daemon/daemon-resilience.js +105 -17
  24. package/dist/daemon/daemon.js +26 -1
  25. package/dist/daemon/heartbeat.d.ts +115 -0
  26. package/dist/daemon/heartbeat.js +141 -0
  27. package/dist/daemon/idle-shutdown.d.ts +13 -5
  28. package/dist/daemon/idle-shutdown.js +14 -6
  29. package/dist/daemon/loopback-alias.d.ts +32 -0
  30. package/dist/daemon/loopback-alias.js +80 -0
  31. package/dist/daemon/port-presence.d.ts +76 -0
  32. package/dist/daemon/port-presence.js +93 -0
  33. package/dist/daemon/state-dir.d.ts +7 -0
  34. package/dist/daemon/state-dir.js +59 -0
  35. package/dist/daemon/unclean-predecessor.d.ts +23 -0
  36. package/dist/daemon/unclean-predecessor.js +27 -0
  37. package/dist/events/predicate-eval.d.ts +17 -3
  38. package/dist/events/predicate-eval.js +183 -96
  39. package/dist/events/predicate-parse.d.ts +23 -0
  40. package/dist/events/predicate-parse.js +68 -0
  41. package/dist/events/predicate.d.ts +5 -0
  42. package/dist/events/predicate.js +17 -3
  43. package/dist/events/ring-buffer.d.ts +8 -0
  44. package/dist/events/ring-buffer.js +35 -17
  45. package/dist/flows/flow-replay.d.ts +1 -1
  46. package/dist/flows/flow-replay.js +19 -2
  47. package/dist/flows/flow-step-runners.js +2 -3
  48. package/dist/flows/flow-tools.js +10 -14
  49. package/dist/flows/role-drift-reason.d.ts +20 -0
  50. package/dist/flows/role-drift-reason.js +39 -0
  51. package/dist/flows/suite-flakes.d.ts +33 -0
  52. package/dist/flows/suite-flakes.js +32 -0
  53. package/dist/honesty/blind-spots.d.ts +19 -0
  54. package/dist/honesty/blind-spots.js +33 -0
  55. package/dist/honesty/honesty.d.ts +11 -0
  56. package/dist/honesty/honesty.js +9 -1
  57. package/dist/honesty/verified.d.ts +10 -1
  58. package/dist/honesty/verified.js +42 -3
  59. package/dist/http-server.js +20 -1
  60. package/dist/index.d.ts +1 -0
  61. package/dist/index.js +36 -3
  62. package/dist/init/agent-rules.d.ts +18 -0
  63. package/dist/init/agent-rules.js +28 -5
  64. package/dist/init/cra.d.ts +13 -0
  65. package/dist/init/cra.js +16 -0
  66. package/dist/init/detect.d.ts +14 -0
  67. package/dist/init/detect.js +23 -0
  68. package/dist/init/mcp-clients.d.ts +81 -0
  69. package/dist/init/mcp-clients.js +248 -0
  70. package/dist/init/node-io.js +8 -2
  71. package/dist/init/non-js-project.d.ts +27 -0
  72. package/dist/init/non-js-project.js +60 -0
  73. package/dist/init/plan-framework.d.ts +9 -0
  74. package/dist/init/plan-framework.js +55 -3
  75. package/dist/init/plan.d.ts +14 -0
  76. package/dist/init/plan.js +93 -12
  77. package/dist/init/run.d.ts +18 -1
  78. package/dist/init/run.js +169 -23
  79. package/dist/init/snippets.d.ts +20 -1
  80. package/dist/init/snippets.js +71 -6
  81. package/dist/init/windows-quote.d.ts +38 -0
  82. package/dist/init/windows-quote.js +70 -0
  83. package/dist/mcp/mcp-outage.d.ts +1 -0
  84. package/dist/mcp/mcp-outage.js +8 -1
  85. package/dist/mcp/mcp-proxy.d.ts +11 -1
  86. package/dist/mcp/mcp-proxy.js +39 -9
  87. package/dist/mcp/mcp.js +64 -2
  88. package/dist/mcp/proxy-lifecycle.d.ts +19 -0
  89. package/dist/mcp/proxy-lifecycle.js +21 -0
  90. package/dist/pool/browser-pool.d.ts +9 -0
  91. package/dist/pool/browser-pool.js +38 -9
  92. package/dist/pool/playwright-launcher.js +10 -2
  93. package/dist/session/attachment-history.d.ts +46 -0
  94. package/dist/session/attachment-history.js +65 -0
  95. package/dist/session/live-control-tools.js +39 -2
  96. package/dist/session/live-control.d.ts +7 -9
  97. package/dist/session/live-control.js +34 -1
  98. package/dist/session/no-session-diagnosis.d.ts +11 -4
  99. package/dist/session/no-session-diagnosis.js +95 -14
  100. package/dist/session/no-session-watch.d.ts +3 -1
  101. package/dist/session/no-session-watch.js +10 -2
  102. package/dist/session/pending-commands.js +1 -0
  103. package/dist/session/review-store.d.ts +19 -0
  104. package/dist/session/review-store.js +34 -5
  105. package/dist/session/session-info.d.ts +17 -1
  106. package/dist/session/session-info.js +5 -1
  107. package/dist/session/session-manager.d.ts +12 -0
  108. package/dist/session/session-manager.js +83 -3
  109. package/dist/session/session.d.ts +9 -0
  110. package/dist/session/session.js +20 -0
  111. package/dist/status-payload.d.ts +13 -1
  112. package/dist/status-payload.js +6 -1
  113. package/dist/telemetry/app-instrumented.d.ts +37 -0
  114. package/dist/telemetry/app-instrumented.js +54 -0
  115. package/dist/telemetry/crash-cause.d.ts +47 -0
  116. package/dist/telemetry/crash-cause.js +104 -0
  117. package/dist/telemetry/daemon-telemetry.d.ts +25 -7
  118. package/dist/telemetry/daemon-telemetry.js +32 -13
  119. package/dist/telemetry/error-class.d.ts +26 -0
  120. package/dist/telemetry/error-class.js +52 -0
  121. package/dist/telemetry/feedback-cli.d.ts +7 -2
  122. package/dist/telemetry/feedback-cli.js +49 -5
  123. package/dist/telemetry/feedback-context.d.ts +1 -6
  124. package/dist/telemetry/feedback-context.js +64 -4
  125. package/dist/telemetry/git-facts.d.ts +7 -0
  126. package/dist/telemetry/git-facts.js +4 -4
  127. package/dist/telemetry/project-profile.js +2 -1
  128. package/dist/telemetry/session-metrics.d.ts +45 -3
  129. package/dist/telemetry/session-metrics.js +247 -9
  130. package/dist/telemetry/telemetry.d.ts +21 -1
  131. package/dist/telemetry/telemetry.js +34 -9
  132. package/dist/telemetry/verification-of.js +26 -1
  133. package/dist/tools/act-tools.js +63 -14
  134. package/dist/tools/assert-verdict.d.ts +7 -1
  135. package/dist/tools/assert-verdict.js +22 -3
  136. package/dist/tools/browser-tools.js +8 -2
  137. package/dist/tools/contract-tools.d.ts +0 -5
  138. package/dist/tools/contract-tools.js +26 -0
  139. package/dist/tools/dynamic-tools.js +6 -1
  140. package/dist/tools/error-recovery.d.ts +14 -0
  141. package/dist/tools/error-recovery.js +89 -8
  142. package/dist/tools/feedback-invite.d.ts +48 -0
  143. package/dist/tools/feedback-invite.js +71 -0
  144. package/dist/tools/invoke-tool.d.ts +0 -6
  145. package/dist/tools/invoke-tool.js +76 -7
  146. package/dist/tools/navigate-arrival.d.ts +26 -0
  147. package/dist/tools/navigate-arrival.js +60 -0
  148. package/dist/tools/navigate-result.d.ts +15 -1
  149. package/dist/tools/navigate-result.js +17 -10
  150. package/dist/tools/observe-tools.js +22 -4
  151. package/dist/tools/real-input-attempt.js +10 -5
  152. package/dist/tools/tool-kit.d.ts +60 -5
  153. package/dist/tools/tool-kit.js +55 -24
  154. package/dist/tools/tool-surface.js +13 -0
  155. package/dist/tools/tools.js +22 -0
  156. package/dist/visual/capture-cleanup.d.ts +16 -0
  157. package/dist/visual/capture-cleanup.js +39 -0
  158. package/dist/visual/visual-tools.d.ts +18 -0
  159. package/dist/visual/visual-tools.js +24 -5
  160. package/docs/README.md +55 -0
  161. package/docs/agent-cheatsheet.md +15 -3
  162. package/docs/benchmarks.md +1 -1
  163. package/docs/fixtures-dispatch-receiver.yml +76 -0
  164. package/docs/fixtures.md +17 -0
  165. package/docs/gate-plan.md +98 -0
  166. package/docs/gates.md +117 -0
  167. package/docs/local-registry.md +1 -1
  168. package/docs/matrix/2.5.0/claude-code-machine.json +19 -0
  169. package/docs/matrix/2.5.0/codex-machine.json +20 -0
  170. package/docs/matrix/2.5.0/cursor-machine.json +22 -0
  171. package/docs/matrix/2.5.0/gemini-machine.json +22 -0
  172. package/docs/matrix/2.5.0/opencode-machine.json +22 -0
  173. package/docs/matrix/2.5.0/vscode-machine.json +22 -0
  174. package/docs/matrix/2.5.0/windsurf-machine.json +22 -0
  175. package/docs/matrix/MATRIX.md +26 -0
  176. package/docs/matrix/README.md +59 -0
  177. package/docs/mint.json +1 -1
  178. package/docs/system-map.md +152 -0
  179. package/docs/telemetry-contract.md +15 -3
  180. package/docs/telemetry.md +16 -1
  181. package/docs/token-efficiency.md +2 -2
  182. package/package.json +2 -2
package/docs/gates.md ADDED
@@ -0,0 +1,117 @@
1
+ # Gates — what to run before you push
2
+
3
+ > **One question this file answers:** _I changed some files. Which command do I run?_
4
+ >
5
+ > The _why_ behind the gate design (tiers, the merge-gate/release-gate split, what is still unbuilt) lives in [`gate-plan.md`](./gate-plan.md). This file is the routing table.
6
+
7
+ ---
8
+
9
+ ## 0. Last verified
10
+
11
+ Every gate below was executed end to end against `main` on **2026-08-12** (macOS, M-series, `v2.6.0`). A green row means somebody watched it go green, not that it is supposed to be green.
12
+
13
+ | Gate | Result | Wall clock |
14
+ | --- | --- | --- |
15
+ | `pnpm build` | ✅ | ~40s |
16
+ | `pnpm lint` (+ both guard self-tests) | ✅ | ~15s |
17
+ | `pnpm typecheck` | ✅ | ~10s |
18
+ | `pnpm test:unit` | ✅ 4,315 tests / 461 files across 8 packages | ~45s |
19
+ | `pnpm format:check` | ✅ | ~10s |
20
+ | `pnpm test:integration` | ✅ 12/12 | 17s |
21
+ | `pnpm test:e2e` | ✅ 32/32 specs + soak 60/60 answered | **490s** |
22
+ | `pnpm test:e2e:desktop` | ✅ 2/2 (Electron 20, Tauri 14) | 58s |
23
+ | `node apps/e2e/soak.mjs --self-check` | ✅ | <1s |
24
+ | `node apps/e2e/matrix.mjs --self-check` | ✅ | <1s |
25
+ | `pnpm matrix:compat --only cursor` | ✅ 4/4 | ~10s |
26
+ | `pnpm bench` | ✅ 10/10 — **was failing before this sweep** (see below) | 279s |
27
+
28
+ **`pnpm bench` was broken and nobody knew.** `suite-rre.mjs` recorded four flows that asserted no observable consequence and then demanded a `pass` verdict from `reticle_flow_verify` — which correctly grades an assertion-free suite `unverifiable`. The product got more honest about false greens; the benchmark measuring it did not follow, so the whole run aborted at script 9 of 10 and `replay-determinism` never ran at all. Each flow now carries a success oracle. This is the failure mode `bench/` is most exposed to: **nothing in CI runs it**, so it can only rot silently.
29
+
30
+ `pnpm gate:install` (~15 min) and the Windows / Rust jobs were **not** run in this sweep — they are CI-only or network-bound. They are green on `main` per the last CI run, which is a weaker claim than every row above, and is stated that way on purpose.
31
+
32
+ ---
33
+
34
+ ## 1. The routing table
35
+
36
+ Find the row that matches what you changed. Run its commands. That is the whole rule.
37
+
38
+ | You changed | Run | Cost |
39
+ | --- | --- | --- |
40
+ | **Anything at all** | `pnpm lint && pnpm typecheck && pnpm test:unit` | ~2 min |
41
+ | The tool surface, the wire contract (`packages/core`), or an observer | ↑ **and** `pnpm test:e2e` | +~8 min |
42
+ | `reticle init`, `@reticlehq/vite-plugin`, `@reticlehq/next`, `@reticlehq/babel-plugin` — anything a user runs before their first session | ↑ **and** `pnpm gate:install` | +~15 min |
43
+ | `@reticlehq/electron`, `packages/tauri`, the IPC observer, desktop capture | ↑ **and** `pnpm test:e2e:desktop` | +~3 min |
44
+ | Telemetry, feedback, or anything that emits an event | ↑ **and** read [`telemetry-contract.md`](./telemetry-contract.md) first. `pnpm test:e2e` covers it (`telemetry-events-test`) | — |
45
+ | `packages/tauri` (Rust) | ↑ **and** `cd packages/tauri && cargo fmt --check && cargo clippy --all-targets -- -D warnings` | +~2 min |
46
+ | Docs, README, comments only | `pnpm format:check` | seconds |
47
+
48
+ **Why routing exists.** The full set is roughly 35 minutes. A gate people resent is a gate people route around, so only the tier that can see your change is worth your time. CI runs everything regardless — routing costs you a slower red, never a missed one.
49
+
50
+ ---
51
+
52
+ ## 2. Every gate, and what each one can actually see
53
+
54
+ Each gate exists because the ones above it are blind to something. That blindness is the column that matters.
55
+
56
+ | Gate | Command | Proves | Blind to | CI job |
57
+ | --- | --- | --- | --- | --- |
58
+ | **Build** | `pnpm build` | every package compiles and emits | anything at runtime | `verify` |
59
+ | **Lint** | `pnpm lint` | style rules, plus the dependency-boundary and lossy-transform guards | anything not expressible as a rule | `verify` |
60
+ | **Typecheck** | `pnpm typecheck` | types agree across package boundaries | runtime behaviour | `verify` |
61
+ | **Unit** | `pnpm test:unit` | 4,315 tests, per-package, no browser | anything crossing a package boundary at runtime | `verify` |
62
+ | **Format** | `pnpm format:check` | Prettier | — | `verify` |
63
+ | **Integration** | `pnpm test:integration` | real headless Chromium: browser pool, crash isolation, framework adapters, `withReticle` | the MCP surface, the daemon | `e2e` |
64
+ | **Web e2e battery** | `pnpm test:e2e` | 30 specs against 3 booted servers and a real browser — the tool surface, the daemon lifecycle, transport faults, telemetry, trace shape — plus the soak | desktop runtimes; the install | `e2e` |
65
+ | **Desktop battery** | `pnpm test:e2e:desktop` | a real Electron main process and a **packaged** Tauri binary, driven headless | web-only paths | `desktop-e2e` |
66
+ | **Install gate** | `pnpm gate:install` | scaffolds 3 pristine apps, publishes this checkout to a local Verdaccio, lets `init` install itself, boots each app in a real browser, polls for a session | install _complexity_ — see [`fixtures.md`](./fixtures.md) | `install-gate` |
67
+ | **Matrix records** | `pnpm matrix:validate` | every submitted client-compat record is well-formed | whether the client actually works | `matrix-records` |
68
+ | **Windows** | (CI only) | that the code runs at all on the majority platform | e2e; Windows is unit-only | `windows` |
69
+ | **Rust** | `cargo fmt/clippy/check` | `packages/tauri` compiles and lints on Linux, macOS, and cross-checks Windows | everything JS | `rust`, `rust-macos` |
70
+
71
+ **The single required status check is `gate`.** It passes when every job above either succeeded or was deliberately skipped by path routing, and fails on anything else. Adding a job to `ci.yml` is half the work; adding it to `gate`'s `needs:` list is the other half — a job missing from that list runs, reports, and is structurally incapable of blocking a merge.
72
+
73
+ ### Guards that self-test
74
+
75
+ Four checks prove they can still fail before they are trusted. A guard that has never refused anything is not a guard, so each has a negative control that CI runs **first**:
76
+
77
+ ```bash
78
+ node scripts/check-boundaries.mjs --self-test # catches a synthetic bad dependency graph
79
+ node scripts/check-lossy-transforms.mjs --self-test # catches an unclassified lossy export
80
+ node apps/e2e/soak.mjs --self-check # the soak gate still reddens on an unanswered call
81
+ node apps/e2e/matrix.mjs --self-check # the validator still refuses a malformed record
82
+ pnpm gate:install:self-test # mis-wires every scaffold; the gate MUST go red
83
+ ```
84
+
85
+ ---
86
+
87
+ ## 3. Gates that are not automatic
88
+
89
+ These are real and they work; they are not on the PR path, so they only run when somebody runs them.
90
+
91
+ | Command | What it is | When |
92
+ | --- | --- | --- |
93
+ | `pnpm gate:soak:record` | the half-hour release soak (20 rounds, 2s idle), re-records `bench/soak-history.jsonl` and `bench/TOOL-PROFILE.md` | before a release |
94
+ | `pnpm matrix:compat` | drives each MCP client's config exactly the way that client would; writes a machine record for `docs/matrix/` | before a release, per client |
95
+ | `pnpm knip` | unused files, exports, and dependencies | when the repo feels heavy |
96
+ | `node scripts/verify.mjs` in the sibling **`reticle-fixtures`** repo | installs this commit into nine real third-party apps (install _complexity_, not regressions) | see [`fixtures.md`](./fixtures.md) |
97
+
98
+ ---
99
+
100
+ ## 4. `bench/` is not a gate
101
+
102
+ `bench/` is **measurement and research**, not a merge check. Nothing there blocks a PR, nothing runs in CI, and it is allowed to bit-rot in a way a gate is not. Read [`bench/README.md`](../bench/README.md) before touching it — it says which scripts are live and which are one-off studies kept as evidence for a published claim.
103
+
104
+ The one exception worth knowing: `pnpm bench` + `pnpm bench:gate` is a working regression gate for the replay numbers, and it is run by hand before a release.
105
+
106
+ ---
107
+
108
+ ## 5. When a gate fails and you think it is the gate's fault
109
+
110
+ Sometimes it is. The specific failures worth recognising:
111
+
112
+ - **`EADDRINUSE` / "died during boot"** — a previous run left something on `:8787`, `:4310`, or `:3100`. `run-ci.sh` frees these on exit; if it was killed, free them by hand.
113
+ - **Killing port 4400 with `lsof -ti tcp:4400 | xargs kill -9`** — this SIGKILLs the `reticle mcp` proxy too, because the proxy holds a _client_ socket on the bridge port. Always add `-sTCP:LISTEN`. This is the root cause of most "the MCP went down" reports.
114
+ - **A timing assertion** — if a test asserts `Date.now() - t < N`, that is a bug in the test, not a flake to re-run. Assert the bound (output size, a truncation flag), or use a generous per-test timeout. See [`harness-rules.md`](../apps/e2e/harness-rules.md).
115
+ - **An `INCONCLUSIVE` verdict** — the harness is telling you the transport did not stay up, so it is claiming nothing about the product. That is the harness working, not the product failing.
116
+
117
+ The four rules every tier obeys, and the incident behind each, are in [`apps/e2e/harness-rules.md`](../apps/e2e/harness-rules.md).
@@ -34,7 +34,7 @@ For a browser app, install `@reticlehq/react` plus the build plugin for your fra
34
34
  In your app's project root, add an `.npmrc` (scopes only `@reticle` to the local registry; everything else still comes from npm):
35
35
 
36
36
  ```ini
37
- @reticle:registry=http://localhost:4873/
37
+ @reticlehq:registry=http://localhost:4873/
38
38
  ```
39
39
 
40
40
  ## 3. Install + wire it up
@@ -0,0 +1,19 @@
1
+ {
2
+ "reticle": {
3
+ "version": "2.5.0",
4
+ "commit": "33629413"
5
+ },
6
+ "client": {
7
+ "id": "claude-code"
8
+ },
9
+ "host": {
10
+ "os": "darwin 25.5.0",
11
+ "node": "v22.14.0"
12
+ },
13
+ "checks": {
14
+ "snippet": true
15
+ },
16
+ "verdict": "runnable-unverified",
17
+ "by": "machine (client-compat.mjs)",
18
+ "note": "machine-checkable half only — nobody has run this client"
19
+ }
@@ -0,0 +1,20 @@
1
+ {
2
+ "reticle": {
3
+ "version": "2.5.0",
4
+ "commit": "33629413"
5
+ },
6
+ "client": {
7
+ "id": "codex"
8
+ },
9
+ "host": {
10
+ "os": "darwin 25.5.0",
11
+ "node": "v22.14.0"
12
+ },
13
+ "checks": {
14
+ "writes": false,
15
+ "snippet": true
16
+ },
17
+ "verdict": "manual-snippet",
18
+ "by": "machine (client-compat.mjs)",
19
+ "note": "machine-checkable half only — nobody has run this client"
20
+ }
@@ -0,0 +1,22 @@
1
+ {
2
+ "reticle": {
3
+ "version": "2.5.0",
4
+ "commit": "33629413"
5
+ },
6
+ "client": {
7
+ "id": "cursor"
8
+ },
9
+ "host": {
10
+ "os": "darwin 25.5.0",
11
+ "node": "v22.14.0"
12
+ },
13
+ "checks": {
14
+ "writes": true,
15
+ "parses": true,
16
+ "initialize": true,
17
+ "tools": 17
18
+ },
19
+ "verdict": "runnable-unverified",
20
+ "by": "machine (client-compat.mjs)",
21
+ "note": "machine-checkable half only — nobody has run this client"
22
+ }
@@ -0,0 +1,22 @@
1
+ {
2
+ "reticle": {
3
+ "version": "2.5.0",
4
+ "commit": "33629413"
5
+ },
6
+ "client": {
7
+ "id": "gemini"
8
+ },
9
+ "host": {
10
+ "os": "darwin 25.5.0",
11
+ "node": "v22.14.0"
12
+ },
13
+ "checks": {
14
+ "writes": true,
15
+ "parses": true,
16
+ "initialize": true,
17
+ "tools": 17
18
+ },
19
+ "verdict": "runnable-unverified",
20
+ "by": "machine (client-compat.mjs)",
21
+ "note": "machine-checkable half only — nobody has run this client"
22
+ }
@@ -0,0 +1,22 @@
1
+ {
2
+ "reticle": {
3
+ "version": "2.5.0",
4
+ "commit": "33629413"
5
+ },
6
+ "client": {
7
+ "id": "opencode"
8
+ },
9
+ "host": {
10
+ "os": "darwin 25.5.0",
11
+ "node": "v22.14.0"
12
+ },
13
+ "checks": {
14
+ "writes": true,
15
+ "parses": true,
16
+ "initialize": true,
17
+ "tools": 17
18
+ },
19
+ "verdict": "runnable-unverified",
20
+ "by": "machine (client-compat.mjs)",
21
+ "note": "machine-checkable half only — nobody has run this client"
22
+ }
@@ -0,0 +1,22 @@
1
+ {
2
+ "reticle": {
3
+ "version": "2.5.0",
4
+ "commit": "33629413"
5
+ },
6
+ "client": {
7
+ "id": "vscode"
8
+ },
9
+ "host": {
10
+ "os": "darwin 25.5.0",
11
+ "node": "v22.14.0"
12
+ },
13
+ "checks": {
14
+ "writes": true,
15
+ "parses": true,
16
+ "initialize": true,
17
+ "tools": 17
18
+ },
19
+ "verdict": "runnable-unverified",
20
+ "by": "machine (client-compat.mjs)",
21
+ "note": "machine-checkable half only — nobody has run this client"
22
+ }
@@ -0,0 +1,22 @@
1
+ {
2
+ "reticle": {
3
+ "version": "2.5.0",
4
+ "commit": "33629413"
5
+ },
6
+ "client": {
7
+ "id": "windsurf"
8
+ },
9
+ "host": {
10
+ "os": "darwin 25.5.0",
11
+ "node": "v22.14.0"
12
+ },
13
+ "checks": {
14
+ "writes": true,
15
+ "parses": true,
16
+ "initialize": true,
17
+ "tools": 17
18
+ },
19
+ "verdict": "runnable-unverified",
20
+ "by": "machine (client-compat.mjs)",
21
+ "note": "machine-checkable half only — nobody has run this client"
22
+ }
@@ -0,0 +1,26 @@
1
+ # Client matrix
2
+
3
+ > Which MCP clients Reticle is known to work in, and on whose machine that was measured.
4
+ > Generated by `node apps/e2e/matrix.mjs` — do not edit by hand.
5
+
6
+ | | meaning |
7
+ | --- | --- |
8
+ | ✅ | a human ran the real client and the tools were usable |
9
+ | ◐ | `init` wrote a runnable entry where the client documents it — **nobody has run that client** |
10
+ | ✎ | `init` will not write this format (TOML); the user pastes a block |
11
+ | ❌ | somebody ran it and it did not work — the most useful row here |
12
+
13
+ **◐ is not a tick.** It means the half a machine can check passed. Only ✅ means somebody saw
14
+ Reticle working in that client.
15
+
16
+ ## 2.5.0
17
+
18
+ | client | verdict | tools | os | submitted by |
19
+ | --- | --- | --- | --- | --- |
20
+ | claude-code | ◐ runnable-unverified | — | darwin 25.5.0 | machine (client-compat.mjs) |
21
+ | codex | ✎ manual-snippet | — | darwin 25.5.0 | machine (client-compat.mjs) |
22
+ | cursor | ◐ runnable-unverified | — | darwin 25.5.0 | machine (client-compat.mjs) |
23
+ | gemini | ◐ runnable-unverified | — | darwin 25.5.0 | machine (client-compat.mjs) |
24
+ | opencode | ◐ runnable-unverified | — | darwin 25.5.0 | machine (client-compat.mjs) |
25
+ | vscode | ◐ runnable-unverified | — | darwin 25.5.0 | machine (client-compat.mjs) |
26
+ | windsurf | ◐ runnable-unverified | — | darwin 25.5.0 | machine (client-compat.mjs) |
@@ -0,0 +1,59 @@
1
+ # Submitting a client-matrix record
2
+
3
+ [`MATRIX.md`](./MATRIX.md) says which MCP clients Reticle is known to work in. Right now most rows are **◐** — `init` writes a runnable entry where that client documents it, and **nobody has run that client**. Turning a ◐ into a ✅ takes about three minutes and is the single most useful thing an outside contributor can do for this project.
4
+
5
+ You do not tell us it works. You **run one command, run your client, and submit what came out** — the same shape as [CNCF's Kubernetes conformance](https://github.com/cncf/k8s-conformance), and for the same reason: a claim nobody can trace back to a machine and a commit cannot gate anything.
6
+
7
+ ## 1. The machine half (no client needed)
8
+
9
+ ```bash
10
+ pnpm build
11
+ node apps/e2e/client-compat.mjs --only <client> --json /tmp/compat.json
12
+ ```
13
+
14
+ That checks `init` writes a config where your client documents it, that it parses under **that client's own key**, and that the command inside it starts and advertises tools. It does **not** check that your client reads it — which is the whole reason step 2 exists.
15
+
16
+ ## 2. The human half (this is the part that matters)
17
+
18
+ 1. Run `npx @reticlehq/server init` in a real project.
19
+ 2. **Restart your client.** Every one of them reads MCP config at startup; this is the step people skip and then report a bug about.
20
+ 3. Ask it to list Reticle's tools. Note **how many** it lists.
21
+ 4. Kill the daemon (`lsof -nP -iTCP:4400 -sTCP:LISTEN -t | xargs kill`) and use a Reticle tool again. Does the client recover on its own, or does it need a manual reconnect?
22
+ 5. If anything failed, copy your client's error text **verbatim**. That sentence is worth more than the rest of the record put together — it is the only thing that says what your client actually thinks went wrong.
23
+
24
+ ## 3. Submit it
25
+
26
+ Write `docs/matrix/<reticle-version>/<client>-<yourhandle>.json`:
27
+
28
+ ```json
29
+ {
30
+ "reticle": { "version": "2.5.0", "commit": "abc1234" },
31
+ "client": { "id": "windsurf", "version": "1.2.3" },
32
+ "host": { "os": "darwin 25.5.0", "node": "v22.14.0" },
33
+ "checks": {
34
+ "registered": true,
35
+ "toolsVisible": 17,
36
+ "survivedDaemonRestart": true,
37
+ "clientError": ""
38
+ },
39
+ "verdict": "works",
40
+ "by": "your-github-handle"
41
+ }
42
+ ```
43
+
44
+ Validate it, regenerate the table, open a PR:
45
+
46
+ ```bash
47
+ node apps/e2e/matrix.mjs --validate docs/matrix/<version>/<file>.json
48
+ node apps/e2e/matrix.mjs # regenerates MATRIX.md
49
+ ```
50
+
51
+ CI runs the same validator on every PR, so a malformed record fails before a human reads it.
52
+
53
+ ## What the validator insists on, and why
54
+
55
+ - **`works` requires `checks.toolsVisible > 0`.** "It works" with no number is the self-report this whole flow exists to replace.
56
+ - **`broken` requires `checks.clientError`** — your client's own words, verbatim. A failure nobody can act on is worse than no record.
57
+ - **Every record needs a host and a version.** A cell nobody can place is decoration.
58
+
59
+ **A `broken` record is as welcome as a `works` one and passes exactly the same checks.** It is usually more useful: it is how we find out that a client changed its config path, or that our entry shape stopped being read. If your client does not work, that is the record we most want.
package/docs/mint.json CHANGED
@@ -48,7 +48,7 @@
48
48
  },
49
49
  {
50
50
  "group": "Reference",
51
- "pages": ["usage", "agent-cheatsheet"]
51
+ "pages": ["usage", "agent-cheatsheet", "telemetry"]
52
52
  },
53
53
  {
54
54
  "group": "Enterprise",
@@ -0,0 +1,152 @@
1
+ # System map
2
+
3
+ > 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.
4
+ >
5
+ > 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).
6
+
7
+ ## 1. Topology — 5 processes, 4 hops
8
+
9
+ ```
10
+ coding agent ──stdio──▶ reticle mcp ──HTTP POST──▶ daemon :4400 ──WebSocket──▶ browser SDK ──▶ your app
11
+ (client) JSON-RPC (proxy) ◀───SSE───── (bridge+tools) ◀───────── (@reticlehq/browser)
12
+ │
13
+ └─▶ .reticle/ flows · capsules · baselines · runs
14
+ ```
15
+
16
+ Each hop has its own failure vocabulary, its own recovery, and its own way of lying. Nothing stated this end to end until this page, which is why gate results have been contradictory.
17
+
18
+ ## 2. Connection establishment
19
+
20
+ | # | Step | Owner | Fails how |
21
+ | --- | --- | --- | --- |
22
+ | 1 | client spawns `reticle mcp` over stdio | client | the client's own startup timeout; **no host respawns a dead stdio server** |
23
+ | 2 | proxy probes `:4400` (`probeDaemon`, plain TCP, 500ms) | proxy | a squatter that accepts and never serves looks alive |
24
+ | 3 | absent → spawn daemon detached, poll to `DAEMON_READY_TIMEOUT_MS` (10s) | proxy | slow CI; raise with `RETICLE_DAEMON_READY_TIMEOUT_MS` |
25
+ | 4 | proxy opens SSE, waits for the `endpoint` frame → POST URL | proxy | an empty frame used to read as "connected"; now refused (`buildSessionUrl`) |
26
+ | 5 | `initialize` — answered **locally** after `LOCAL_HANDSHAKE_MS` (12s) when the daemon cannot | proxy | any client with a startup budget under ~12s loses Reticle for the whole session |
27
+ | 6 | daemon binds `:4400`, writes the pid file, starts bridge + idle watcher | daemon | `EADDRINUSE` → child exits 1, **parent already exited 0** |
28
+ | 7 | app calls `reticle.connect()` → `ws://localhost:4400/reticle` | SDK | wrong port; missing pairing token; mixed content (Safari throws synchronously on `new WebSocket`) |
29
+ | 8 | HELLO: `sessionId`, `projectId`, `protocolVersion`, capabilities | SDK → bridge | version mismatch → `1008` close → **the SDK stops retrying, permanently** |
30
+ | 9 | bridge authenticates: pairing token / loopback tier / Origin allow-list | bridge | another project's daemon answers → `authFailureReason` names it |
31
+ | 10 | Session registered, `everConnected = true` | daemon | — |
32
+ | 11 | tool call → `runTool` → `session.send` → WS command → SDK → reply | all | any hop above, mid-flight |
33
+
34
+ **Sequencing hazard:** steps 6 and 7 race. The product absorbs it internally — the first live tool call blocks briefly for a session to appear rather than failing — but a gate that boots an app and immediately asks whether a session exists is outside that protection and has been burned by it. Poll for the session; never sample once.
35
+
36
+ ## 3. The tool graph — what produces what
37
+
38
+ Three different counts, all measured, and the difference between them matters:
39
+
40
+ | Count | What it is |
41
+ | --- | --- |
42
+ | **68** | name constants in `ReticleTool` |
43
+ | **48** | tools advertised under `RETICLE_ADVERTISE_ALL_TOOLS=1` — what `tool-surface-sweep-test` drives (51 calls) |
44
+ | **17** | advertised by default; the rest are reached through `reticle_run` |
45
+
46
+ 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).
47
+
48
+ | Token | Produced by | Consumed by | Invalidated by |
49
+ | --- | --- | --- | --- |
50
+ | `sessionId` | `reticle_sessions`, HELLO | ~every tool | tab close; **survives reload** via `sessionStorage` |
51
+ | `ref` (`e115`) | `snapshot`, `query`, `scroll_to` | `inspect`, `act`, `act_and_wait`, `state` | any re-render — refuses cleanly rather than clicking the new occupant |
52
+ | `actionId` (`a2`) | `act`, `act_and_wait` | `observe`, `network`, `console` filters | window expiry |
53
+ | `since` cursor | every read | every read | ring-buffer eviction |
54
+ | `capsuleSaved` | `act_and_wait` | `replay` | — |
55
+ | flow | `record_start` → acts → `flow_save_recorded` | `flow_verify`, `flow_replay`, `flow_heal`, `affected`, `gate` | DOM drift → `flow_heal` |
56
+ | baseline | `baseline_save`, `viewport` | `visual_diff`, `diff` | viewport change |
57
+ | `session_lease` | direct tool results | the agent (`yield` / `end`) | **dropped on the `reticle_run` path** (#119) |
58
+ | run artifact | `run_export`, `reticle verify` | CI, `reticle gate` | — |
59
+
60
+ Two structural facts:
61
+
62
+ - **`reticle_run` is a second dispatch path.** It routes through `runTool`, so telemetry is intact, but anything decorating results _above_ `runTool` silently does not apply. The session lease is the instance we know about; the next decorator inherits the same bug.
63
+ - **`record → save → verify → heal` is the only model-free loop**, and it is what makes a cheap gate possible at all.
64
+
65
+ ## 4. Fragility inventory
66
+
67
+ **Silent** = fails with no error surfaced to the agent or the user.
68
+
69
+ ### Hop 1 — client ↔ proxy
70
+
71
+ | Failure | Silent | Guarded by |
72
+ | --- | --- | --- |
73
+ | proxy killed externally (the `lsof -ti` recipe) | **yes, total** | nothing — needs `reticle kill` (#114). See [harness rule 1](../apps/e2e/harness-rules.md) |
74
+ | proxy uncaught exception | no | `installProxyResilience` + `proxyLog` |
75
+ | proxy exits when the reconnect budget is spent | _was the worst bug in the product_ | `onReconnectBudgetSpent` → DORMANT, never exit |
76
+ | client closes stdin with calls in flight | no | `SHUTDOWN_DRAIN_MS` (5s) |
77
+
78
+ ### Hop 2 — proxy ↔ daemon
79
+
80
+ Three distinct populations of unanswered call, **all covered**:
81
+
82
+ | Population | Answered by |
83
+ | --------------------------------------------- | ---------------------------------- |
84
+ | forwarded, then the SSE stream dropped | `streamLossReplies` → `-32001` |
85
+ | queued while dormant/reconnecting, never sent | `QUEUE_WAIT_MS` (20s) → `-32001` |
86
+ | POST-leg `ECONNRESET` while SSE is healthy | `postToSession` returns the reason |
87
+
88
+ Still fragile:
89
+
90
+ | Failure | Silent | Note |
91
+ | --- | --- | --- |
92
+ | squatter on `:4400` | partly | 12s handshake, then the first tool call diagnoses it |
93
+ | stale tool catalog served while dormant | **yes** | `ToolCatalogCache` answers `tools/list` from cache; after a version change a client can hold a catalog the daemon no longer implements |
94
+ | handshake replay regresses | **yes** | the daemon builds a fresh `McpServer` per SSE connection, so every `tools/call` would hit an uninitialized server |
95
+
96
+ ### Hop 3 — daemon lifecycle
97
+
98
+ | Failure | Silent | Note |
99
+ | --- | --- | --- |
100
+ | `serve` exits 0 on `EADDRINUSE` | **yes** | #115 — the parent reports the spawn, the child reports the bind, nothing joins them |
101
+ | liveness is the pid file, never the port | **yes** | `daemon/daemon.ts:156` |
102
+ | idle shutdown fires mid-install | **yes** | 5min base / 30min attached — already scored three fixtures as install failures |
103
+ | `isUselessDaemon` ends a daemon that served nothing | **yes** | lifetime facts, but a gate that installs for six minutes before its first call is that daemon |
104
+ | a daemon spawned inside a tool call is reaped when the call ends | **yes** | `SIGTERM` within ~1s, `detached` or not |
105
+ | no `daemon_alive` heartbeat | **fixed** | a killed daemon and a tidy exit used to read identically; the daemon now beats every 30s and `classifyDaemonLife` turns silence into `died_silently` (#123) |
106
+ | no port arbitration | **yes** | a second daemon can bind a port a first one held |
107
+
108
+ ### Hop 4 — bridge ↔ SDK
109
+
110
+ | Failure | Silent | Note |
111
+ | --- | --- | --- |
112
+ | protocol mismatch → `1008` | no, but **terminal** | the SDK stops retrying by design; the app is dark until reload (#127) |
113
+ | another project's daemon answers | no | `authFailureReason` — evidence-based, only claims it when the daemon has demonstrably served another project |
114
+ | `rejectAll('session disconnected')` | **fixed** | used to become `verified:"no" / assertion_failed` — Reticle blaming the app, by file and line, for its own lost connection. Now `verified:"unknown" / observation_lost` (#124) |
115
+ | reaper ends a quiet session | no | revivable on the agent's next action |
116
+ | opaque origins (`tauri://localhost`, `file://`) | no | kept verbatim in the allow-list rather than collapsed to `"null"` |
117
+
118
+ ### Hop 5 — SDK internals
119
+
120
+ | Failure | Silent | Note |
121
+ | --- | --- | --- |
122
+ | fixed 1s reconnect, no backoff, unbounded | **yes** | and the noise lands in the console channel we report to the agent (#116) |
123
+ | `onUnreachable` suppressed after the first success | **yes** | a **mid-session** outage never warns at all |
124
+ | offline queue overflow (`MAX_QUEUE = 500`) | no | gap marker stamped at the **oldest** drop — the honest floor for where the hole starts |
125
+ | `sessionStorage` throws in a sandboxed iframe | no | falls back; a session must still start |
126
+
127
+ ## 5. Where the gates are
128
+
129
+ | Layer | Class | Covered by | Gap |
130
+ | --- | --- | --- | --- |
131
+ | stdio | proxy survival | `mcp-survives-test` | external SIGKILL |
132
+ | SSE/POST | unanswered calls | `mcp-stress-test` | fault injection is `kill -9`, not specified toxics |
133
+ | daemon | lifecycle, idle, port | `daemon-lifecycle-test`, `status-honesty-test` | port arbitration, heartbeat |
134
+ | bridge | auth, origins, version | `version-skew-test` | desktop origins under install |
135
+ | SDK | queue, gaps, reconnect | browser unit tests | mid-session outage → verdict |
136
+ | tools | surface, schema, refusals | `tool-surface-sweep-test`, `tool-fuzz-test` | latency/failure budget |
137
+ | verdict | honesty | `release-smoke-test` | transport ≠ app (#124); false-green rate |
138
+ | telemetry | silent absence | `telemetry-events-test`, `telemetry-contract.test.ts` | chokepoint coverage |
139
+ | install | pristine-surface wiring | — | the whole class |
140
+ | client | per-agent MCP compatibility | — | the whole class |
141
+
142
+ Nine of eleven layers have a gate. The two that do not are exactly the two that need a second repo and a human. The plan for both is in [`gate-plan.md`](./gate-plan.md).
143
+
144
+ ## 6. Traces already encode the invariants
145
+
146
+ `runTool` opens one `tool.handler` root span per call and every stage underneath inherits it. With `RETICLE_TRACE=1` that makes three assertions available from any existing battery run, at the cost of one log parser:
147
+
148
+ - exactly one root span at `depth: 0` per tool call
149
+ - **zero orphan spans** — 23 orphans on one healthy run is the documented signature of a hung call
150
+ - `settleTiming` always fires; it sits in a `finally` so a thrown call cannot leak a concurrency slot
151
+
152
+ Today that output is produced and discarded.
@@ -33,7 +33,7 @@ Do **not** add telemetry inside a tool handler. If you find yourself wanting to,
33
33
 
34
34
  `bug_found` fires once per OCCURRENCE. A defect hit five times in a session is five events, which is the right raw signal — frequency is what says which classes of defect actually cost anybody anything. But it means a naive count answers "how often were defects hit", not "how many defects were found", while looking like it answers the second.
35
35
 
36
- So every `bug_found` carries **`repeat`**: false the first time a KIND is seen in a session, true after. Count `repeat: false` for **distinct defects**; count everything for **instances**. Measured on a real app: 7 events, 3 defects, 4 repeats. Publishing the 7 as defects would have inflated the claim by more than double.
36
+ So every `bug_found` carries **`repeat`**: false the first time a KIND is seen in a session, true after. Count `repeat: false` for **distinct defects**; count everything for **instances**. Measured on a real app, the instance count was more than double the defect count. Publishing instances as defects inflates the claim accordingly.
37
37
 
38
38
  The denominator is **`verification_completed`**, which fires per verdict with `via`, `verified`, `passed` and `falseGreenCaught`. Defects per verification is the honest rate; raw defect counts grow with usage and say nothing on their own.
39
39
 
@@ -98,10 +98,21 @@ The single exception is `daemon_stopped`, which is **awaited** — because the p
98
98
  | `feedback_submitted` | `reticle feedback`, or an agent's report | the qualitative channel |
99
99
  | `identified` | `reticle identify` | joins anonymous machine ids to a person who volunteered one |
100
100
  | `mcp_client_connected` | an MCP client attached | how many sessions are agent-driven at all |
101
+ | `app_instrumented` | the first app carrying the SDK reached this daemon | **the funnel step everything turns on** — see below |
101
102
  | `mcp_connection_lost` | the proxy lost its daemon | **the transport-stability metric.** The disconnect that makes a user reopen `/mcp` is invisible without it |
102
103
  | `init_completed` | `reticle init` finished | does install actually work, outside the fixtures gate |
103
104
  | `bug_found` | a defect was detected in the app under test | the value delivered, as opposed to the work done |
104
105
 
106
+ ## The install has two halves — `app_instrumented`
107
+
108
+ Reticle is only usable when both halves are done: the MCP server is registered so the agent has the tools, and the SDK is loaded by a running page so there is something for those tools to look at. They are done by different commands, at different times, often in different directories. Almost everyone completes the first. The second is where the users go.
109
+
110
+ Nothing measured the second. `daemon_started` and `mcp_client_connected` describe the agent half. `session_appConnects` describes the app half but is a **window counter** — it resets on every flush, so a user whose app connected in one window reads zero in every other. The population it under-counts is precisely the population being measured, and a funnel built on it reported fewer instrumented users than there were users calling tools, which is impossible on its face.
111
+
112
+ `app_instrumented` fires **once per daemon run**, on the first session-ready only, so `daemon_started` → `app_instrumented` is a rate rather than an inference and a reloading page cannot inflate it. It carries `initialized` (had `init` run here), `agentAttached` (was an agent already waiting), and `msToFirstApp` (how long the daemon sat with nothing wired). It deliberately carries no stack and no framework: `project_profiled` already reports both for the same run, and the two join on `sessionId`.
113
+
114
+ What it still cannot see is **why** an app never connected — every cause for that is page-side (the non-localhost gate, a port mismatch, a stale build, a dev server never restarted), where the daemon has no visibility. That needs the SDK to report its own refusals, and it is the next thing to build.
115
+
105
116
  ## Sessions: `daemon_stopped` vs `session_progress`
106
117
 
107
118
  Count sessions with **`daemon_stopped`** (`final: true`). It fires once, at a clean exit.
@@ -123,7 +134,8 @@ Four counters and one flag were added because the data could not answer question
123
134
  | `abandonedActions` | session summary | actions driven with no verdict AFTER them — the trailing unsettled run, not `actions - verifications`. That difference ignores order, so a verdict that drove nothing (a `flow_verify` over saved flows) silently paid for an abandoned action elsewhere. |
124
135
  | `verification.browser` | `verification_completed` | `headless` \| `headed` \| `attached` — who DROVE the browser. `attached` (Reticle launched nothing, the SDK connected from a browser somebody else opened) is the common case in production, so on its own this is mostly "somebody's own browser". |
125
136
  | `verification.brand` | `verification_completed` | WHICH browser it was: `chrome` \| `edge` \| `arc` \| `dia` \| `brave` \| `opera` \| `firefox` \| `safari` \| `other` — the closed `BrowserBrand` list in core. The axis `engine` cannot answer, since Chrome, Edge, Arc, Dia and Brave are all `blink`. The SDK reads `navigator.userAgentData.brands` (and the UA string on Firefox/Safari, which expose no `userAgentData`) and normalises IN THE PAGE: a raw brand or UA string is unbounded and fingerprintable and never leaves. Anything unrecognised is `other`. **Omitted rather than `"unknown"`** when the page did not say — a desktop webview has no brand and an older SDK does not report one, and a guess is indistinguishable from a measurement on a dashboard. |
126
- | `verification.reason` | `verification_completed` | WHICH clause of `decideVerified` produced the verdict, from core's closed `VerifiedReason`: `inconclusive` \| `assertion_failed` \| `contradicted` \| `already_true` \| `unclean_capture` \| `vacuous_grade` \| `outcome_pending` \| `outcome_unread` \| `unsettled` \| `proved`. See below. |
137
+ | `verification.reason` | `verification_completed` | WHICH clause of `decideVerified` produced the verdict, from core's closed `VerifiedReason`: `inconclusive` \| `observation_lost` \| `assertion_failed` \| `contradicted` \| `already_true` \| `unclean_capture` \| `vacuous_grade` \| `outcome_pending` \| `outcome_unread` \| `unsettled` \| `proved`. See below. |
138
+ | `verification.uncleanLoss` | `verification_completed` | WHAT was lost when `reason` is `unclean_capture`, from core's closed `CaptureLoss`: `buffer_loss` (our server ring buffer evicted evidence from the window) \| `transport_gap` (our browser queue overflowed) \| `blind_spot` (a boundary in the page — a cross-origin frame, a closed shadow root) \| `other`. Three owners, three fixes, and one bar on a dashboard until this existed. ONE value, not a list — a multi-value property is not something a breakdown can group by, so the first is sent, ours before the page's. **Absent whenever the capture was clean**, so its presence is itself the signal. Reported as `other` rather than omitted when the block says dirty and names nothing: a gap there would read as "no unclean verdicts happened". |
127
139
  | `bug.attribution` | `bug_found` | `app` \| `request` \| `reticle` — whose fault the defect was. **Absent means unclassified**, never `app`. See below. |
128
140
  | `outage.stage` / `outage.reason` / `outage.attempts` | `mcp_connection_lost` | which stage of the outage, why the stream went away (closed `OutageReason`, `other` for anything unnamed), and how many reconnects had been tried. See below. |
129
141
  | `tzOffsetMin` | every event | minutes offset from UTC. One integer, no location. |
@@ -131,7 +143,7 @@ Four counters and one flag were added because the data could not answer question
131
143
 
132
144
  ## Why a verdict came out that way — `verification.reason`
133
145
 
134
- `verified` has three values. The rule that produces it has **ten clauses**. Everything in between was thrown away at the moment it was known.
146
+ `verified` has three values. The rule that produces it has **eleven clauses**. Everything in between was thrown away at the moment it was known.
135
147
 
136
148
  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.
137
149